diff --git a/libs/flake/CMakeLists.txt b/libs/flake/CMakeLists.txt index bef6be2f23..107f653c92 100644 --- a/libs/flake/CMakeLists.txt +++ b/libs/flake/CMakeLists.txt @@ -1,228 +1,230 @@ project(kritaflake) include_directories( ${CMAKE_SOURCE_DIR}/libs/flake/commands ${CMAKE_SOURCE_DIR}/libs/flake/tools ${CMAKE_SOURCE_DIR}/libs/flake/svg ${CMAKE_BINARY_DIR}/libs/flake ) add_subdirectory(styles) add_subdirectory(tests) set(kritaflake_SRCS KoGradientHelper.cpp KoFlake.cpp KoCanvasBase.cpp KoResourceManager_p.cpp KoDerivedResourceConverter.cpp KoResourceUpdateMediator.cpp KoCanvasResourceManager.cpp KoDocumentResourceManager.cpp KoCanvasObserverBase.cpp KoCanvasSupervisor.cpp KoDockFactoryBase.cpp KoDockRegistry.cpp KoDataCenterBase.cpp KoInsets.cpp KoPathShape.cpp KoPathPoint.cpp KoPathSegment.cpp KoSelection.cpp KoSelectedShapesProxy.cpp KoSelectedShapesProxySimple.cpp KoShape.cpp KoShapeAnchor.cpp KoShapeBasedDocumentBase.cpp KoShapeApplicationData.cpp KoShapeContainer.cpp KoShapeContainerModel.cpp KoShapeGroup.cpp KoShapeManager.cpp KoShapePaintingContext.cpp KoFrameShape.cpp KoUnavailShape.cpp KoMarker.cpp KoMarkerCollection.cpp KoToolBase.cpp KoCanvasController.cpp KoCanvasControllerWidget.cpp KoCanvasControllerWidgetViewport_p.cpp KoShapeRegistry.cpp KoDeferredShapeFactoryBase.cpp KoToolFactoryBase.cpp KoPathShapeFactory.cpp KoShapeFactoryBase.cpp KoShapeUserData.cpp KoParameterShape.cpp KoPointerEvent.cpp KoShapeController.cpp KoToolSelection.cpp KoShapeLayer.cpp KoPostscriptPaintDevice.cpp KoInputDevice.cpp KoToolManager_p.cpp KoToolManager.cpp KoToolRegistry.cpp KoToolProxy.cpp KoShapeSavingContext.cpp KoShapeLoadingContext.cpp KoLoadingShapeUpdater.cpp KoPathShapeLoader.cpp KoShapeStrokeModel.cpp KoShapeStroke.cpp KoShapeBackground.cpp KoColorBackground.cpp KoGradientBackground.cpp KoOdfGradientBackground.cpp KoHatchBackground.cpp KoPatternBackground.cpp KoVectorPatternBackground.cpp KoShapeConfigWidgetBase.cpp KoDrag.cpp KoSvgPaste.cpp KoDragOdfSaveHelper.cpp KoShapeOdfSaveHelper.cpp KoConnectionPoint.cpp KoConnectionShape.cpp KoConnectionShapeLoadingUpdater.cpp KoConnectionShapeFactory.cpp KoConnectionShapeConfigWidget.cpp KoSnapGuide.cpp KoSnapProxy.cpp KoSnapStrategy.cpp KoSnapData.cpp KoShapeShadow.cpp KoSharedLoadingData.cpp KoSharedSavingData.cpp KoViewConverter.cpp KoInputDeviceHandler.cpp KoInputDeviceHandlerEvent.cpp KoInputDeviceHandlerRegistry.cpp KoImageData.cpp KoImageData_p.cpp KoImageCollection.cpp KoOdfWorkaround.cpp KoFilterEffect.cpp KoFilterEffectStack.cpp KoFilterEffectFactoryBase.cpp KoFilterEffectRegistry.cpp KoFilterEffectConfigWidgetBase.cpp KoFilterEffectRenderContext.cpp KoFilterEffectLoadingContext.cpp KoTextShapeDataBase.cpp KoTosContainer.cpp KoTosContainerModel.cpp KoClipPath.cpp KoClipMask.cpp KoClipMaskPainter.cpp KoCurveFit.cpp commands/KoShapeGroupCommand.cpp commands/KoShapeAlignCommand.cpp commands/KoShapeBackgroundCommand.cpp commands/KoShapeCreateCommand.cpp commands/KoShapeDeleteCommand.cpp commands/KoShapeDistributeCommand.cpp commands/KoShapeLockCommand.cpp commands/KoShapeMoveCommand.cpp commands/KoShapeResizeCommand.cpp commands/KoShapeShearCommand.cpp commands/KoShapeSizeCommand.cpp commands/KoShapeStrokeCommand.cpp commands/KoShapeUngroupCommand.cpp commands/KoShapeReorderCommand.cpp commands/KoShapeKeepAspectRatioCommand.cpp commands/KoPathBaseCommand.cpp commands/KoPathPointMoveCommand.cpp commands/KoPathControlPointMoveCommand.cpp commands/KoPathPointTypeCommand.cpp commands/KoPathPointRemoveCommand.cpp commands/KoPathPointInsertCommand.cpp commands/KoPathSegmentBreakCommand.cpp commands/KoPathBreakAtPointCommand.cpp commands/KoPathSegmentTypeCommand.cpp commands/KoPathCombineCommand.cpp commands/KoSubpathRemoveCommand.cpp commands/KoSubpathJoinCommand.cpp commands/KoParameterHandleMoveCommand.cpp commands/KoParameterToPathCommand.cpp commands/KoShapeTransformCommand.cpp commands/KoPathFillRuleCommand.cpp commands/KoConnectionShapeTypeCommand.cpp commands/KoShapeShadowCommand.cpp commands/KoPathReverseCommand.cpp commands/KoShapeRenameCommand.cpp commands/KoShapeRunAroundCommand.cpp commands/KoPathPointMergeCommand.cpp commands/KoShapeTransparencyCommand.cpp commands/KoShapeClipCommand.cpp commands/KoShapeUnclipCommand.cpp commands/KoPathShapeMarkerCommand.cpp commands/KoShapeConnectionChangeCommand.cpp + commands/KoMultiPathPointMergeCommand.cpp + commands/KoMultiPathPointJoinCommand.cpp tools/KoCreateShapeStrategy.cpp tools/KoPathToolFactory.cpp tools/KoPathTool.cpp tools/KoPathToolSelection.cpp tools/KoPathToolHandle.cpp tools/PathToolOptionWidget.cpp tools/KoPathPointRubberSelectStrategy.cpp tools/KoPathPointMoveStrategy.cpp tools/KoPathConnectionPointStrategy.cpp tools/KoPathControlPointMoveStrategy.cpp tools/KoParameterChangeStrategy.cpp tools/KoZoomTool.cpp tools/KoZoomToolFactory.cpp tools/KoZoomToolWidget.cpp tools/KoZoomStrategy.cpp tools/KoPanTool.cpp tools/KoPanToolFactory.cpp tools/KoInteractionTool.cpp tools/KoInteractionStrategy.cpp tools/KoInteractionStrategyFactory.cpp tools/KoCreateShapesTool.cpp tools/KoCreateShapesToolFactory.cpp tools/KoShapeRubberSelectStrategy.cpp tools/KoPathSegmentChangeStrategy.cpp svg/KoShapePainter.cpp svg/SvgUtil.cpp svg/SvgGraphicContext.cpp svg/SvgSavingContext.cpp svg/SvgWriter.cpp svg/SvgStyleWriter.cpp svg/SvgShape.cpp svg/SvgParser.cpp svg/SvgStyleParser.cpp svg/SvgGradientHelper.cpp svg/SvgFilterHelper.cpp svg/SvgCssHelper.cpp svg/SvgClipPathHelper.cpp svg/SvgLoadingContext.cpp svg/SvgShapeFactory.cpp svg/parsers/SvgTransformParser.cpp FlakeDebug.cpp ) ki18n_wrap_ui(kritaflake_SRCS tools/PathToolOptionWidgetBase.ui KoConnectionShapeConfigWidget.ui tools/KoZoomToolWidget.ui ) add_library(kritaflake SHARED ${kritaflake_SRCS}) generate_export_header(kritaflake BASE_NAME kritaflake) target_include_directories(kritaflake PUBLIC $ $ $ ) 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} ) install(TARGETS kritaflake ${INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/libs/flake/KoPathShape.cpp b/libs/flake/KoPathShape.cpp index a6a4aeab11..e3a678294a 100644 --- a/libs/flake/KoPathShape.cpp +++ b/libs/flake/KoPathShape.cpp @@ -1,1673 +1,1675 @@ /* This file is part of the KDE project Copyright (C) 2006-2008, 2010-2011 Thorsten Zachmann Copyright (C) 2006-2011 Jan Hambrecht Copyright (C) 2007-2009 Thomas Zander Copyright (C) 2011 Jean-Nicolas Artaud This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoPathShape.h" #include "KoPathShape_p.h" #include "KoPathSegment.h" #include "KoOdfWorkaround.h" #include "KoPathPoint.h" #include "KoShapeStrokeModel.h" #include "KoViewConverter.h" #include "KoPathShapeLoader.h" #include "KoShapeSavingContext.h" #include "KoShapeLoadingContext.h" #include "KoShapeShadow.h" #include "KoShapeBackground.h" #include "KoShapeContainer.h" #include "KoFilterEffectStack.h" #include "KoMarker.h" #include "KoShapeStroke.h" #include "KoInsets.h" #include #include #include #include #include #include #include #include #include #include "kis_global.h" #include // for qIsNaN static bool qIsNaNPoint(const QPointF &p) { return qIsNaN(p.x()) || qIsNaN(p.y()); } static const qreal DefaultMarkerWidth = 3.0; KoPathShapePrivate::KoPathShapePrivate(KoPathShape *q) : KoTosContainerPrivate(q), fillRule(Qt::OddEvenFill), autoFillMarkers(false) { } KoPathShapePrivate::KoPathShapePrivate(const KoPathShapePrivate &rhs, KoPathShape *q) : KoTosContainerPrivate(rhs, q), fillRule(rhs.fillRule), markersNew(rhs.markersNew), autoFillMarkers(rhs.autoFillMarkers) { Q_FOREACH (KoSubpath *subPath, rhs.subpaths) { KoSubpath *clonedSubPath = new KoSubpath(); Q_FOREACH (KoPathPoint *point, *subPath) { *clonedSubPath << new KoPathPoint(*point, q); } subpaths << clonedSubPath; } } QRectF KoPathShapePrivate::handleRect(const QPointF &p, qreal radius) const { return QRectF(p.x() - radius, p.y() - radius, 2*radius, 2*radius); } void KoPathShapePrivate::applyViewboxTransformation(const KoXmlElement &element) { // apply viewbox transformation const QRect viewBox = KoPathShape::loadOdfViewbox(element); if (! viewBox.isEmpty()) { // load the desired size QSizeF size; size.setWidth(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "width", QString()))); size.setHeight(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "height", QString()))); // load the desired position QPointF pos; pos.setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x", QString()))); pos.setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y", QString()))); // create matrix to transform original path data into desired size and position QTransform viewMatrix; viewMatrix.translate(-viewBox.left(), -viewBox.top()); viewMatrix.scale(size.width() / viewBox.width(), size.height() / viewBox.height()); viewMatrix.translate(pos.x(), pos.y()); // transform the path data map(viewMatrix); } } KoPathShape::KoPathShape() :KoTosContainer(new KoPathShapePrivate(this)) { } KoPathShape::KoPathShape(KoPathShapePrivate *dd) : KoTosContainer(dd) { } KoPathShape::KoPathShape(const KoPathShape &rhs) : KoTosContainer(new KoPathShapePrivate(*rhs.d_func(), this)) { } KoPathShape::~KoPathShape() { clear(); } KoShape *KoPathShape::cloneShape() const { return new KoPathShape(*this); } void KoPathShape::saveContourOdf(KoShapeSavingContext &context, const QSizeF &scaleFactor) const { Q_D(const KoPathShape); if (d->subpaths.length() <= 1) { QTransform matrix; matrix.scale(scaleFactor.width(), scaleFactor.height()); QString points; KoSubpath *subPath = d->subpaths.first(); KoSubpath::const_iterator pointIt(subPath->constBegin()); KoPathPoint *currPoint= 0; // iterate over all points for (; pointIt != subPath->constEnd(); ++pointIt) { currPoint = *pointIt; if (currPoint->activeControlPoint1() || currPoint->activeControlPoint2()) { break; } const QPointF p = matrix.map(currPoint->point()); points += QString("%1,%2 ").arg(qRound(1000*p.x())).arg(qRound(1000*p.y())); } if (currPoint && !(currPoint->activeControlPoint1() || currPoint->activeControlPoint2())) { context.xmlWriter().startElement("draw:contour-polygon"); context.xmlWriter().addAttributePt("svg:width", size().width()); context.xmlWriter().addAttributePt("svg:height", size().height()); const QSizeF s(size()); QString viewBox = QString("0 0 %1 %2").arg(qRound(1000*s.width())).arg(qRound(1000*s.height())); context.xmlWriter().addAttribute("svg:viewBox", viewBox); context.xmlWriter().addAttribute("draw:points", points); context.xmlWriter().addAttribute("draw:recreate-on-edit", "true"); context.xmlWriter().endElement(); return; } } // if we get here we couldn't save as polygon - let-s try contour-path context.xmlWriter().startElement("draw:contour-path"); saveOdfAttributes(context, OdfViewbox); context.xmlWriter().addAttribute("svg:d", toString()); context.xmlWriter().addAttribute("calligra:nodeTypes", d->nodeTypes()); context.xmlWriter().addAttribute("draw:recreate-on-edit", "true"); context.xmlWriter().endElement(); } void KoPathShape::saveOdf(KoShapeSavingContext & context) const { Q_D(const KoPathShape); context.xmlWriter().startElement("draw:path"); saveOdfAttributes(context, OdfAllAttributes | OdfViewbox); context.xmlWriter().addAttribute("svg:d", toString()); context.xmlWriter().addAttribute("calligra:nodeTypes", d->nodeTypes()); saveOdfCommonChildElements(context); saveText(context); context.xmlWriter().endElement(); } bool KoPathShape::loadContourOdf(const KoXmlElement &element, KoShapeLoadingContext &, const QSizeF &scaleFactor) { Q_D(KoPathShape); // first clear the path data from the default path clear(); if (element.localName() == "contour-polygon") { QString points = element.attributeNS(KoXmlNS::draw, "points").simplified(); points.replace(',', ' '); points.remove('\r'); points.remove('\n'); bool firstPoint = true; const QStringList coordinateList = points.split(' '); for (QStringList::ConstIterator it = coordinateList.constBegin(); it != coordinateList.constEnd(); ++it) { QPointF point; point.setX((*it).toDouble()); ++it; point.setY((*it).toDouble()); if (firstPoint) { moveTo(point); firstPoint = false; } else lineTo(point); } close(); } else if (element.localName() == "contour-path") { KoPathShapeLoader loader(this); loader.parseSvg(element.attributeNS(KoXmlNS::svg, "d"), true); d->loadNodeTypes(element); } // apply viewbox transformation const QRect viewBox = KoPathShape::loadOdfViewbox(element); if (! viewBox.isEmpty()) { QSizeF size; size.setWidth(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "width", QString()))); size.setHeight(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "height", QString()))); // create matrix to transform original path data into desired size and position QTransform viewMatrix; viewMatrix.translate(-viewBox.left(), -viewBox.top()); viewMatrix.scale(scaleFactor.width(), scaleFactor.height()); viewMatrix.scale(size.width() / viewBox.width(), size.height() / viewBox.height()); // transform the path data d->map(viewMatrix); } setTransformation(QTransform()); return true; } bool KoPathShape::loadOdf(const KoXmlElement & element, KoShapeLoadingContext &context) { Q_D(KoPathShape); loadOdfAttributes(element, context, OdfMandatories | OdfAdditionalAttributes | OdfCommonChildElements); // first clear the path data from the default path clear(); if (element.localName() == "line") { QPointF start; start.setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x1", ""))); start.setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y1", ""))); QPointF end; end.setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x2", ""))); end.setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y2", ""))); moveTo(start); lineTo(end); } else if (element.localName() == "polyline" || element.localName() == "polygon") { QString points = element.attributeNS(KoXmlNS::draw, "points").simplified(); points.replace(',', ' '); points.remove('\r'); points.remove('\n'); bool firstPoint = true; const QStringList coordinateList = points.split(' '); for (QStringList::ConstIterator it = coordinateList.constBegin(); it != coordinateList.constEnd(); ++it) { QPointF point; point.setX((*it).toDouble()); ++it; point.setY((*it).toDouble()); if (firstPoint) { moveTo(point); firstPoint = false; } else lineTo(point); } if (element.localName() == "polygon") close(); } else { // path loading KoPathShapeLoader loader(this); loader.parseSvg(element.attributeNS(KoXmlNS::svg, "d"), true); d->loadNodeTypes(element); } d->applyViewboxTransformation(element); QPointF pos = normalize(); setTransformation(QTransform()); if (element.hasAttributeNS(KoXmlNS::svg, "x") || element.hasAttributeNS(KoXmlNS::svg, "y")) { pos.setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x", QString()))); pos.setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y", QString()))); } setPosition(pos); loadOdfAttributes(element, context, OdfTransformation); // now that the correct transformation is set up // apply that matrix to the path geometry so that // we don't transform the stroke d->map(transformation()); setTransformation(QTransform()); normalize(); loadText(element, context); return true; } QString KoPathShape::saveStyle(KoGenStyle &style, KoShapeSavingContext &context) const { Q_D(const KoPathShape); style.addProperty("svg:fill-rule", d->fillRule == Qt::OddEvenFill ? "evenodd" : "nonzero"); QSharedPointer lineBorder = qSharedPointerDynamicCast(stroke()); qreal lineWidth = 0; if (lineBorder) { lineWidth = lineBorder->lineWidth(); } Q_UNUSED(lineWidth) return KoTosContainer::saveStyle(style, context); } void KoPathShape::loadStyle(const KoXmlElement & element, KoShapeLoadingContext &context) { Q_D(KoPathShape); KoTosContainer::loadStyle(element, context); KoStyleStack &styleStack = context.odfLoadingContext().styleStack(); styleStack.setTypeProperties("graphic"); if (styleStack.hasProperty(KoXmlNS::svg, "fill-rule")) { QString rule = styleStack.property(KoXmlNS::svg, "fill-rule"); d->fillRule = (rule == "nonzero") ? Qt::WindingFill : Qt::OddEvenFill; } else { d->fillRule = Qt::WindingFill; #ifndef NWORKAROUND_ODF_BUGS KoOdfWorkaround::fixMissingFillRule(d->fillRule, context); #endif } QSharedPointer lineBorder = qSharedPointerDynamicCast(stroke()); qreal lineWidth = 0; if (lineBorder) { lineWidth = lineBorder->lineWidth(); } Q_UNUSED(lineWidth); } QRect KoPathShape::loadOdfViewbox(const KoXmlElement & element) { QRect viewbox; QString data = element.attributeNS(KoXmlNS::svg, QLatin1String("viewBox")); if (! data.isEmpty()) { data.replace(QLatin1Char(','), QLatin1Char(' ')); const QStringList coordinates = data.simplified().split(QLatin1Char(' '), QString::SkipEmptyParts); if (coordinates.count() == 4) { viewbox.setRect(coordinates.at(0).toInt(), coordinates.at(1).toInt(), coordinates.at(2).toInt(), coordinates.at(3).toInt()); } } return viewbox; } void KoPathShape::clear() { Q_D(KoPathShape); Q_FOREACH (KoSubpath *subpath, d->subpaths) { Q_FOREACH (KoPathPoint *point, *subpath) delete point; delete subpath; } d->subpaths.clear(); } void KoPathShape::paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { Q_D(KoPathShape); applyConversion(painter, converter); QPainterPath path(outline()); path.setFillRule(d->fillRule); if (background()) { background()->paint(painter, converter, paintContext, path); } //d->paintDebug(painter); } #ifndef NDEBUG void KoPathShapePrivate::paintDebug(QPainter &painter) { Q_Q(KoPathShape); KoSubpathList::const_iterator pathIt(subpaths.constBegin()); int i = 0; QPen pen(Qt::black, 0); painter.save(); painter.setPen(pen); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { ++i; KoPathPoint *point = (*it); QRectF r(point->point(), QSizeF(5, 5)); r.translate(-2.5, -2.5); QPen pen(Qt::black, 0); painter.setPen(pen); if (point->activeControlPoint1() && point->activeControlPoint2()) { QBrush b(Qt::red); painter.setBrush(b); } else if (point->activeControlPoint1()) { QBrush b(Qt::yellow); painter.setBrush(b); } else if (point->activeControlPoint2()) { QBrush b(Qt::darkYellow); painter.setBrush(b); } painter.drawEllipse(r); } } painter.restore(); debugFlake << "nop =" << i; } void KoPathShapePrivate::debugPath() const { Q_Q(const KoPathShape); KoSubpathList::const_iterator pathIt(subpaths.constBegin()); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { debugFlake << "p:" << (*pathIt) << "," << *it << "," << (*it)->point() << "," << (*it)->properties(); } } } #endif void KoPathShape::paintPoints(KisHandlePainterHelper &handlesHelper) { Q_D(KoPathShape); KoSubpathList::const_iterator pathIt(d->subpaths.constBegin()); for (; pathIt != d->subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) (*it)->paint(handlesHelper, KoPathPoint::Node); } } QRectF KoPathShape::outlineRect() const { return outline().boundingRect(); } QPainterPath KoPathShape::outline() const { Q_D(const KoPathShape); QPainterPath path; Q_FOREACH (KoSubpath * subpath, d->subpaths) { KoPathPoint * lastPoint = subpath->first(); bool activeCP = false; Q_FOREACH (KoPathPoint * currPoint, *subpath) { KoPathPoint::PointProperties currProperties = currPoint->properties(); if (currPoint == subpath->first()) { if (currProperties & KoPathPoint::StartSubpath) { Q_ASSERT(!qIsNaNPoint(currPoint->point())); path.moveTo(currPoint->point()); } } else if (activeCP && currPoint->activeControlPoint1()) { Q_ASSERT(!qIsNaNPoint(lastPoint->controlPoint2())); Q_ASSERT(!qIsNaNPoint(currPoint->controlPoint1())); Q_ASSERT(!qIsNaNPoint(currPoint->point())); path.cubicTo( lastPoint->controlPoint2(), currPoint->controlPoint1(), currPoint->point()); } else if (activeCP || currPoint->activeControlPoint1()) { Q_ASSERT(!qIsNaNPoint(lastPoint->controlPoint2())); Q_ASSERT(!qIsNaNPoint(currPoint->controlPoint1())); path.quadTo( activeCP ? lastPoint->controlPoint2() : currPoint->controlPoint1(), currPoint->point()); } else { Q_ASSERT(!qIsNaNPoint(currPoint->point())); path.lineTo(currPoint->point()); } if (currProperties & KoPathPoint::CloseSubpath && currProperties & KoPathPoint::StopSubpath) { // add curve when there is a curve on the way to the first point KoPathPoint * firstPoint = subpath->first(); Q_ASSERT(!qIsNaNPoint(firstPoint->point())); if (currPoint->activeControlPoint2() && firstPoint->activeControlPoint1()) { path.cubicTo( currPoint->controlPoint2(), firstPoint->controlPoint1(), firstPoint->point()); } else if (currPoint->activeControlPoint2() || firstPoint->activeControlPoint1()) { Q_ASSERT(!qIsNaNPoint(currPoint->point())); Q_ASSERT(!qIsNaNPoint(currPoint->controlPoint1())); path.quadTo( currPoint->activeControlPoint2() ? currPoint->controlPoint2() : firstPoint->controlPoint1(), firstPoint->point()); } path.closeSubpath(); } if (currPoint->activeControlPoint2()) { activeCP = true; } else { activeCP = false; } lastPoint = currPoint; } } return path; } QRectF KoPathShape::boundingRect() const { QTransform transform = absoluteTransformation(0); // calculate the bounding rect of the transformed outline QRectF bb; const QSharedPointer lineBorder = qSharedPointerDynamicCast(stroke()); QPen pen; if (lineBorder) { pen.setWidthF(lineBorder->lineWidth()); } bb = transform.map(pathStroke(pen)).boundingRect(); if (stroke()) { KoInsets inset; stroke()->strokeInsets(this, inset); // calculate transformed border insets QPointF center = transform.map(QPointF()); QPointF tl = transform.map(QPointF(-inset.left,-inset.top)) - center; QPointF br = transform.map(QPointF(inset.right,inset.bottom)) -center; qreal left = qMin(tl.x(),br.x()); qreal right = qMax(tl.x(),br.x()); 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; shadow()->insets(insets); bb.adjust(-insets.left, -insets.top, insets.right, insets.bottom); } if (filterEffectStack()) { QRectF clipRect = filterEffectStack()->clipRectForBoundingRect(QRectF(QPointF(), size())); bb |= transform.mapRect(clipRect); } return bb; } QSizeF KoPathShape::size() const { // don't call boundingRect here as it uses absoluteTransformation // which itself uses size() -> leads to infinite reccursion return outlineRect().size(); } void KoPathShape::setSize(const QSizeF &newSize) { Q_D(KoPathShape); QTransform matrix(resizeMatrix(newSize)); KoShape::setSize(newSize); d->map(matrix); } QTransform KoPathShape::resizeMatrix(const QSizeF & newSize) const { QSizeF oldSize = size(); if (oldSize.width() == 0.0) { oldSize.setWidth(0.000001); } if (oldSize.height() == 0.0) { oldSize.setHeight(0.000001); } QSizeF sizeNew(newSize); if (sizeNew.width() == 0.0) { sizeNew.setWidth(0.000001); } if (sizeNew.height() == 0.0) { sizeNew.setHeight(0.000001); } return QTransform(sizeNew.width() / oldSize.width(), 0, 0, sizeNew.height() / oldSize.height(), 0, 0); } KoPathPoint * KoPathShape::moveTo(const QPointF &p) { Q_D(KoPathShape); KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StartSubpath | KoPathPoint::StopSubpath); KoSubpath * path = new KoSubpath; path->push_back(point); d->subpaths.push_back(path); return point; } KoPathPoint * KoPathShape::lineTo(const QPointF &p) { Q_D(KoPathShape); if (d->subpaths.empty()) { moveTo(QPointF(0, 0)); } KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StopSubpath); KoPathPoint * lastPoint = d->subpaths.last()->last(); d->updateLast(&lastPoint); d->subpaths.last()->push_back(point); return point; } KoPathPoint * KoPathShape::curveTo(const QPointF &c1, const QPointF &c2, const QPointF &p) { Q_D(KoPathShape); if (d->subpaths.empty()) { moveTo(QPointF(0, 0)); } KoPathPoint * lastPoint = d->subpaths.last()->last(); d->updateLast(&lastPoint); lastPoint->setControlPoint2(c1); KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StopSubpath); point->setControlPoint1(c2); d->subpaths.last()->push_back(point); return point; } KoPathPoint * KoPathShape::curveTo(const QPointF &c, const QPointF &p) { Q_D(KoPathShape); if (d->subpaths.empty()) moveTo(QPointF(0, 0)); KoPathPoint * lastPoint = d->subpaths.last()->last(); d->updateLast(&lastPoint); lastPoint->setControlPoint2(c); KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StopSubpath); d->subpaths.last()->push_back(point); return point; } KoPathPoint * KoPathShape::arcTo(qreal rx, qreal ry, qreal startAngle, qreal sweepAngle) { Q_D(KoPathShape); if (d->subpaths.empty()) { moveTo(QPointF(0, 0)); } KoPathPoint * lastPoint = d->subpaths.last()->last(); if (lastPoint->properties() & KoPathPoint::CloseSubpath) { lastPoint = d->subpaths.last()->first(); } QPointF startpoint(lastPoint->point()); KoPathPoint * newEndPoint = lastPoint; QPointF curvePoints[12]; int pointCnt = arcToCurve(rx, ry, startAngle, sweepAngle, startpoint, curvePoints); for (int i = 0; i < pointCnt; i += 3) { newEndPoint = curveTo(curvePoints[i], curvePoints[i+1], curvePoints[i+2]); } return newEndPoint; } int KoPathShape::arcToCurve(qreal rx, qreal ry, qreal startAngle, qreal sweepAngle, const QPointF & offset, QPointF * curvePoints) const { int pointCnt = 0; // check Parameters if (sweepAngle == 0.0) return pointCnt; sweepAngle = qBound(-360.0, sweepAngle, 360.0); if (rx == 0 || ry == 0) { //TODO } // split angles bigger than 90° so that it gives a good aproximation to the circle qreal parts = ceil(qAbs(sweepAngle / 90.0)); qreal sa_rad = startAngle * M_PI / 180.0; qreal partangle = sweepAngle / parts; qreal endangle = startAngle + partangle; qreal se_rad = endangle * M_PI / 180.0; qreal sinsa = sin(sa_rad); qreal cossa = cos(sa_rad); qreal kappa = 4.0 / 3.0 * tan((se_rad - sa_rad) / 4); // startpoint is at the last point is the path but when it is closed // it is at the first point QPointF startpoint(offset); //center berechnen QPointF center(startpoint - QPointF(cossa * rx, -sinsa * ry)); //debugFlake <<"kappa" << kappa <<"parts" << parts; for (int part = 0; part < parts; ++part) { // start tangent curvePoints[pointCnt++] = QPointF(startpoint - QPointF(sinsa * rx * kappa, cossa * ry * kappa)); qreal sinse = sin(se_rad); qreal cosse = cos(se_rad); // end point QPointF endpoint(center + QPointF(cosse * rx, -sinse * ry)); // end tangent curvePoints[pointCnt++] = QPointF(endpoint - QPointF(-sinse * rx * kappa, -cosse * ry * kappa)); curvePoints[pointCnt++] = endpoint; // set the endpoint as next start point startpoint = endpoint; sinsa = sinse; cossa = cosse; endangle += partangle; se_rad = endangle * M_PI / 180.0; } return pointCnt; } void KoPathShape::close() { Q_D(KoPathShape); if (d->subpaths.empty()) { return; } d->closeSubpath(d->subpaths.last()); } void KoPathShape::closeMerge() { Q_D(KoPathShape); if (d->subpaths.empty()) { return; } d->closeMergeSubpath(d->subpaths.last()); } QPointF KoPathShape::normalize() { Q_D(KoPathShape); QPointF tl(outline().boundingRect().topLeft()); QTransform matrix; matrix.translate(-tl.x(), -tl.y()); d->map(matrix); // keep the top left point of the object applyTransformation(matrix.inverted()); d->shapeChanged(ContentChanged); return tl; } void KoPathShapePrivate::map(const QTransform &matrix) { Q_Q(KoPathShape); KoSubpathList::const_iterator pathIt(subpaths.constBegin()); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { (*it)->map(matrix); } } } void KoPathShapePrivate::updateLast(KoPathPoint **lastPoint) { Q_Q(KoPathShape); // check if we are about to add a new point to a closed subpath if ((*lastPoint)->properties() & KoPathPoint::StopSubpath && (*lastPoint)->properties() & KoPathPoint::CloseSubpath) { // get the first point of the subpath KoPathPoint *subpathStart = subpaths.last()->first(); // clone the first point of the subpath... KoPathPoint * newLastPoint = new KoPathPoint(*subpathStart, q); // ... and make it a normal point newLastPoint->setProperties(KoPathPoint::Normal); // now start a new subpath with the cloned start point KoSubpath *path = new KoSubpath; path->push_back(newLastPoint); subpaths.push_back(path); *lastPoint = newLastPoint; } else { // the subpath was not closed so the formerly last point // of the subpath is no end point anymore (*lastPoint)->unsetProperty(KoPathPoint::StopSubpath); } (*lastPoint)->unsetProperty(KoPathPoint::CloseSubpath); } QList KoPathShape::pointsAt(const QRectF &r) const { Q_D(const KoPathShape); QList result; KoSubpathList::const_iterator pathIt(d->subpaths.constBegin()); for (; pathIt != d->subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { if (r.contains((*it)->point())) result.append(*it); else if ((*it)->activeControlPoint1() && r.contains((*it)->controlPoint1())) result.append(*it); else if ((*it)->activeControlPoint2() && r.contains((*it)->controlPoint2())) result.append(*it); } } return result; } QList KoPathShape::segmentsAt(const QRectF &r) const { Q_D(const KoPathShape); QList segments; int subpathCount = d->subpaths.count(); for (int subpathIndex = 0; subpathIndex < subpathCount; ++subpathIndex) { KoSubpath * subpath = d->subpaths[subpathIndex]; int pointCount = subpath->count(); bool subpathClosed = isClosedSubpath(subpathIndex); for (int pointIndex = 0; pointIndex < pointCount; ++pointIndex) { if (pointIndex == (pointCount - 1) && ! subpathClosed) break; KoPathSegment s(subpath->at(pointIndex), subpath->at((pointIndex + 1) % pointCount)); QRectF controlRect = s.controlPointRect(); if (! r.intersects(controlRect) && ! controlRect.contains(r)) continue; QRectF bound = s.boundingRect(); if (! r.intersects(bound) && ! bound.contains(r)) continue; segments.append(s); } } return segments; } KoPathPointIndex KoPathShape::pathPointIndex(const KoPathPoint *point) const { Q_D(const KoPathShape); for (int subpathIndex = 0; subpathIndex < d->subpaths.size(); ++subpathIndex) { KoSubpath * subpath = d->subpaths.at(subpathIndex); for (int pointPos = 0; pointPos < subpath->size(); ++pointPos) { if (subpath->at(pointPos) == point) { return KoPathPointIndex(subpathIndex, pointPos); } } } return KoPathPointIndex(-1, -1); } KoPathPoint * KoPathShape::pointByIndex(const KoPathPointIndex &pointIndex) const { Q_D(const KoPathShape); KoSubpath *subpath = d->subPath(pointIndex.first); if (subpath == 0 || pointIndex.second < 0 || pointIndex.second >= subpath->size()) return 0; return subpath->at(pointIndex.second); } KoPathSegment KoPathShape::segmentByIndex(const KoPathPointIndex &pointIndex) const { Q_D(const KoPathShape); KoPathSegment segment(0, 0); KoSubpath *subpath = d->subPath(pointIndex.first); if (subpath != 0 && pointIndex.second >= 0 && pointIndex.second < subpath->size()) { KoPathPoint * point = subpath->at(pointIndex.second); int index = pointIndex.second; // check if we have a (closing) segment starting from the last point if ((index == subpath->size() - 1) && point->properties() & KoPathPoint::CloseSubpath) index = 0; else ++index; if (index < subpath->size()) { segment = KoPathSegment(point, subpath->at(index)); } } return segment; } int KoPathShape::pointCount() const { Q_D(const KoPathShape); int i = 0; KoSubpathList::const_iterator pathIt(d->subpaths.constBegin()); for (; pathIt != d->subpaths.constEnd(); ++pathIt) { i += (*pathIt)->size(); } return i; } int KoPathShape::subpathCount() const { Q_D(const KoPathShape); return d->subpaths.count(); } int KoPathShape::subpathPointCount(int subpathIndex) const { Q_D(const KoPathShape); KoSubpath *subpath = d->subPath(subpathIndex); if (subpath == 0) return -1; return subpath->size(); } bool KoPathShape::isClosedSubpath(int subpathIndex) const { Q_D(const KoPathShape); KoSubpath *subpath = d->subPath(subpathIndex); if (subpath == 0) return false; const bool firstClosed = subpath->first()->properties() & KoPathPoint::CloseSubpath; const bool lastClosed = subpath->last()->properties() & KoPathPoint::CloseSubpath; return firstClosed && lastClosed; } bool KoPathShape::insertPoint(KoPathPoint* point, const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(pointIndex.first); if (subpath == 0 || pointIndex.second < 0 || pointIndex.second > subpath->size()) return false; KoPathPoint::PointProperties properties = point->properties(); properties &= ~KoPathPoint::StartSubpath; properties &= ~KoPathPoint::StopSubpath; properties &= ~KoPathPoint::CloseSubpath; // check if new point starts subpath if (pointIndex.second == 0) { properties |= KoPathPoint::StartSubpath; // subpath was closed if (subpath->last()->properties() & KoPathPoint::CloseSubpath) { // keep the path closed properties |= KoPathPoint::CloseSubpath; } // old first point does not start the subpath anymore subpath->first()->unsetProperty(KoPathPoint::StartSubpath); } // check if new point stops subpath else if (pointIndex.second == subpath->size()) { properties |= KoPathPoint::StopSubpath; // subpath was closed if (subpath->last()->properties() & KoPathPoint::CloseSubpath) { // keep the path closed properties = properties | KoPathPoint::CloseSubpath; } // old last point does not end subpath anymore subpath->last()->unsetProperty(KoPathPoint::StopSubpath); } point->setProperties(properties); point->setParent(this); subpath->insert(pointIndex.second , point); return true; } KoPathPoint * KoPathShape::removePoint(const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(pointIndex.first); if (subpath == 0 || pointIndex.second < 0 || pointIndex.second >= subpath->size()) return 0; KoPathPoint * point = subpath->takeAt(pointIndex.second); point->setParent(0); //don't do anything (not even crash), if there was only one point if (pointCount()==0) { return point; } // check if we removed the first point else if (pointIndex.second == 0) { // first point removed, set new StartSubpath subpath->first()->setProperty(KoPathPoint::StartSubpath); // check if path was closed if (subpath->last()->properties() & KoPathPoint::CloseSubpath) { // keep path closed subpath->first()->setProperty(KoPathPoint::CloseSubpath); } } // check if we removed the last point else if (pointIndex.second == subpath->size()) { // use size as point is already removed // last point removed, set new StopSubpath subpath->last()->setProperty(KoPathPoint::StopSubpath); // check if path was closed if (point->properties() & KoPathPoint::CloseSubpath) { // keep path closed subpath->last()->setProperty(KoPathPoint::CloseSubpath); } } return point; } bool KoPathShape::breakAfter(const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(pointIndex.first); if (!subpath || pointIndex.second < 0 || pointIndex.second > subpath->size() - 2 || isClosedSubpath(pointIndex.first)) return false; KoSubpath * newSubpath = new KoSubpath; int size = subpath->size(); for (int i = pointIndex.second + 1; i < size; ++i) { newSubpath->append(subpath->takeAt(pointIndex.second + 1)); } // now make the first point of the new subpath a starting node newSubpath->first()->setProperty(KoPathPoint::StartSubpath); // the last point of the old subpath is now an ending node subpath->last()->setProperty(KoPathPoint::StopSubpath); // insert the new subpath after the broken one d->subpaths.insert(pointIndex.first + 1, newSubpath); return true; } bool KoPathShape::join(int subpathIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(subpathIndex); KoSubpath *nextSubpath = d->subPath(subpathIndex + 1); if (!subpath || !nextSubpath || isClosedSubpath(subpathIndex) || isClosedSubpath(subpathIndex+1)) return false; // the last point of the subpath does not end the subpath anymore subpath->last()->unsetProperty(KoPathPoint::StopSubpath); // the first point of the next subpath does not start a subpath anymore nextSubpath->first()->unsetProperty(KoPathPoint::StartSubpath); // append the second subpath to the first Q_FOREACH (KoPathPoint * p, *nextSubpath) subpath->append(p); // remove the nextSubpath from path d->subpaths.removeAt(subpathIndex + 1); // delete it as it is no longer possible to use it delete nextSubpath; return true; } bool KoPathShape::moveSubpath(int oldSubpathIndex, int newSubpathIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(oldSubpathIndex); if (subpath == 0 || newSubpathIndex >= d->subpaths.size()) return false; if (oldSubpathIndex == newSubpathIndex) return true; d->subpaths.removeAt(oldSubpathIndex); d->subpaths.insert(newSubpathIndex, subpath); return true; } KoPathPointIndex KoPathShape::openSubpath(const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(pointIndex.first); if (!subpath || pointIndex.second < 0 || pointIndex.second >= subpath->size() || !isClosedSubpath(pointIndex.first)) return KoPathPointIndex(-1, -1); KoPathPoint * oldStartPoint = subpath->first(); // the old starting node no longer starts the subpath oldStartPoint->unsetProperty(KoPathPoint::StartSubpath); // the old end node no longer closes the subpath subpath->last()->unsetProperty(KoPathPoint::StopSubpath); // reorder the subpath for (int i = 0; i < pointIndex.second; ++i) { subpath->append(subpath->takeFirst()); } // make the first point a start node subpath->first()->setProperty(KoPathPoint::StartSubpath); // make the last point an end node subpath->last()->setProperty(KoPathPoint::StopSubpath); return pathPointIndex(oldStartPoint); } KoPathPointIndex KoPathShape::closeSubpath(const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(pointIndex.first); if (!subpath || pointIndex.second < 0 || pointIndex.second >= subpath->size() || isClosedSubpath(pointIndex.first)) return KoPathPointIndex(-1, -1); KoPathPoint * oldStartPoint = subpath->first(); // the old starting node no longer starts the subpath oldStartPoint->unsetProperty(KoPathPoint::StartSubpath); // the old end node no longer ends the subpath subpath->last()->unsetProperty(KoPathPoint::StopSubpath); // reorder the subpath for (int i = 0; i < pointIndex.second; ++i) { subpath->append(subpath->takeFirst()); } subpath->first()->setProperty(KoPathPoint::StartSubpath); subpath->last()->setProperty(KoPathPoint::StopSubpath); d->closeSubpath(subpath); return pathPointIndex(oldStartPoint); } bool KoPathShape::reverseSubpath(int subpathIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(subpathIndex); if (subpath == 0) return false; int size = subpath->size(); for (int i = 0; i < size; ++i) { KoPathPoint *p = subpath->takeAt(i); p->reverse(); subpath->prepend(p); } // adjust the position dependent properties KoPathPoint *first = subpath->first(); KoPathPoint *last = subpath->last(); KoPathPoint::PointProperties firstProps = first->properties(); KoPathPoint::PointProperties lastProps = last->properties(); firstProps |= KoPathPoint::StartSubpath; firstProps &= ~KoPathPoint::StopSubpath; lastProps |= KoPathPoint::StopSubpath; lastProps &= ~KoPathPoint::StartSubpath; if (firstProps & KoPathPoint::CloseSubpath) { firstProps |= KoPathPoint::CloseSubpath; lastProps |= KoPathPoint::CloseSubpath; } first->setProperties(firstProps); last->setProperties(lastProps); return true; } KoSubpath * KoPathShape::removeSubpath(int subpathIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(subpathIndex); if (subpath != 0) { Q_FOREACH (KoPathPoint* point, *subpath) { point->setParent(this); } d->subpaths.removeAt(subpathIndex); } return subpath; } bool KoPathShape::addSubpath(KoSubpath * subpath, int subpathIndex) { Q_D(KoPathShape); if (subpathIndex < 0 || subpathIndex > d->subpaths.size()) return false; Q_FOREACH (KoPathPoint* point, *subpath) { point->setParent(this); } d->subpaths.insert(subpathIndex, subpath); return true; } - -bool KoPathShape::combine(KoPathShape *path) +int KoPathShape::combine(KoPathShape *path) { Q_D(KoPathShape); - - if (! path) - return false; + int insertSegmentPosition = -1; + if (!path) return insertSegmentPosition; QTransform pathMatrix = path->absoluteTransformation(0); QTransform myMatrix = absoluteTransformation(0).inverted(); Q_FOREACH (KoSubpath* subpath, path->d_func()->subpaths) { KoSubpath *newSubpath = new KoSubpath(); Q_FOREACH (KoPathPoint* point, *subpath) { KoPathPoint *newPoint = new KoPathPoint(*point, this); newPoint->map(pathMatrix); newPoint->map(myMatrix); newSubpath->append(newPoint); } d->subpaths.append(newSubpath); + + if (insertSegmentPosition < 0) { + insertSegmentPosition = d->subpaths.size() - 1; + } } normalize(); - return true; + return insertSegmentPosition; } bool KoPathShape::separate(QList & separatedPaths) { Q_D(KoPathShape); if (! d->subpaths.size()) return false; QTransform myMatrix = absoluteTransformation(0); Q_FOREACH (KoSubpath* subpath, d->subpaths) { KoPathShape *shape = new KoPathShape(); if (! shape) continue; shape->setStroke(stroke()); shape->setShapeId(shapeId()); KoSubpath *newSubpath = new KoSubpath(); Q_FOREACH (KoPathPoint* point, *subpath) { KoPathPoint *newPoint = new KoPathPoint(*point, shape); newPoint->map(myMatrix); newSubpath->append(newPoint); } shape->d_func()->subpaths.append(newSubpath); shape->normalize(); separatedPaths.append(shape); } return true; } void KoPathShapePrivate::closeSubpath(KoSubpath *subpath) { if (! subpath) return; subpath->last()->setProperty(KoPathPoint::CloseSubpath); subpath->first()->setProperty(KoPathPoint::CloseSubpath); } void KoPathShapePrivate::closeMergeSubpath(KoSubpath *subpath) { if (! subpath || subpath->size() < 2) return; KoPathPoint * lastPoint = subpath->last(); KoPathPoint * firstPoint = subpath->first(); // check if first and last points are coincident if (lastPoint->point() == firstPoint->point()) { // we are removing the current last point and // reuse its first control point if active firstPoint->setProperty(KoPathPoint::StartSubpath); firstPoint->setProperty(KoPathPoint::CloseSubpath); if (lastPoint->activeControlPoint1()) firstPoint->setControlPoint1(lastPoint->controlPoint1()); // remove last point delete subpath->takeLast(); // the new last point closes the subpath now lastPoint = subpath->last(); lastPoint->setProperty(KoPathPoint::StopSubpath); lastPoint->setProperty(KoPathPoint::CloseSubpath); } else { closeSubpath(subpath); } } KoSubpath *KoPathShapePrivate::subPath(int subpathIndex) const { Q_Q(const KoPathShape); if (subpathIndex < 0 || subpathIndex >= subpaths.size()) return 0; return subpaths.at(subpathIndex); } QString KoPathShape::pathShapeId() const { return KoPathShapeId; } QString KoPathShape::toString(const QTransform &matrix) const { Q_D(const KoPathShape); QString pathString; // iterate over all subpaths KoSubpathList::const_iterator pathIt(d->subpaths.constBegin()); for (; pathIt != d->subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator pointIt((*pathIt)->constBegin()); // keep a pointer to the first point of the subpath KoPathPoint *firstPoint(*pointIt); // keep a pointer to the previous point of the subpath KoPathPoint *lastPoint = firstPoint; // keep track if the previous point has an active control point 2 bool activeControlPoint2 = false; // iterate over all points of the current subpath for (; pointIt != (*pathIt)->constEnd(); ++pointIt) { KoPathPoint *currPoint(*pointIt); // first point of subpath ? if (currPoint == firstPoint) { // are we starting a subpath ? if (currPoint->properties() & KoPathPoint::StartSubpath) { const QPointF p = matrix.map(currPoint->point()); pathString += QString("M%1 %2").arg(p.x()).arg(p.y()); } } // end point of curve segment ? else if (activeControlPoint2 || currPoint->activeControlPoint1()) { // check if we have a cubic or quadratic curve const bool isCubic = activeControlPoint2 && currPoint->activeControlPoint1(); KoPathSegment cubicSeg = isCubic ? KoPathSegment(lastPoint, currPoint) : KoPathSegment(lastPoint, currPoint).toCubic(); const QPointF cp1 = matrix.map(cubicSeg.first()->controlPoint2()); const QPointF cp2 = matrix.map(cubicSeg.second()->controlPoint1()); const QPointF p = matrix.map(cubicSeg.second()->point()); pathString += QString("C%1 %2 %3 %4 %5 %6") .arg(cp1.x()).arg(cp1.y()) .arg(cp2.x()).arg(cp2.y()) .arg(p.x()).arg(p.y()); } // end point of line segment! else { const QPointF p = matrix.map(currPoint->point()); pathString += QString("L%1 %2").arg(p.x()).arg(p.y()); } // last point closes subpath ? if (currPoint->properties() & KoPathPoint::StopSubpath && currPoint->properties() & KoPathPoint::CloseSubpath) { // add curve when there is a curve on the way to the first point if (currPoint->activeControlPoint2() || firstPoint->activeControlPoint1()) { // check if we have a cubic or quadratic curve const bool isCubic = currPoint->activeControlPoint2() && firstPoint->activeControlPoint1(); KoPathSegment cubicSeg = isCubic ? KoPathSegment(currPoint, firstPoint) : KoPathSegment(currPoint, firstPoint).toCubic(); const QPointF cp1 = matrix.map(cubicSeg.first()->controlPoint2()); const QPointF cp2 = matrix.map(cubicSeg.second()->controlPoint1()); const QPointF p = matrix.map(cubicSeg.second()->point()); pathString += QString("C%1 %2 %3 %4 %5 %6") .arg(cp1.x()).arg(cp1.y()) .arg(cp2.x()).arg(cp2.y()) .arg(p.x()).arg(p.y()); } pathString += QString("Z"); } activeControlPoint2 = currPoint->activeControlPoint2(); lastPoint = currPoint; } } return pathString; } char nodeType(const KoPathPoint * point) { if (point->properties() & KoPathPoint::IsSmooth) { return 's'; } else if (point->properties() & KoPathPoint::IsSymmetric) { return 'z'; } else { return 'c'; } } QString KoPathShapePrivate::nodeTypes() const { Q_Q(const KoPathShape); QString types; KoSubpathList::const_iterator pathIt(subpaths.constBegin()); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { if (it == (*pathIt)->constBegin()) { types.append('c'); } else { types.append(nodeType(*it)); } if ((*it)->properties() & KoPathPoint::StopSubpath && (*it)->properties() & KoPathPoint::CloseSubpath) { KoPathPoint * firstPoint = (*pathIt)->first(); types.append(nodeType(firstPoint)); } } } return types; } void updateNodeType(KoPathPoint * point, const QChar & nodeType) { if (nodeType == 's') { point->setProperty(KoPathPoint::IsSmooth); } else if (nodeType == 'z') { point->setProperty(KoPathPoint::IsSymmetric); } } void KoPathShapePrivate::loadNodeTypes(const KoXmlElement &element) { Q_Q(KoPathShape); if (element.hasAttributeNS(KoXmlNS::calligra, "nodeTypes")) { QString nodeTypes = element.attributeNS(KoXmlNS::calligra, "nodeTypes"); QString::const_iterator nIt(nodeTypes.constBegin()); KoSubpathList::const_iterator pathIt(subpaths.constBegin()); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it, nIt++) { // be sure not to crash if there are not enough nodes in nodeTypes if (nIt == nodeTypes.constEnd()) { warnFlake << "not enough nodes in calligra:nodeTypes"; return; } // the first node is always of type 'c' if (it != (*pathIt)->constBegin()) { updateNodeType(*it, *nIt); } if ((*it)->properties() & KoPathPoint::StopSubpath && (*it)->properties() & KoPathPoint::CloseSubpath) { ++nIt; updateNodeType((*pathIt)->first(), *nIt); } } } } } Qt::FillRule KoPathShape::fillRule() const { Q_D(const KoPathShape); return d->fillRule; } void KoPathShape::setFillRule(Qt::FillRule fillRule) { Q_D(KoPathShape); d->fillRule = fillRule; } KoPathShape * KoPathShape::createShapeFromPainterPath(const QPainterPath &path) { KoPathShape * shape = new KoPathShape(); int elementCount = path.elementCount(); for (int i = 0; i < elementCount; i++) { QPainterPath::Element element = path.elementAt(i); switch (element.type) { case QPainterPath::MoveToElement: shape->moveTo(QPointF(element.x, element.y)); break; case QPainterPath::LineToElement: shape->lineTo(QPointF(element.x, element.y)); break; case QPainterPath::CurveToElement: shape->curveTo(QPointF(element.x, element.y), QPointF(path.elementAt(i + 1).x, path.elementAt(i + 1).y), QPointF(path.elementAt(i + 2).x, path.elementAt(i + 2).y)); break; default: continue; } } //shape->normalize(); return shape; } bool KoPathShape::hitTest(const QPointF &position) const { if (parent() && parent()->isClipped(this) && ! parent()->hitTest(position)) return false; QPointF point = absoluteTransformation(0).inverted().map(position); const QPainterPath outlinePath = outline(); if (stroke()) { KoInsets insets; stroke()->strokeInsets(this, insets); QRectF roi(QPointF(-insets.left, -insets.top), QPointF(insets.right, insets.bottom)); roi.moveCenter(point); if (outlinePath.intersects(roi) || outlinePath.contains(roi)) return true; } else { if (outlinePath.contains(point)) return true; } // if there is no shadow we can as well just leave if (! shadow()) return false; // the shadow has an offset to the shape, so we simply // check if the position minus the shadow offset hits the shape point = absoluteTransformation(0).inverted().map(position - shadow()->offset()); return outlinePath.contains(point); } void KoPathShape::setMarker(KoMarker *marker, KoFlake::MarkerPosition pos) { Q_D(KoPathShape); if (!marker && d->markersNew.contains(pos)) { d->markersNew.remove(pos); } else { d->markersNew[pos] = marker; } } KoMarker *KoPathShape::marker(KoFlake::MarkerPosition pos) const { Q_D(const KoPathShape); return d->markersNew[pos].data(); } bool KoPathShape::hasMarkers() const { Q_D(const KoPathShape); return !d->markersNew.isEmpty(); } bool KoPathShape::autoFillMarkers() const { Q_D(const KoPathShape); return d->autoFillMarkers; } void KoPathShape::setAutoFillMarkers(bool value) { Q_D(KoPathShape); d->autoFillMarkers = value; } QPainterPath KoPathShape::pathStroke(const QPen &pen) const { Q_D(const KoPathShape); if (d->subpaths.isEmpty()) { return QPainterPath(); } QPainterPath pathOutline; QPainterPathStroker stroker; stroker.setWidth(0); stroker.setJoinStyle(Qt::MiterJoin); QPair firstSegments; QPair lastSegments; KoPathPoint *firstPoint = 0; KoPathPoint *lastPoint = 0; KoPathPoint *secondPoint = 0; KoPathPoint *preLastPoint = 0; KoSubpath *firstSubpath = d->subpaths.first(); stroker.setWidth(pen.widthF()); stroker.setJoinStyle(pen.joinStyle()); stroker.setMiterLimit(pen.miterLimit()); stroker.setCapStyle(pen.capStyle()); stroker.setDashOffset(pen.dashOffset()); stroker.setDashPattern(pen.dashPattern()); // shortent the path to make it look nice // replace the point temporarily in case there is an arrow // BE AWARE: this changes the content of the path so that outline give the correct values. if (firstPoint) { firstSubpath->first() = firstSegments.second.first(); if (secondPoint) { (*firstSubpath)[1] = firstSegments.second.second(); } } if (lastPoint) { if (preLastPoint) { (*firstSubpath)[firstSubpath->count() - 2] = lastSegments.first.first(); } firstSubpath->last() = lastSegments.first.second(); } QPainterPath path = stroker.createStroke(outline()); if (firstPoint) { firstSubpath->first() = firstPoint; if (secondPoint) { (*firstSubpath)[1] = secondPoint; } } if (lastPoint) { if (preLastPoint) { (*firstSubpath)[firstSubpath->count() - 2] = preLastPoint; } firstSubpath->last() = lastPoint; } pathOutline.addPath(path); pathOutline.setFillRule(Qt::WindingFill); return pathOutline; } diff --git a/libs/flake/KoPathShape.h b/libs/flake/KoPathShape.h index 47161adec3..089f535b74 100644 --- a/libs/flake/KoPathShape.h +++ b/libs/flake/KoPathShape.h @@ -1,510 +1,510 @@ /* This file is part of the KDE project Copyright (C) 2006, 2011 Thorsten Zachmann Copyright (C) 2007,2009 Thomas Zander Copyright (C) 2006-2008 Jan Hambrecht Copyright (C) 2011 Jean-Nicolas Artaud This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOPATHSHAPE_H #define KOPATHSHAPE_H #include "kritaflake_export.h" #include #include #include "KoTosContainer.h" #define KoPathShapeId "KoPathShape" class KoPathSegment; class KoPathPoint; class KoPathShapePrivate; class KoMarker; class KisHandlePainterHelper; typedef QPair KoPathPointIndex; /// a KoSubpath contains a path from a moveTo until a close or a new moveTo typedef QList KoSubpath; typedef QList KoSubpathList; /// The position of a path point within a path shape /** * @brief This is the base for all graphical objects. * * All graphical objects are based on this object e.g. lines, rectangulars, pies * and so on. * * The KoPathShape uses KoPathPoint's to describe the path of the shape. * * Here a short example: * 3 points connected by a curveTo's described by the following svg: * M 100,200 C 100,100 250,100 250,200 C 250,200 400,300 400,200. * * This will be stored in 3 KoPathPoint's as * The first point contains in * point 100,200 * controlPoint2 100,100 * The second point contains in * point 250,200 * controlPoint1 250,100 * controlPoint2 250,300 * The third point contains in * point 400,300 * controlPoint1 400,200 * * Not the segments are stored but the points. Out of the points the segments are * generated. See the outline method. The reason for storing it like that is that * it is the points that are modified by the user and not the segments. */ class KRITAFLAKE_EXPORT KoPathShape : public KoTosContainer { public: /** * @brief constructor */ KoPathShape(); /** * @brief */ virtual ~KoPathShape(); KoShape *cloneShape() const; /// reimplemented virtual void paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext); virtual void paintPoints(KisHandlePainterHelper &handlesHelper); /// reimplemented QRectF outlineRect() const override; /// reimplemented QPainterPath outline() const override; /// reimplemented QRectF boundingRect() const override; /// reimplemented QSizeF size() const override; QPainterPath pathStroke(const QPen &pen) const; /** * Resize the shape * * This makes sure that the pathshape will not be resized to 0 if the new size * is null as that makes it impossible to undo the change. * * All functions that overwrite this function should also use the resizeMatrix * function to get and use the same data in resizing. * * @see resizeMatrix() */ virtual void setSize(const QSizeF &size); /// reimplemented virtual bool hitTest(const QPointF &position) const; // reimplemented virtual void saveOdf(KoShapeSavingContext &context) const; // reimplemented virtual bool loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context); // basically the same as loadOdf but adapted to the contour cases // tag needs to be either contour-polygon or contour-path from another shape bool loadContourOdf(const KoXmlElement & element, KoShapeLoadingContext &context, const QSizeF &scaleFactor); /** basically the equivalent saveOdf but adapted to the contour cases * @param originalSize the original size of the unscaled image. */ void saveContourOdf(KoShapeSavingContext &context, const QSizeF &originalSize) const; /// Removes all subpaths and their points from the path void clear(); /** * @brief Starts a new Subpath * * Moves the pen to p and starts a new subpath. * * @return the newly created point */ KoPathPoint *moveTo(const QPointF &p); /** * @brief Adds a new line segment * * Adds a straight line between the last point and the given point p. * * @return the newly created point */ KoPathPoint *lineTo(const QPointF &p); /** * @brief Adds a new cubic Bezier curve segment. * * Adds a cubic Bezier curve between the last point and the given point p, * using the control points specified by c1 and c2. * * @param c1 control point1 * @param c2 control point2 * @param p the endpoint of this curve segment * * @return The newly created point */ KoPathPoint *curveTo(const QPointF &c1, const QPointF &c2, const QPointF &p); /** * @brief Adds a new quadratic Bezier curve segment. * * Adds a quadratic Bezier curve between the last point and the given point p, * using the control point specified by c. * * @param c control point * @param p the endpoint of this curve segment * * @return The newly created point */ KoPathPoint *curveTo(const QPointF &c, const QPointF &p); /** * @brief Add an arc. * * Adds an arc starting at the current point. The arc will be converted to bezier curves. * * @param rx x radius of the ellipse * @param ry y radius of the ellipse * @param startAngle the angle where the arc will be started * @param sweepAngle the length of the angle * TODO add param to have angle of the ellipse * * @return The newly created point */ KoPathPoint *arcTo(qreal rx, qreal ry, qreal startAngle, qreal sweepAngle); /** * @brief Closes the current subpath */ void close(); /** * @brief Closes the current subpath * * It tries to merge the last and first point of the subpath * to one point and then closes the subpath. If merging is not * possible as the two point are to far from each other a close * will be done. * TODO define a maximum distance between the two points until this is working */ void closeMerge(); /** * @brief Normalizes the path data. * * The path points are transformed so that the top-left corner * of the bounding rect is at (0,0). * This should be called after adding points to the path or changing * positions of path points. * @return the offset by which the points are moved in shape coordinates. */ virtual QPointF normalize(); /** * @brief Returns the path points within the given rectangle. * @param rect the rectangle the requested points are in * @return list of points within the rectangle */ QList pointsAt(const QRectF &rect) const; /** * @brief Returns the list of path segments within the given rectangle. * @param rect the rectangle the requested segments are in * @return list of segments within the rectangle */ QList segmentsAt(const QRectF &rect) const; /** * @brief Returns the path point index of a given path point * * @param point the point for which you want to get the index * @return path point index of the point if it exists * otherwise KoPathPointIndex( -1, -1 ) */ KoPathPointIndex pathPointIndex(const KoPathPoint *point) const; /** * @brief Returns the path point specified by a path point index * * @param pointIndex index of the point to get * * @return KoPathPoint on success, 0 otherwise e.g. out of bounds */ KoPathPoint *pointByIndex(const KoPathPointIndex &pointIndex) const; /** * @brief Returns the segment specified by a path point index * * A semgent is defined by the point index of the first point in the segment. * A segment contains the defined point and its following point. If the subpath is * closed and the and the pointIndex point to the last point in the subpath, the * following point is the first point in the subpath. * * @param pointIndex index of the first point of the segment * * @return Segment containing both points of the segment or KoPathSegment( 0, 0 ) on error e.g. out of bounds */ KoPathSegment segmentByIndex(const KoPathPointIndex &pointIndex) const; /** * @brief Returns the number of points in the path * * @return The number of points in the path */ int pointCount() const; /** * @brief Returns the number of subpaths in the path * * @return The number of subpaths in the path */ int subpathCount() const; /** * @brief Returns the number of points in a subpath * * @return The number of points in the subpath or -1 if subpath out of bounds */ int subpathPointCount(int subpathIndex) const; /** * @brief Checks if a subpath is closed * * @param subpathIndex index of the subpath to check * * @return true when the subpath is closed, false otherwise */ bool isClosedSubpath(int subpathIndex) const; /** * @brief Inserts a new point into the given subpath at the specified position * * This method keeps the subpath closed if it is closed, and open when it was * open. So it can change the properties of the point inserted. * You might need to update the point before/after to get the desired result * e.g. when you insert the point into a curve. * * @param point to insert * @param pointIndex index at which the point should be inserted * * @return true on success, * false when pointIndex is out of bounds */ bool insertPoint(KoPathPoint *point, const KoPathPointIndex &pointIndex); /** * @brief Removes a point from the path. * * Note that the ownership of the point will pass to the caller. * * @param pointIndex index of the point which should be removed * * @return The removed point on success, * otherwise 0 */ KoPathPoint *removePoint(const KoPathPointIndex &pointIndex); /** * @brief Breaks the path after the point index * * The new subpath will be behind the one that was broken. The segment between * the given point and the one behind will be removed. If you want to split at * one point insert first a copy of the point behind it. * This does not work when the subpath is closed. Use openSubpath for this. * It does not break at the last position of a subpath or if there is only one * point in the subpath. * * @param pointIndex index of the point after which the path should be broken * * @return true if the subpath was broken, otherwise false */ bool breakAfter(const KoPathPointIndex &pointIndex); /** * @brief Joins the given subpath with the following one * * Joins the given subpath with the following one by inserting a segment between * the two subpaths. * This does nothing if the specified subpath is the last subpath * or one of both subpaths is closed. * * @param subpathIndex index of the subpath being joined with the following subpath * * @return true if the subpath was joined, otherwise false */ bool join(int subpathIndex); /** * @brief Moves the position of a subpath within a path * * @param oldSubpathIndex old index of the subpath * @param newSubpathIndex new index of the subpath * * @return true if the subpath was moved, otherwise false e.g. if an index is out of bounds */ bool moveSubpath(int oldSubpathIndex, int newSubpathIndex); /** * @brief Opens a closed subpath * * The subpath is opened by removing the segment before the given point, making * the given point the new start point of the subpath. * * @param pointIndex the index of the point at which to open the closed subpath * @return the new position of the old first point in the subpath * otherwise KoPathPointIndex( -1, -1 ) */ KoPathPointIndex openSubpath(const KoPathPointIndex &pointIndex); /** * @brief Close a open subpath * * The subpath is closed be inserting a segment between the start and end point, making * the given point the new start point of the subpath. * * @return the new position of the old first point in the subpath * otherwise KoPathPointIndex( -1, -1 ) */ KoPathPointIndex closeSubpath(const KoPathPointIndex &pointIndex); /** * @brief Reverse subpath * * The last point becomes the first point and the first one becomes the last one. * * @param subpathIndex the index of the subpath to reverse */ bool reverseSubpath(int subpathIndex); /** * @brief Removes subpath from the path * @param subpathIndex the index of the subpath to remove * @return the removed subpath on succes, 0 otherwise. */ KoSubpath *removeSubpath(int subpathIndex); /** * @brief Adds a subpath at the given index to the path * @param subpath the subpath to add * @param subpathIndex the index at which the new subpath should be inserted * @return true on success, false otherwise e.g. subpathIndex out of bounds */ bool addSubpath(KoSubpath *subpath, int subpathIndex); /** * @brief Combines two path shapes by appending the data of the specified path. * @param path the path to combine with - * @return 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. * @param separatedPaths the list which contains the separated path shapes * @return true if separating the path was successful, false otherwise */ bool separate(QList &separatedPaths); /** * Returns the specific path shape id. * * Path shape derived shapes have a different shape id which link them * to their respective shape factories. In most cases they do not have * a special tool for editing them. * This function returns the specific shape id for finding the shape * factory from KoShapeRegistry. The default KoPathShapeId is returned * from KoShape::shapeId() so that the generic path editing tool gets * activated when the shape is selected. * * @return the specific shape id */ virtual QString pathShapeId() const; /// Returns a odf/svg string represenatation of the path data with the given matrix applied. QString toString(const QTransform &matrix = QTransform()) const; /// Returns the fill rule for the path object Qt::FillRule fillRule() const; /// Sets the fill rule to be used for painting the background void setFillRule(Qt::FillRule fillRule); /// Creates path shape from given QPainterPath static KoPathShape *createShapeFromPainterPath(const QPainterPath &path); /// Returns the viewbox from the given xml element. static QRect loadOdfViewbox(const KoXmlElement &element); void setMarker(KoMarker *marker, KoFlake::MarkerPosition pos); KoMarker* marker(KoFlake::MarkerPosition pos) const; bool hasMarkers() const; bool autoFillMarkers() const; void setAutoFillMarkers(bool value); private: /// constructor: to be used in cloneShape(), not in descendants! /// \internal KoPathShape(const KoPathShape &rhs); protected: /// constructor:to be used in descendant shapes /// \internal KoPathShape(KoPathShapePrivate *); /// reimplemented virtual QString saveStyle(KoGenStyle &style, KoShapeSavingContext &context) const; /// reimplemented virtual void loadStyle(const KoXmlElement &element, KoShapeLoadingContext &context); /** * @brief Add an arc. * * Adds an arc starting at the current point. The arc will be converted to bezier curves. * @param rx x radius of the ellipse * @param ry y radius of the ellipse * @param startAngle the angle where the arc will be started * @param sweepAngle the length of the angle * TODO add param to have angle of the ellipse * @param offset to the first point in the arc * @param curvePoints a array which take the cuve points, pass a 'QPointF curvePoins[12]'; * * @return number of points created by the curve */ int arcToCurve(qreal rx, qreal ry, qreal startAngle, qreal sweepAngle, const QPointF &offset, QPointF *curvePoints) const; /** * Get the resize matrix * * This makes sure that also if the newSize isNull that there will be a * very small size of 0.000001 pixels */ QTransform resizeMatrix( const QSizeF &newSize ) const; private: Q_DECLARE_PRIVATE(KoPathShape) }; Q_DECLARE_METATYPE(KoPathShape*) #endif /* KOPATHSHAPE_H */ diff --git a/libs/flake/KoSelection.cpp b/libs/flake/KoSelection.cpp index c7c409570f..571acd6d41 100644 --- a/libs/flake/KoSelection.cpp +++ b/libs/flake/KoSelection.cpp @@ -1,304 +1,309 @@ /* This file is part of the KDE project Copyright (C) 2006 Boudewijn Rempt Copyright (C) 2006 Thorsten Zachmann Copyright (C) 2006 Jan Hambrecht Copyright (C) 2006-2007,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. */ #include "KoSelection.h" #include "KoSelection_p.h" #include "KoShapeContainer.h" #include "KoShapeGroup.h" #include "KoPointerEvent.h" #include "KoShapePaintingContext.h" #include "kis_algebra_2d.h" #include "krita_container_utils.h" #include #include "kis_debug.h" KoSelection::KoSelection() : KoShape(new KoSelectionPrivate(this)) { Q_D(KoSelection); connect(&d->selectionChangedCompressor, SIGNAL(timeout()), SIGNAL(selectionChanged())); } KoSelection::~KoSelection() { } void KoSelection::paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintcontext) { Q_UNUSED(painter); Q_UNUSED(converter); Q_UNUSED(paintcontext); } void KoSelection::setSize(const QSizeF &size) { Q_UNUSED(size); qWarning() << "WARNING: KoSelection::setSize() should never be used!"; } QSizeF KoSelection::size() const { return outlineRect().size(); } 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(); } QRectF KoSelection::boundingRect() const { Q_D(const KoSelection); return KoShape::boundingRect(d->selectedShapes); } void KoSelection::select(KoShape *shape) { Q_D(KoSelection); KIS_SAFE_ASSERT_RECOVER_RETURN(shape != this); KIS_SAFE_ASSERT_RECOVER_RETURN(shape); if (!shape->isSelectable() || !shape->isVisible(true)) { return; } // check recursively if (isSelected(shape)) { return; } // find the topmost parent to select while (KoShapeGroup *parentGroup = dynamic_cast(shape->parent())) { shape = parentGroup; } d->selectedShapes << shape; shape->addShapeChangeListener(this); d->savedMatrices = d->fetchShapesMatrices(); if (d->selectedShapes.size() == 1) { setTransformation(shape->absoluteTransformation(0)); } else { setTransformation(QTransform()); } d->selectionChangedCompressor.start(); } void KoSelection::deselect(KoShape *shape) { Q_D(KoSelection); if (!d->selectedShapes.contains(shape)) return; d->selectedShapes.removeAll(shape); shape->removeShapeChangeListener(this); d->savedMatrices = d->fetchShapesMatrices(); if (d->selectedShapes.size() == 1) { setTransformation(d->selectedShapes.first()->absoluteTransformation(0)); } d->selectionChangedCompressor.start(); } void KoSelection::deselectAll() { Q_D(KoSelection); 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->selectionChangedCompressor.start(); } int KoSelection::count() const { Q_D(const KoSelection); return d->selectedShapes.size(); } bool KoSelection::hitTest(const QPointF &position) const { Q_D(const KoSelection); Q_FOREACH (KoShape *shape, d->selectedShapes) { if (shape->hitTest(position)) return true; } return false; } const QList KoSelection::selectedShapes() const { Q_D(const KoSelection); return d->selectedShapes; } const QList KoSelection::selectedEditableShapes() const { Q_D(const KoSelection); QList shapes = selectedShapes(); KritaUtils::filterContainer (shapes, [](KoShape *shape) { return shape->isEditable(); }); return shapes; } const QList KoSelection::selectedEditableShapesAndDelegates() const { 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); } } } return shapes; } bool KoSelection::isSelected(const KoShape *shape) const { Q_D(const KoSelection); if (shape == this) 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 tmpShape; } KoShape *KoSelection::firstSelectedShape() const { Q_D(const KoSelection); return !d->selectedShapes.isEmpty() ? d->selectedShapes.first() : 0; } void KoSelection::setActiveLayer(KoShapeLayer *layer) { Q_D(KoSelection); d->activeLayer = layer; emit currentLayerChanged(layer); } KoShapeLayer* KoSelection::activeLayer() const { Q_D(const KoSelection); return d->activeLayer; } void KoSelection::notifyShapeChanged(KoShape::ChangeType type, KoShape *shape) { Q_UNUSED(shape); Q_D(KoSelection); - if (type >= KoShape::PositionChanged && type <= KoShape::GenericMatrixChange) { + 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 { } bool KoSelection::loadOdf(const KoXmlElement &, KoShapeLoadingContext &) { return true; } 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/commands/KoMultiPathPointJoinCommand.cpp b/libs/flake/commands/KoMultiPathPointJoinCommand.cpp new file mode 100644 index 0000000000..3af7f3b84b --- /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/KoMultiPathPointJoinCommand.h b/libs/flake/commands/KoMultiPathPointJoinCommand.h new file mode 100644 index 0000000000..b1505f3ff3 --- /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/KoMultiPathPointMergeCommand.cpp b/libs/flake/commands/KoMultiPathPointMergeCommand.cpp new file mode 100644 index 0000000000..3d7408db38 --- /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/KoMultiPathPointMergeCommand.h b/libs/flake/commands/KoMultiPathPointMergeCommand.h new file mode 100644 index 0000000000..597593ba0c --- /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/KoPathCombineCommand.cpp b/libs/flake/commands/KoPathCombineCommand.cpp index db89f6d7b2..80264c9137 100644 --- a/libs/flake/commands/KoPathCombineCommand.cpp +++ b/libs/flake/commands/KoPathCombineCommand.cpp @@ -1,121 +1,144 @@ /* This file is part of the KDE project * Copyright (C) 2006,2008 Jan Hambrecht * Copyright (C) 2006,2007 Thorsten Zachmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoPathCombineCommand.h" #include "KoShapeBasedDocumentBase.h" #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; + } } } KoPathCombineCommand::~KoPathCombineCommand() { delete d; } 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); } } void KoPathCombineCommand::undo() { if (! d->paths.size()) return; d->isCombined = false; 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/KoPathCombineCommand.h b/libs/flake/commands/KoPathCombineCommand.h index 680e872df4..4c3ddb8bf8 100644 --- a/libs/flake/commands/KoPathCombineCommand.h +++ b/libs/flake/commands/KoPathCombineCommand.h @@ -1,53 +1,57 @@ /* This file is part of the KDE project * Copyright (C) 2006 Jan Hambrecht * Copyright (C) 2006,2007 Thorsten Zachmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOPATHCOMBINECOMMAND_H #define KOPATHCOMBINECOMMAND_H #include #include #include "kritaflake_export.h" class KoShapeBasedDocumentBase; class KoPathShape; +class KoPathPointData; /// The undo / redo command for combining two or more paths into one class KRITAFLAKE_EXPORT KoPathCombineCommand : public KUndo2Command { public: /** * Command for combining a list of paths into one single path. * @param controller the controller to used for removing/inserting. * @param paths the list of paths to combine * @param parent the parent command used for macro commands */ KoPathCombineCommand(KoShapeBasedDocumentBase *controller, const QList &paths, KUndo2Command *parent = 0); virtual ~KoPathCombineCommand(); /// redo the command void redo(); /// revert the actions done in redo void undo(); + KoPathShape *combinedPath() const; + KoPathPointData originalToCombined(KoPathPointData pd) const; + private: class Private; Private * const d; }; #endif // KOPATHCOMBINECOMMAND_H diff --git a/libs/flake/commands/KoPathPointMergeCommand.cpp b/libs/flake/commands/KoPathPointMergeCommand.cpp index 99f43a8c06..e0bbeae528 100644 --- a/libs/flake/commands/KoPathPointMergeCommand.cpp +++ b/libs/flake/commands/KoPathPointMergeCommand.cpp @@ -1,242 +1,262 @@ /* This file is part of the KDE project * Copyright (C) 2009 Jan Hambrecht * Copyright (C) 2006,2007 Thorsten Zachmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoPathPointMergeCommand.h" #include "KoPathPoint.h" #include "KoPathPointData.h" #include "KoPathShape.h" #include #include +#include "kis_assert.h" + class Q_DECL_HIDDEN KoPathPointMergeCommand::Private { public: Private(const KoPathPointData &pointData1, const KoPathPointData &pointData2) : pathShape(pointData1.pathShape) , endPoint(pointData1.pointIndex) , startPoint(pointData2.pointIndex) , splitIndex(KoPathPointIndex(-1, -1)) , removedPoint(0) + , mergedPointIndex(-1, -1) , reverse(ReverseNone) { } ~Private() { delete removedPoint; } KoPathPoint * mergePoints(KoPathPoint * p1, KoPathPoint * p2) { QPointF mergePosition = 0.5 * (p1->point() + p2->point()); QPointF mergeControlPoint1 = mergePosition + (p1->controlPoint1() - p1->point()); QPointF mergeControlPoint2 = mergePosition + (p2->controlPoint2() - p2->point()); // change position and control points of first merged point p1->setPoint(mergePosition); if (p1->activeControlPoint1()) { p1->setControlPoint1(mergeControlPoint1); } if (p2->activeControlPoint2()) { p1->setControlPoint2(mergeControlPoint2); } // remove the second merged point KoPathPointIndex removeIndex = pathShape->pathPointIndex(p2); return pathShape->removePoint(removeIndex); } void resetPoints(KoPathPointIndex index1, KoPathPointIndex index2) { KoPathPoint * p1 = pathShape->pointByIndex(index1); KoPathPoint * p2 = pathShape->pointByIndex(index2); p1->setPoint(pathShape->documentToShape(oldNodePoint1)); p2->setPoint(pathShape->documentToShape(oldNodePoint2)); if (p1->activeControlPoint1()) { p1->setControlPoint1(pathShape->documentToShape(oldControlPoint1)); } if (p2->activeControlPoint2()) { p2->setControlPoint2(pathShape->documentToShape(oldControlPoint2)); } } KoPathShape * pathShape; KoPathPointIndex endPoint; KoPathPointIndex startPoint; KoPathPointIndex splitIndex; // the control points have to be stored in document positions QPointF oldNodePoint1; QPointF oldControlPoint1; QPointF oldNodePoint2; QPointF oldControlPoint2; KoPathPoint * removedPoint; + KoPathPointIndex mergedPointIndex; enum Reverse { ReverseNone = 0, ReverseFirst = 1, ReverseSecond = 2 }; int reverse; }; /** * How does is work: * * The goal is to merge the point that is ending an open subpath with the one * starting the same or another open subpath. */ 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 if (d->startPoint < d->endPoint) qSwap(d->endPoint, d->startPoint); // mark first subpath to be reversed if first point starts a subpath with more than one point if (d->endPoint.second == 0 && d->pathShape->subpathPointCount(d->endPoint.first) > 1) d->reverse |= Private::ReverseFirst; // mark second subpath to be reversed if second point does not start a subpath with more than one point if (d->startPoint.second != 0 && d->pathShape->subpathPointCount(d->startPoint.first) > 1) d->reverse |= Private::ReverseSecond; } else { Q_ASSERT(d->endPoint.second != d->startPoint.second); if (d->endPoint < d->startPoint) qSwap(d->endPoint, d->startPoint); } KoPathPoint * p1 = d->pathShape->pointByIndex(d->endPoint); KoPathPoint * p2 = d->pathShape->pointByIndex(d->startPoint); d->oldNodePoint1 = d->pathShape->shapeToDocument(p1->point()); if (d->reverse & Private::ReverseFirst) { d->oldControlPoint1 = d->pathShape->shapeToDocument(p1->controlPoint2()); } else { d->oldControlPoint1 = d->pathShape->shapeToDocument(p1->controlPoint1()); } d->oldNodePoint2 = d->pathShape->shapeToDocument(p2->point()); if (d->reverse & Private::ReverseSecond) { d->oldControlPoint2 = d->pathShape->shapeToDocument(p2->controlPoint1()); } else { d->oldControlPoint2 = d->pathShape->shapeToDocument(p2->controlPoint2()); } setText(kundo2_i18n("Merge points")); } KoPathPointMergeCommand::~KoPathPointMergeCommand() { delete d; } void KoPathPointMergeCommand::redo() { KUndo2Command::redo(); if (d->removedPoint) return; d->pathShape->update(); KoPathPoint * endPoint = d->pathShape->pointByIndex(d->endPoint); KoPathPoint * startPoint = d->pathShape->pointByIndex(d->startPoint); // are we just closing a single subpath ? if (d->endPoint.first == d->startPoint.first) { // change the endpoint of the subpath d->removedPoint = d->mergePoints(endPoint, startPoint); // set endpoint of subpath to close the subpath endPoint->setProperty(KoPathPoint::CloseSubpath); // 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) { d->pathShape->reverseSubpath(d->endPoint.first); } if (d->reverse & Private::ReverseSecond) { d->pathShape->reverseSubpath(d->startPoint.first); } // move the subpaths so the second is directly after the first d->pathShape->moveSubpath(d->startPoint.first, d->endPoint.first + 1); d->splitIndex = d->pathShape->pathPointIndex(endPoint); // join both subpaths 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(); d->pathShape->update(); } void KoPathPointMergeCommand::undo() { KUndo2Command::undo(); if (!d->removedPoint) return; d->pathShape->update(); // check if we just have closed a single subpath if (d->endPoint.first == d->startPoint.first) { // open the subpath at the old/new first point d->pathShape->openSubpath(d->startPoint); // reinsert the old first point d->pathShape->insertPoint(d->removedPoint, d->startPoint); // reposition the points d->resetPoints(d->endPoint, d->startPoint); } else { // break merged subpaths apart d->pathShape->breakAfter(d->splitIndex); // reinsert the old second point d->pathShape->insertPoint(d->removedPoint, KoPathPointIndex(d->splitIndex.first+1,0)); // reposition the first point d->resetPoints(d->splitIndex, KoPathPointIndex(d->splitIndex.first+1,0)); // move second subpath to its old position d->pathShape->moveSubpath(d->splitIndex.first+1, d->startPoint.first); // undo the reversion of the subpaths if (d->reverse & Private::ReverseFirst) { d->pathShape->reverseSubpath(d->endPoint.first); } if (d->reverse & Private::ReverseSecond) { d->pathShape->reverseSubpath(d->startPoint.first); } } d->pathShape->normalize(); d->pathShape->update(); // 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/KoPathPointMergeCommand.h b/libs/flake/commands/KoPathPointMergeCommand.h index 740fe8611e..a33d634c45 100644 --- a/libs/flake/commands/KoPathPointMergeCommand.h +++ b/libs/flake/commands/KoPathPointMergeCommand.h @@ -1,56 +1,58 @@ /* 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 KOPATHPOINTMERGECOMMAND_H #define KOPATHPOINTMERGECOMMAND_H #include #include "kritaflake_export.h" class KoPathPointData; /// The undo / redo command for merging two subpath end points class KRITAFLAKE_EXPORT KoPathPointMergeCommand : public KUndo2Command { public: /** * Command to merge two subpath end points. * * The points have to be from the same path shape. * * @param pointData1 the data of the first point to merge * @param pointData2 the data of the second point to merge * @param parent the parent command used for macro commands */ KoPathPointMergeCommand(const KoPathPointData &pointData1, const KoPathPointData &pointData2, KUndo2Command *parent = 0); ~KoPathPointMergeCommand(); /// redo the command void redo(); /// revert the actions done in redo void undo(); + KoPathPointData mergedPointData() const; + private: class Private; Private * const d; }; #endif // KOPATHPOINTMERGECOMMAND_H diff --git a/libs/flake/tests/TestPointMergeCommand.cpp b/libs/flake/tests/TestPointMergeCommand.cpp index a4e6b122c8..85f6f0f7c8 100644 --- a/libs/flake/tests/TestPointMergeCommand.cpp +++ b/libs/flake/tests/TestPointMergeCommand.cpp @@ -1,249 +1,564 @@ /* 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 "TestPointMergeCommand.h" #include "KoPathPointMergeCommand.h" #include "KoPathShape.h" #include "KoPathPoint.h" #include "KoPathPointData.h" #include #include void TestPointMergeCommand::closeSingleLinePath() { KoPathShape path1; path1.moveTo(QPointF(40, 0)); path1.lineTo(QPointF(60, 0)); path1.lineTo(QPointF(60, 30)); path1.lineTo(QPointF(0, 30)); path1.lineTo(QPointF(0, 0)); path1.lineTo(QPointF(20, 0)); KoPathPointIndex index1(0,0); KoPathPointIndex index2(0,5); KoPathPointData pd1(&path1, index1); KoPathPointData pd2(&path1, index2); KoPathPoint * p1 = path1.pointByIndex(index1); KoPathPoint * p2 = path1.pointByIndex(index2); QVERIFY(!path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 6); QCOMPARE(p1->point(), QPointF(40,0)); QCOMPARE(p2->point(), QPointF(20,0)); KoPathPointMergeCommand cmd1(pd1,pd2); cmd1.redo(); QVERIFY(path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 5); QCOMPARE(p2->point(), QPointF(30,0)); cmd1.undo(); QVERIFY(!path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 6); QCOMPARE(p1->point(), QPointF(40,0)); QCOMPARE(p2->point(), QPointF(20,0)); KoPathPointMergeCommand cmd2(pd2,pd1); cmd2.redo(); QVERIFY(path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 5); QCOMPARE(p2->point(), QPointF(30,0)); cmd2.undo(); QVERIFY(!path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 6); QCOMPARE(p1->point(), QPointF(40,0)); QCOMPARE(p2->point(), QPointF(20,0)); } void TestPointMergeCommand::closeSingleCurvePath() { KoPathShape path1; path1.moveTo(QPointF(40, 0)); path1.curveTo(QPointF(60, 0), QPointF(60,0), QPointF(60,60)); path1.lineTo(QPointF(0, 60)); path1.curveTo(QPointF(0, 0), QPointF(0,0), QPointF(20,0)); KoPathPointIndex index1(0,0); KoPathPointIndex index2(0,3); KoPathPointData pd1(&path1, index1); KoPathPointData pd2(&path1, index2); KoPathPoint * p1 = path1.pointByIndex(index1); KoPathPoint * p2 = path1.pointByIndex(index2); QVERIFY(!path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 4); QCOMPARE(p1->point(), QPointF(40,0)); QVERIFY(!p1->activeControlPoint1()); QCOMPARE(p2->point(), QPointF(20,0)); QVERIFY(!p2->activeControlPoint2()); KoPathPointMergeCommand cmd1(pd1,pd2); cmd1.redo(); QVERIFY(path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 3); QCOMPARE(p2->point(), QPointF(30,0)); QVERIFY(p2->activeControlPoint1()); QVERIFY(p2->activeControlPoint2()); cmd1.undo(); QVERIFY(!path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 4); QCOMPARE(p1->point(), QPointF(40,0)); QVERIFY(!p1->activeControlPoint1()); QCOMPARE(p2->point(), QPointF(20,0)); QVERIFY(!p2->activeControlPoint2()); KoPathPointMergeCommand cmd2(pd2,pd1); cmd2.redo(); QVERIFY(path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 3); QCOMPARE(p2->point(), QPointF(30,0)); QVERIFY(p2->activeControlPoint1()); QVERIFY(p2->activeControlPoint2()); cmd2.undo(); QVERIFY(!path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 4); QCOMPARE(p1->point(), QPointF(40,0)); QVERIFY(!p1->activeControlPoint1()); QCOMPARE(p2->point(), QPointF(20,0)); QVERIFY(!p2->activeControlPoint2()); } void TestPointMergeCommand::connectLineSubpaths() { KoPathShape path1; path1.moveTo(QPointF(0,0)); path1.lineTo(QPointF(10,0)); path1.moveTo(QPointF(20,0)); path1.lineTo(QPointF(30,0)); KoPathPointIndex index1(0,1); KoPathPointIndex index2(1,0); KoPathPointData pd1(&path1, index1); KoPathPointData pd2(&path1, index2); QCOMPARE(path1.subpathCount(), 2); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(10,0)); QCOMPARE(path1.pointByIndex(index2)->point(), QPointF(20,0)); KoPathPointMergeCommand cmd1(pd1, pd2); cmd1.redo(); QCOMPARE(path1.subpathCount(), 1); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(15,0)); cmd1.undo(); QCOMPARE(path1.subpathCount(), 2); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(10,0)); QCOMPARE(path1.pointByIndex(index2)->point(), QPointF(20,0)); KoPathPointMergeCommand cmd2(pd2, pd1); cmd2.redo(); QCOMPARE(path1.subpathCount(), 1); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(15,0)); cmd2.undo(); QCOMPARE(path1.subpathCount(), 2); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(10,0)); QCOMPARE(path1.pointByIndex(index2)->point(), QPointF(20,0)); } void TestPointMergeCommand::connectCurveSubpaths() { KoPathShape path1; path1.moveTo(QPointF(0,0)); path1.curveTo(QPointF(20,0),QPointF(0,20),QPointF(20,20)); path1.moveTo(QPointF(50,0)); path1.curveTo(QPointF(30,0), QPointF(50,20), QPointF(30,20)); KoPathPointIndex index1(0,1); KoPathPointIndex index2(1,1); KoPathPointData pd1(&path1, index1); KoPathPointData pd2(&path1, index2); QCOMPARE(path1.subpathCount(), 2); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(20,20)); QCOMPARE(path1.pointByIndex(index1)->controlPoint1(), QPointF(0,20)); QCOMPARE(path1.pointByIndex(index2)->point(), QPointF(30,20)); QCOMPARE(path1.pointByIndex(index2)->controlPoint1(), QPointF(50,20)); QVERIFY(path1.pointByIndex(index1)->activeControlPoint1()); QVERIFY(!path1.pointByIndex(index1)->activeControlPoint2()); KoPathPointMergeCommand cmd1(pd1, pd2); cmd1.redo(); QCOMPARE(path1.subpathCount(), 1); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(25,20)); QCOMPARE(path1.pointByIndex(index1)->controlPoint1(), QPointF(5,20)); QCOMPARE(path1.pointByIndex(index1)->controlPoint2(), QPointF(45,20)); QVERIFY(path1.pointByIndex(index1)->activeControlPoint1()); QVERIFY(path1.pointByIndex(index1)->activeControlPoint2()); cmd1.undo(); QCOMPARE(path1.subpathCount(), 2); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(20,20)); QCOMPARE(path1.pointByIndex(index1)->controlPoint1(), QPointF(0,20)); QCOMPARE(path1.pointByIndex(index2)->point(), QPointF(30,20)); QCOMPARE(path1.pointByIndex(index2)->controlPoint1(), QPointF(50,20)); QVERIFY(path1.pointByIndex(index1)->activeControlPoint1()); QVERIFY(!path1.pointByIndex(index1)->activeControlPoint2()); KoPathPointMergeCommand cmd2(pd2, pd1); cmd2.redo(); QCOMPARE(path1.subpathCount(), 1); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(25,20)); QCOMPARE(path1.pointByIndex(index1)->controlPoint1(), QPointF(5,20)); QCOMPARE(path1.pointByIndex(index1)->controlPoint2(), QPointF(45,20)); QVERIFY(path1.pointByIndex(index1)->activeControlPoint1()); QVERIFY(path1.pointByIndex(index1)->activeControlPoint2()); cmd2.undo(); QCOMPARE(path1.subpathCount(), 2); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(20,20)); QCOMPARE(path1.pointByIndex(index1)->controlPoint1(), QPointF(0,20)); QCOMPARE(path1.pointByIndex(index2)->point(), QPointF(30,20)); QCOMPARE(path1.pointByIndex(index2)->controlPoint1(), QPointF(50,20)); QVERIFY(path1.pointByIndex(index1)->activeControlPoint1()); 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/TestPointMergeCommand.h b/libs/flake/tests/TestPointMergeCommand.h index 197230863a..fc58322ce8 100644 --- a/libs/flake/tests/TestPointMergeCommand.h +++ b/libs/flake/tests/TestPointMergeCommand.h @@ -1,35 +1,53 @@ /* 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 TESTPOINTMERGECOMMAND_H #define TESTPOINTMERGECOMMAND_H #include class TestPointMergeCommand : public QObject { Q_OBJECT private Q_SLOTS: void closeSingleLinePath(); 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/tools/KoPathTool.cpp b/libs/flake/tools/KoPathTool.cpp index e49f6ba0d4..2b2f80af88 100644 --- a/libs/flake/tools/KoPathTool.cpp +++ b/libs/flake/tools/KoPathTool.cpp @@ -1,1175 +1,1221 @@ /* This file is part of the KDE project * Copyright (C) 2006-2012 Jan Hambrecht * Copyright (C) 2006,2007 Thorsten Zachmann * Copyright (C) 2007, 2010 Thomas Zander * Copyright (C) 2007 Boudewijn Rempt * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoPathTool.h" #include "KoToolBase_p.h" #include "KoPathShape_p.h" #include "KoPathToolHandle.h" #include "KoCanvasBase.h" #include "KoShapeManager.h" #include "KoSelectedShapesProxy.h" #include "KoDocumentResourceManager.h" #include "KoViewConverter.h" #include "KoSelection.h" #include "KoPointerEvent.h" #include "commands/KoPathPointTypeCommand.h" #include "commands/KoPathPointInsertCommand.h" #include "commands/KoPathPointRemoveCommand.h" #include "commands/KoPathSegmentTypeCommand.h" #include "commands/KoPathBreakAtPointCommand.h" #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" #include "KoPathSegmentChangeStrategy.h" #include "KoPathConnectionPointStrategy.h" #include "KoParameterChangeStrategy.h" #include "PathToolOptionWidget.h" #include "KoConnectionShape.h" #include "KoSnapGuide.h" #include "KoShapeController.h" #include "kis_action_registry.h" #include #include #include "kis_command_utils.h" + #include #include #include #include #include #include #include #include #include static const unsigned char needle_bits[] = { 0x00, 0x00, 0x10, 0x00, 0x20, 0x00, 0x60, 0x00, 0xc0, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x80, 0x07, 0x00, 0x0f, 0x00, 0x1f, 0x00, 0x3e, 0x00, 0x7e, 0x00, 0x7c, 0x00, 0x1c, 0x00, 0x18, 0x00, 0x00 }; static const unsigned char needle_move_bits[] = { 0x00, 0x00, 0x10, 0x00, 0x20, 0x00, 0x60, 0x00, 0xc0, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x80, 0x07, 0x10, 0x0f, 0x38, 0x1f, 0x54, 0x3e, 0xfe, 0x7e, 0x54, 0x7c, 0x38, 0x1c, 0x10, 0x18, 0x00, 0x00 }; // helper function to calculate the squared distance between two points qreal squaredDistance(const QPointF& p1, const QPointF &p2) { qreal dx = p1.x()-p2.x(); qreal dy = p1.y()-p2.y(); return dx*dx + dy*dy; } struct KoPathTool::PathSegment { PathSegment() : path(0), segmentStart(0), positionOnSegment(0) { } bool isValid() { return path && segmentStart; } KoPathShape *path; KoPathPoint *segmentStart; qreal positionOnSegment; }; KoPathTool::KoPathTool(KoCanvasBase *canvas) : KoToolBase(canvas) , m_pointSelection(this) , m_activeHandle(0) , m_handleRadius(3) , m_activeSegment(0) , m_currentStrategy(0) , m_activatedTemporarily(false) { QActionGroup *points = new QActionGroup(this); // m_pointTypeGroup->setExclusive(true); KisActionRegistry *actionRegistry = KisActionRegistry::instance(); m_actionPathPointCorner = actionRegistry->makeQAction("pathpoint-corner", this); addAction("pathpoint-corner", m_actionPathPointCorner); m_actionPathPointCorner->setData(KoPathPointTypeCommand::Corner); points->addAction(m_actionPathPointCorner); m_actionPathPointSmooth = actionRegistry->makeQAction("pathpoint-smooth", this); addAction("pathpoint-smooth", m_actionPathPointSmooth); m_actionPathPointSmooth->setData(KoPathPointTypeCommand::Smooth); points->addAction(m_actionPathPointSmooth); m_actionPathPointSymmetric = actionRegistry->makeQAction("pathpoint-symmetric", this); addAction("pathpoint-symmetric", m_actionPathPointSymmetric); m_actionPathPointSymmetric->setData(KoPathPointTypeCommand::Symmetric); points->addAction(m_actionPathPointSymmetric); m_actionCurvePoint = actionRegistry->makeQAction("pathpoint-curve", this); addAction("pathpoint-curve", m_actionCurvePoint); connect(m_actionCurvePoint, SIGNAL(triggered()), this, SLOT(pointToCurve())); m_actionLinePoint = actionRegistry->makeQAction("pathpoint-line", this); addAction("pathpoint-line", m_actionLinePoint); connect(m_actionLinePoint, SIGNAL(triggered()), this, SLOT(pointToLine())); m_actionLineSegment = actionRegistry->makeQAction("pathsegment-line", this); addAction("pathsegment-line", m_actionLineSegment); connect(m_actionLineSegment, SIGNAL(triggered()), this, SLOT(segmentToLine())); m_actionCurveSegment = actionRegistry->makeQAction("pathsegment-curve", this); addAction("pathsegment-curve", m_actionCurveSegment); connect(m_actionCurveSegment, SIGNAL(triggered()), this, SLOT(segmentToCurve())); m_actionAddPoint = actionRegistry->makeQAction("pathpoint-insert", this); addAction("pathpoint-insert", m_actionAddPoint); connect(m_actionAddPoint, SIGNAL(triggered()), this, SLOT(insertPoints())); m_actionRemovePoint = actionRegistry->makeQAction("pathpoint-remove", this); addAction("pathpoint-remove", m_actionRemovePoint); connect(m_actionRemovePoint, SIGNAL(triggered()), this, SLOT(removePoints())); m_actionBreakPoint = actionRegistry->makeQAction("path-break-point", this); addAction("path-break-point", m_actionBreakPoint); connect(m_actionBreakPoint, SIGNAL(triggered()), this, SLOT(breakAtPoint())); m_actionBreakSegment = actionRegistry->makeQAction("path-break-segment", this); addAction("path-break-segment", m_actionBreakSegment); connect(m_actionBreakSegment, SIGNAL(triggered()), this, SLOT(breakAtSegment())); m_actionJoinSegment = actionRegistry->makeQAction("pathpoint-join", this); addAction("pathpoint-join", m_actionJoinSegment); connect(m_actionJoinSegment, SIGNAL(triggered()), this, SLOT(joinPoints())); m_actionMergePoints = actionRegistry->makeQAction("pathpoint-merge", this); addAction("pathpoint-merge", m_actionMergePoints); connect(m_actionMergePoints, SIGNAL(triggered()), this, SLOT(mergePoints())); m_actionConvertToPath = actionRegistry->makeQAction("convert-to-path", this); 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())); QBitmap b = QBitmap::fromData(QSize(16, 16), needle_bits); QBitmap m = b.createHeuristicMask(false); m_selectCursor = QCursor(b, m, 2, 0); b = QBitmap::fromData(QSize(16, 16), needle_move_bits); m = b.createHeuristicMask(false); m_moveCursor = QCursor(b, m, 2, 0); } KoPathTool::~KoPathTool() { delete m_activeHandle; delete m_activeSegment; delete m_currentStrategy; } QList > KoPathTool::createOptionWidgets() { QList > list; 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("Edit Shape")); list.append(toolOptions); return list; } void KoPathTool::pointTypeChanged(QAction *type) { Q_D(KoToolBase); if (m_pointSelection.hasSelection()) { QList selectedPoints = m_pointSelection.selectedPointsData(); KUndo2Command *initialConversionCommand = createPointToCurveCommand(selectedPoints); // conversion should happen before the c-tor // of KoPathPointTypeCommand is executed! if (initialConversionCommand) { initialConversionCommand->redo(); } 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); - } + 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); } } } void KoPathTool::removePoints() { Q_D(KoToolBase); if (m_pointSelection.size() > 0) { KUndo2Command *cmd = KoPathPointRemoveCommand::createCommand(m_pointSelection.selectedPointsData(), d->canvas->shapeController()); PointHandle *pointHandle = dynamic_cast(m_activeHandle); if (pointHandle && m_pointSelection.contains(pointHandle->activePoint())) { delete m_activeHandle; m_activeHandle = 0; } clearActivePointSelectionReferences(); d->canvas->addCommand(cmd); } } void KoPathTool::pointToLine() { 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); } if (! pointToChange.isEmpty()) { d->canvas->addCommand(new KoPathPointTypeCommand(pointToChange, KoPathPointTypeCommand::Line)); } } } void KoPathTool::pointToCurve() { Q_D(KoToolBase); if (m_pointSelection.hasSelection()) { QList selectedPoints = m_pointSelection.selectedPointsData(); KUndo2Command *command = createPointToCurveCommand(selectedPoints); 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)); } } } void KoPathTool::segmentToCurve() { Q_D(KoToolBase); if (m_pointSelection.size() > 1) { QList segments(m_pointSelection.selectedSegmentsData()); if (segments.size() > 0) { d->canvas->addCommand(new KoPathSegmentTypeCommand(segments, KoPathSegmentTypeCommand::Curve)); } } } void KoPathTool::convertToPath() { Q_D(KoToolBase); QList shapesToConvert; Q_FOREACH (KoShape *shape, m_pointSelection.selectedShapes()) { KoParameterShape * parameterShape = dynamic_cast(shape); if (parameterShape && parameterShape->isParametricShape()) shapesToConvert.append(parameterShape); } if (shapesToConvert.count()) d->canvas->addCommand(new KoParameterToPathCommand(shapesToConvert)); 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); - } - } + 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); - d->canvas->addCommand(cmd); + KUndo2Command *cmd = 0; - KoPathPoint *pt = path->pointByIndex(index1); - if (!pt) { - pt = path->pointByIndex(index2); + 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); +} - if (pt) { - m_pointSelection.add(pt, true); - } +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())); } } void KoPathTool::breakAtSegment() { Q_D(KoToolBase); // only try to break a segment when 2 points of the same object are selected if (m_pointSelection.objectCount() == 1 && m_pointSelection.size() == 2) { QList segments(m_pointSelection.selectedSegmentsData()); if (segments.size() == 1) { d->canvas->addCommand(new KoPathSegmentBreakCommand(segments.at(0))); } } } void KoPathTool::paint(QPainter &painter, const KoViewConverter &converter) { Q_D(KoToolBase); Q_FOREACH (KoPathShape *shape, m_pointSelection.selectedShapes()) { KisHandlePainterHelper helper = KoShape::createHandlePainterHelper(&painter, shape, converter, m_handleRadius); helper.setHandleStyle(KisHandleStyle::primarySelection()); KoParameterShape * parameterShape = dynamic_cast(shape); if (parameterShape && parameterShape->isParametricShape()) { parameterShape->paintHandles(helper); } else { shape->paintPoints(helper); } 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(); } m_pointSelection.paint(painter, converter, m_handleRadius); if (m_activeHandle) { if (m_activeHandle->check(m_pointSelection.selectedShapes())) { 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); d->canvas->snapGuide()->paint(painter, converter); painter.restore(); } } void KoPathTool::repaintDecorations() { Q_FOREACH (KoShape *shape, m_pointSelection.selectedShapes()) { repaint(shape->boundingRect()); } m_pointSelection.repaint(); updateOptionsWidget(); } void KoPathTool::mousePressEvent(KoPointerEvent *event) { // we are moving if we hit a point and use the left mouse button event->ignore(); if (m_activeHandle) { m_currentStrategy = m_activeHandle->handleMousePress(event); event->accept(); } else { if (event->button() & Qt::LeftButton) { // 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(); } else { 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(); } } } } } void KoPathTool::mouseMoveEvent(KoPointerEvent *event) { if (event->button() & Qt::RightButton) return; if (m_currentStrategy) { m_lastPoint = event->point; m_currentStrategy->handleMouseMove(event->point, event->modifiers()); // repaint new handle positions m_pointSelection.repaint(); if (m_activeHandle) { m_activeHandle->repaint(); } if (m_activeSegment) { repaintSegment(m_activeSegment); } return; } 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)); KoParameterShape * parameterShape = dynamic_cast(shape); if (parameterShape && parameterShape->isParametricShape()) { int handleId = parameterShape->handleIdAt(roi); if (handleId != -1) { useCursor(m_moveCursor); emit statusTextChanged(i18n("Drag to move handle.")); if (m_activeHandle) m_activeHandle->repaint(); delete m_activeHandle; if (KoConnectionShape * connectionShape = dynamic_cast(parameterShape)) { //qDebug() << "handleId" << handleId; m_activeHandle = new ConnectionHandle(this, connectionShape, handleId); m_activeHandle->repaint(); return; } else { //qDebug() << "handleId" << handleId; m_activeHandle = new ParameterHandle(this, parameterShape, handleId); m_activeHandle->repaint(); return; } } } else { QList points = shape->pointsAt(roi); if (! points.empty()) { // find the nearest control point from all points within the roi KoPathPoint * bestPoint = 0; KoPathPoint::PointType bestPointType = KoPathPoint::Node; qreal minDistance = HUGE_VAL; Q_FOREACH (KoPathPoint *p, points) { // the node point must be hit if the point is not selected yet if (! m_pointSelection.contains(p) && ! roi.contains(p->point())) continue; // check for the control points first as otherwise it is no longer // possible to change the control points when they are the same as the point if (p->activeControlPoint1() && roi.contains(p->controlPoint1())) { qreal dist = squaredDistance(roi.center(), p->controlPoint1()); if (dist < minDistance) { bestPoint = p; bestPointType = KoPathPoint::ControlPoint1; minDistance = dist; } } if (p->activeControlPoint2() && roi.contains(p->controlPoint2())) { qreal dist = squaredDistance(roi.center(), p->controlPoint2()); if (dist < minDistance) { bestPoint = p; bestPointType = KoPathPoint::ControlPoint2; minDistance = dist; } } // check the node point at last qreal dist = squaredDistance(roi.center(), p->point()); if (dist < minDistance) { bestPoint = p; bestPointType = KoPathPoint::Node; minDistance = dist; } } if (! bestPoint) return; useCursor(m_moveCursor); if (bestPointType == KoPathPoint::Node) emit statusTextChanged(i18n("Drag to move point. Shift click to change point type.")); else emit statusTextChanged(i18n("Drag to move control point.")); PointHandle *prev = dynamic_cast(m_activeHandle); if (prev && prev->activePoint() == bestPoint && prev->activePointType() == bestPointType) return; // no change; if (m_activeHandle) m_activeHandle->repaint(); delete m_activeHandle; m_activeHandle = new PointHandle(this, bestPoint, bestPointType); m_activeHandle->repaint(); return; } } } useCursor(m_selectCursor); 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) emit statusTextChanged(QString()); else if (selectedPointCount == 1) emit statusTextChanged(i18n("Press B to break path at selected point.")); else emit statusTextChanged(i18n("Press B to break path at selected segments.")); } } 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); if (m_currentStrategy) { const bool hadNoSelection = !m_pointSelection.hasSelection(); m_currentStrategy->finishInteraction(event->modifiers()); KUndo2Command *command = m_currentStrategy->createCommand(); if (command) d->canvas->addCommand(command); if (hadNoSelection && dynamic_cast(m_currentStrategy) && !m_pointSelection.hasSelection()) { // the click didn't do anything at all. Allow it to be used by others. event->ignore(); } delete m_currentStrategy; m_currentStrategy = 0; } } void KoPathTool::keyPressEvent(QKeyEvent *event) { Q_D(KoToolBase); if (m_currentStrategy) { switch (event->key()) { case Qt::Key_Control: case Qt::Key_Alt: case Qt::Key_Shift: case Qt::Key_Meta: if (! event->isAutoRepeat()) { m_currentStrategy->handleMouseMove(m_lastPoint, event->modifiers()); } break; case Qt::Key_Escape: m_currentStrategy->cancelInteraction(); delete m_currentStrategy; m_currentStrategy = 0; break; default: event->ignore(); return; } } else { switch (event->key()) { #ifndef NDEBUG case Qt::Key_D: if (m_pointSelection.objectCount() == 1) { QList selectedPoints = m_pointSelection.selectedPointsData(); KoPathShapePrivate *p = static_cast(selectedPoints[0].pathShape->priv()); p->debugPath(); } break; #endif case Qt::Key_B: if (m_pointSelection.size() == 1) breakAtPoint(); else if (m_pointSelection.size() >= 2) breakAtSegment(); break; default: event->ignore(); return; } } event->accept(); } void KoPathTool::keyReleaseEvent(QKeyEvent *event) { if (m_currentStrategy) { switch (event->key()) { case Qt::Key_Control: case Qt::Key_Alt: case Qt::Key_Shift: case Qt::Key_Meta: if (! event->isAutoRepeat()) { m_currentStrategy->handleMouseMove(m_lastPoint, Qt::NoModifier); } break; default: break; } } event->accept(); } void KoPathTool::mouseDoubleClickEvent(KoPointerEvent *event) { Q_D(KoToolBase); event->ignore(); // check if we are doing something else at the moment if (m_currentStrategy) return; QScopedPointer s(segmentAtPoint(event->point)); if (s && s->isValid()) { QList segments; segments.append(KoPathPointData(s->path, s->path->pathPointIndex(s->segmentStart))); KoPathPointInsertCommand *cmd = new KoPathPointInsertCommand(segments, s->positionOnSegment); d->canvas->addCommand(cmd); foreach (KoPathPoint * p, cmd->insertedPoints()) { m_pointSelection.add(p, false); } updateActions(); event->accept(); } else if (!m_activeHandle && !m_activeSegment && m_activatedTemporarily) { emit done(); } } KoPathTool::PathSegment* KoPathTool::segmentAtPoint(const QPointF &point) { Q_D(KoToolBase); const int clickProximity = 5; // convert click proximity to point using the current zoom level QPointF clickOffset = d->canvas->viewConverter()->viewToDocument(QPointF(clickProximity, clickProximity)); // the max allowed distance from a segment const qreal maxSquaredDistance = clickOffset.x()*clickOffset.x(); QScopedPointer segment(new PathSegment); Q_FOREACH (KoPathShape *shape, m_pointSelection.selectedShapes()) { KoParameterShape * parameterShape = dynamic_cast(shape); if (parameterShape && parameterShape->isParametricShape()) continue; // convert document point to shape coordinates QPointF p = shape->documentToShape(point); // our region of interest, i.e. a region around our mouse position QRectF roi(p - clickOffset, p + clickOffset); qreal minSqaredDistance = HUGE_VAL; // check all segments of this shape which intersect the region of interest QList segments = shape->segmentsAt(roi); foreach (const KoPathSegment &s, segments) { qreal nearestPointParam = s.nearestPoint(p); QPointF nearestPoint = s.pointAt(nearestPointParam); QPointF diff = p - nearestPoint; qreal squaredDistance = diff.x()*diff.x() + diff.y()*diff.y(); // are we within the allowed distance ? if (squaredDistance > maxSquaredDistance) continue; // are we closer to the last closest point ? if (squaredDistance < minSqaredDistance) { segment->path = shape; segment->segmentStart = s.first(); segment->positionOnSegment = nearestPointParam; } } } if (!segment->isValid()) { segment.reset(); } return segment.take(); } void KoPathTool::activate(ToolActivation activation, const QSet &shapes) { KoToolBase::activate(activation, shapes); Q_D(KoToolBase); m_activatedTemporarily = activation == TemporaryActivation; // retrieve the actual global handle radius m_handleRadius = handleRadius(); d->canvas->snapGuide()->reset(); 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) { - clearActivePointSelectionReferences(); - - repaintDecorations(); 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); } } - m_pointSelection.setSelectedShapes(selectedShapes); + if (selectedShapes != m_pointSelection.selectedShapes()) { + clearActivePointSelectionReferences(); + m_pointSelection.setSelectedShapes(selectedShapes); + repaintDecorations(); + } + + 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()); + } + updateOptionsWidget(); updateActions(); } void KoPathTool::updateOptionsWidget() { PathToolOptionWidget::Types type; QList selectedShapes = m_pointSelection.selectedShapes(); Q_FOREACH (KoPathShape *shape, selectedShapes) { KoParameterShape * parameterShape = dynamic_cast(shape); type |= parameterShape && parameterShape->isParametricShape() ? PathToolOptionWidget::ParametricShape : PathToolOptionWidget::PlainPath; } emit singleShapeChanged(selectedShapes.size() == 1 ? selectedShapes.first() : 0); emit typeChanged(type); } void KoPathTool::updateActions() { 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 canJoinSubpaths = false; + 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); - m_actionJoinSegment->setEnabled(canJoinSubpaths); - m_actionMergePoints->setEnabled(canJoinSubpaths); + } void KoPathTool::deactivate() { Q_D(KoToolBase); m_canvasConnections.clear(); m_pointSelection.clear(); m_pointSelection.setSelectedShapes(QList()); delete m_activeHandle; m_activeHandle = 0; delete m_activeSegment; m_activeSegment = 0; delete m_currentStrategy; m_currentStrategy = 0; d->canvas->snapGuide()->reset(); KoToolBase::deactivate(); } void KoPathTool::documentResourceChanged(int key, const QVariant & res) { if (key == KoDocumentResourceManager::HandleRadius) { int oldHandleRadius = m_handleRadius; m_handleRadius = res.toUInt(); // repaint with the bigger of old and new handle radius int maxRadius = qMax(m_handleRadius, oldHandleRadius); Q_FOREACH (KoPathShape *shape, m_pointSelection.selectedShapes()) { QRectF controlPointRect = shape->absoluteTransformation(0).map(shape->outline()).controlPointRect(); repaint(controlPointRect.adjusted(-maxRadius, -maxRadius, maxRadius, maxRadius)); } } } void KoPathTool::pointSelectionChanged() { Q_D(KoToolBase); updateActions(); d->canvas->snapGuide()->setIgnoredPathPoints(m_pointSelection.selectedPoints().toList()); emit selectionChanged(m_pointSelection.hasSelection()); } void KoPathTool::repaint(const QRectF &repaintRect) { Q_D(KoToolBase); //debugFlake <<"KoPathTool::repaint(" << repaintRect <<")" << m_handleRadius; // widen border to take antialiasing into account qreal radius = m_handleRadius + 1; 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(); } KoToolSelection * KoPathTool::selection() { 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/KoPathTool.h b/libs/flake/tools/KoPathTool.h index 005528b48e..336fa8601b 100644 --- a/libs/flake/tools/KoPathTool.h +++ b/libs/flake/tools/KoPathTool.h @@ -1,156 +1,156 @@ /* This file is part of the KDE project * Copyright (C) 2006-2012 Jan Hambrecht * Copyright (C) 2006,2007 Thorsten Zachmann * Copyright (C) 2007 Thomas Zander * Copyright (C) 2007 Boudewijn Rempt * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOPATHTOOL_H #define KOPATHTOOL_H #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. class KRITAFLAKE_EXPORT KoPathTool : public KoToolBase { Q_OBJECT public: explicit KoPathTool(KoCanvasBase *canvas); ~KoPathTool(); 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 singleShapeChanged(KoPathShape* path); protected: /// reimplemented virtual QList > createOptionWidgets(); private: struct PathSegment; void updateOptionsWidget(); PathSegment* segmentAtPoint(const QPointF &point); private Q_SLOTS: void pointTypeChanged(QAction *type); void insertPoints(); void removePoints(); void segmentToLine(); void segmentToCurve(); void convertToPath(); void joinPoints(); void mergePoints(); void breakAtPoint(); void breakAtSegment(); void pointSelectionChanged(); void updateActions(); void pointToLine(); void pointToCurve(); 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 QPointF m_lastPoint; ///< needed for interaction strategy PathSegment *m_activeSegment; // make a frind so that it can test private member/methods friend class TestPathTool; KoInteractionStrategy *m_currentStrategy; ///< the rubber selection strategy QButtonGroup *m_pointTypeGroup; QAction *m_actionPathPointCorner; QAction *m_actionPathPointSmooth; QAction *m_actionPathPointSymmetric; QAction *m_actionCurvePoint; QAction *m_actionLinePoint; QAction *m_actionLineSegment; QAction *m_actionCurveSegment; QAction *m_actionAddPoint; QAction *m_actionRemovePoint; QAction *m_actionBreakPoint; QAction *m_actionBreakSegment; QAction *m_actionJoinSegment; QAction *m_actionMergePoints; QAction *m_actionConvertToPath; QCursor m_moveCursor; bool m_activatedTemporarily; QScopedPointer m_contextMenu; KisSignalAutoConnectionsStore m_canvasConnections; Q_DECLARE_PRIVATE(KoToolBase) }; #endif diff --git a/libs/flake/tools/KoPathToolHandle.cpp b/libs/flake/tools/KoPathToolHandle.cpp index 89e44b70b9..58c7ea3328 100644 --- a/libs/flake/tools/KoPathToolHandle.cpp +++ b/libs/flake/tools/KoPathToolHandle.cpp @@ -1,209 +1,219 @@ /* This file is part of the KDE project * Copyright (C) 2006,2008 Jan Hambrecht * Copyright (C) 2006,2007 Thorsten Zachmann * Copyright (C) 2007,2010 Thomas Zander * Copyright (C) 2007 Boudewijn Rempt * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoPathToolHandle.h" #include "KoPathTool.h" #include "KoPathPointMoveStrategy.h" #include "KoPathControlPointMoveStrategy.h" #include "KoPathConnectionPointStrategy.h" #include "KoSelection.h" #include "commands/KoPathPointTypeCommand.h" #include "KoParameterChangeStrategy.h" #include "KoParameterShape.h" #include "KoCanvasBase.h" #include "KoDocumentResourceManager.h" #include "KoConnectionShape.h" #include "KoViewConverter.h" #include "KoPointerEvent.h" #include "KoShapeController.h" #include #include KoPathToolHandle::KoPathToolHandle(KoPathTool *tool) : m_tool(tool) { } KoPathToolHandle::~KoPathToolHandle() { } uint KoPathToolHandle::handleRadius() const { return m_tool->canvas()->shapeController()->resourceManager()->handleRadius(); } PointHandle::PointHandle(KoPathTool *tool, KoPathPoint *activePoint, KoPathPoint::PointType activePointType) : KoPathToolHandle(tool) , m_activePoint(activePoint) , m_activePointType(activePointType) { } void PointHandle::paint(QPainter &painter, const KoViewConverter &converter, qreal handleRadius) { KoPathToolSelection * selection = dynamic_cast(m_tool->selection()); KoPathPoint::PointType type = KoPathPoint::Node; if (selection && selection->contains(m_activePoint)) { type = KoPathPoint::All; } KisHandlePainterHelper helper = KoShape::createHandlePainterHelper(&painter, m_activePoint->parent(), converter, handleRadius); helper.setHandleStyle(KisHandleStyle::highlightedPrimaryHandles()); m_activePoint->paint(helper, type); } void PointHandle::repaint() const { m_tool->repaint(m_oldRepaintedRect); bool active = false; KoPathToolSelection * selection = dynamic_cast(m_tool->selection()); if (selection && selection->contains(m_activePoint)) active = true; m_oldRepaintedRect = m_activePoint->boundingRect(!active); m_tool->repaint(m_oldRepaintedRect); } KoInteractionStrategy * PointHandle::handleMousePress(KoPointerEvent *event) { if ((event->button() & Qt::LeftButton) == 0) return 0; 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::ShiftModifier) { if (selection->contains(m_activePoint)) { selection->remove(m_activePoint); } else { selection->add(m_activePoint, false); } m_tool->repaint(m_activePoint->boundingRect(false)); } else { // no control modifier, so clear selection and select active point if (!selection->contains(m_activePoint)) { selection->add(m_activePoint, true); m_tool->repaint(m_activePoint->boundingRect(false)); } } // TODO remove canvas from call ? if (m_activePointType == KoPathPoint::Node) { QPointF startPoint = m_activePoint->parent()->shapeToDocument(m_activePoint->point()); return new KoPathPointMoveStrategy(m_tool, startPoint); } else { KoPathShape * pathShape = m_activePoint->parent(); KoPathPointData pd(pathShape, pathShape->pathPointIndex(m_activePoint)); return new KoPathControlPointMoveStrategy(m_tool, pd, m_activePointType, event->point); } } else { KoPathPoint::PointProperties props = m_activePoint->properties(); if (! m_activePoint->activeControlPoint1() || ! m_activePoint->activeControlPoint2()) return 0; KoPathPointTypeCommand::PointType pointType = KoPathPointTypeCommand::Smooth; // cycle the smooth->symmetric->unsmooth state of the path point if (props & KoPathPoint::IsSmooth) pointType = KoPathPointTypeCommand::Symmetric; else if (props & KoPathPoint::IsSymmetric) pointType = KoPathPointTypeCommand::Corner; QList pointData; pointData.append(KoPathPointData(m_activePoint->parent(), m_activePoint->parent()->pathPointIndex(m_activePoint))); m_tool->canvas()->addCommand(new KoPathPointTypeCommand(pointData, pointType)); } return 0; } bool PointHandle::check(const QList &selectedShapes) { if (selectedShapes.contains(m_activePoint->parent())) { return m_activePoint->parent()->pathPointIndex(m_activePoint) != KoPathPointIndex(-1, -1); } return false; } KoPathPoint * PointHandle::activePoint() const { return m_activePoint; } KoPathPoint::PointType PointHandle::activePointType() const { 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, qreal handleRadius) { KisHandlePainterHelper helper = KoShape::createHandlePainterHelper(&painter, m_parameterShape, converter, handleRadius); helper.setHandleStyle(KisHandleStyle::highlightedPrimaryHandles()); m_parameterShape->paintHandle(helper, m_handleId); } void ParameterHandle::repaint() const { m_tool->repaint(m_parameterShape->shapeToDocument(QRectF(m_parameterShape->handlePosition(m_handleId), QSize(1, 1)))); } KoInteractionStrategy * ParameterHandle::handleMousePress(KoPointerEvent *event) { if (event->button() & Qt::LeftButton) { KoPathToolSelection * selection = dynamic_cast(m_tool->selection()); if (selection) selection->clear(); return new KoParameterChangeStrategy(m_tool, m_parameterShape, m_handleId); } return 0; } bool ParameterHandle::check(const QList &selectedShapes) { return selectedShapes.contains(m_parameterShape); } ConnectionHandle::ConnectionHandle(KoPathTool *tool, KoParameterShape *parameterShape, int handleId) : ParameterHandle(tool, parameterShape, handleId) { } KoInteractionStrategy * ConnectionHandle::handleMousePress(KoPointerEvent *event) { if (event->button() & Qt::LeftButton) { KoPathToolSelection * selection = dynamic_cast(m_tool->selection()); if (selection) selection->clear(); KoConnectionShape * shape = dynamic_cast(m_parameterShape); if (! shape) return 0; return new KoPathConnectionPointStrategy(m_tool, shape, m_handleId); } return 0; } diff --git a/libs/flake/tools/KoPathToolHandle.h b/libs/flake/tools/KoPathToolHandle.h index d2c5b62485..a363a6ed7b 100644 --- a/libs/flake/tools/KoPathToolHandle.h +++ b/libs/flake/tools/KoPathToolHandle.h @@ -1,96 +1,99 @@ /* This file is part of the KDE project * Copyright (C) 2006,2008 Jan Hambrecht * Copyright (C) 2006,2007 Thorsten Zachmann * Copyright (C) 2007,2010 Thomas Zander * Copyright (C) 2007 Boudewijn Rempt * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOPATHTOOLHANDLE_H #define KOPATHTOOLHANDLE_H #include #include "KoInteractionStrategy.h" #include #include class KoPathTool; class KoParameterShape; class KoViewConverter; 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, 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; }; class PointHandle : public KoPathToolHandle { public: PointHandle(KoPathTool *tool, KoPathPoint *activePoint, KoPathPoint::PointType activePointType); 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; mutable QRectF m_oldRepaintedRect; }; class ParameterHandle : public KoPathToolHandle { public: ParameterHandle(KoPathTool *tool, KoParameterShape *parameterShape, int handleId); void paint(QPainter &painter, const KoViewConverter &converter, qreal handleRadius); void repaint() const; KoInteractionStrategy *handleMousePress(KoPointerEvent *event); bool check(const QList &selectedShapes); protected: KoParameterShape *m_parameterShape; int m_handleId; }; class ConnectionHandle : public ParameterHandle { public: ConnectionHandle(KoPathTool *tool, KoParameterShape *parameterShape, int handleId); // XXX: Later: create a paint even to distinguish a connection // handle from another handle type KoInteractionStrategy *handleMousePress(KoPointerEvent *event); }; #endif // KOPATHTOOLHANDLE_H diff --git a/libs/ui/input/kis_show_palette_action.cpp b/libs/ui/input/kis_show_palette_action.cpp index e53bb2ea6f..c10da1fa24 100644 --- a/libs/ui/input/kis_show_palette_action.cpp +++ b/libs/ui/input/kis_show_palette_action.cpp @@ -1,76 +1,104 @@ /* This file is part of the KDE project * Copyright (C) 2012 Arjen Hiemstra * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_show_palette_action.h" #include #include #include #include #include #include "kis_tool_proxy.h" #include "kis_input_manager.h" KisShowPaletteAction::KisShowPaletteAction() - : KisAbstractInputAction("Show Popup Palette") + : KisAbstractInputAction("Show Popup Palette"), + m_requestedWithStylus(false) { setName(i18n("Show Popup Palette")); setDescription(i18n("The Show Popup Palette displays the popup palette.")); } KisShowPaletteAction::~KisShowPaletteAction() { } int KisShowPaletteAction::priority() const { return 1; } void KisShowPaletteAction::begin(int, QEvent *event) { m_menu = inputManager()->toolProxy()->popupActionsMenu(); if (m_menu) { + m_requestedWithStylus = event->type() == QEvent::TabletPress; + /** * Opening a menu changes the focus of the windows, so we should not open it * inside the filtering loop. Just raise it using the timer. */ QTimer::singleShot(0, this, SLOT(slotShowMenu())); } else { QPoint pos = eventPos(event); if (pos.isNull()) { pos = inputManager()->canvas()->canvasWidget()->mapFromGlobal(QCursor::pos()); } inputManager()->canvas()->slotShowPopupPalette(pos); } } +struct SinglePressEventEater : public QObject +{ + bool eventFilter(QObject *, QEvent *event) { + if (hungry && event->type() == QEvent::MouseButtonPress) { + hungry = false; + return true; + } + + return false; + } + +private: + bool hungry = true; +}; + void KisShowPaletteAction::slotShowMenu() { if (m_menu) { - m_menu->exec(QCursor::pos()); + + QPoint stylusOffset; + QScopedPointer eater; + + if (m_requestedWithStylus) { + eater.reset(new SinglePressEventEater()); + m_menu->installEventFilter(eater.data()); + stylusOffset += QPoint(10,10); + } + + m_menu->exec(QCursor::pos() + stylusOffset); m_menu.clear(); } } diff --git a/libs/ui/input/kis_show_palette_action.h b/libs/ui/input/kis_show_palette_action.h index d497b2a08a..6cd2606845 100644 --- a/libs/ui/input/kis_show_palette_action.h +++ b/libs/ui/input/kis_show_palette_action.h @@ -1,52 +1,53 @@ /* This file is part of the KDE project * Copyright (C) 2012 Arjen Hiemstra * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_SHOW_PALETTE_ACTION_H #define KIS_SHOW_PALETTE_ACTION_H #include "kis_abstract_input_action.h" #include #include class QMenu; /** * \brief Show Palette implementation of KisAbstractInputAction. * * The Show Palette action shows the popup palette. */ class KisShowPaletteAction : public QObject, public KisAbstractInputAction { Q_OBJECT public: explicit KisShowPaletteAction(); virtual ~KisShowPaletteAction(); virtual int priority() const; virtual void begin(int, QEvent *); private Q_SLOTS: void slotShowMenu(); private: QPointer m_menu; + bool m_requestedWithStylus; }; #endif // KIS_SHOW_PALETTE_ACTION_H