diff --git a/licence.txt b/licence.txt index 5686b2a4..d0ac9a70 100644 --- a/licence.txt +++ b/licence.txt @@ -1,6 +1,7 @@ BSD License 2.0 Copyright (c) 2008-2023, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2025, Siemens Energy Global GmbH & Co. KG Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/samples/edges/Ortho.qml b/samples/edges/Ortho.qml index d98c79cb..d2d2d65c 100644 --- a/samples/edges/Ortho.qml +++ b/samples/edges/Ortho.qml @@ -57,14 +57,26 @@ Qan.GraphView { console.error('pos=' + pos) } } // Qan.Graph - CheckBox { + Column { anchors.top: parent.top; anchors.topMargin: 4 - anchors.right: parent.right; anchors.rightMargin: 2 - id: dashed - text: "Ortho" - checked: defaultEdgeStyle.lineType === Qan.EdgeStyle.Ortho - onClicked: { - defaultEdgeStyle.lineType = !checked ? Qan.EdgeStyle.Straight : Qan.EdgeStyle.Ortho + anchors.right: parent.right; anchors.rightMargin: 4 + + RadioButton { + text: "Straight" + checked: defaultEdgeStyle.lineType === Qan.EdgeStyle.Straight + onClicked: defaultEdgeStyle.lineType = Qan.EdgeStyle.Straight + } + RadioButton { + text: "Ortho" + checked: defaultEdgeStyle.lineType === Qan.EdgeStyle.Ortho + onClicked: defaultEdgeStyle.lineType = Qan.EdgeStyle.Ortho + } + RadioButton { + text: "OrthoDouble" + checked: defaultEdgeStyle.lineType === Qan.EdgeStyle.OrthoDouble + onClicked: defaultEdgeStyle.lineType = Qan.EdgeStyle.OrthoDouble } } + + } // Qan.GraphView diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 80b48aa4..2b615568 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -106,6 +106,7 @@ set(qan_qml_files EdgeTemplate.qml EdgeStraightPath.qml EdgeOrthoPath.qml + EdgeOrthoDoublePath.qml EdgeCurvedPath.qml EdgeSrcArrowPath.qml EdgeSrcCirclePath.qml @@ -141,12 +142,13 @@ set(qan_qml_files # Configure Qt set(CMAKE_AUTOMOC ON) -#qt_wrap_cpp(qan_moc_files ${qan_header_files}) # Creates .moc files from sources set(CMAKE_AUTORCC ON) set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS $<$:QT_QML_DEBUG>) # Configure QuickQanava library ############################################### +qt_policy(SET QTP0001 NEW) + # qt_add_library(QuickQanava STATIC) qt_add_qml_module(QuickQanava STATIC @@ -156,10 +158,8 @@ qt_add_qml_module(QuickQanava ${qan_source_files} ${qan_header_files} ${qan_moc_files} ${quickcontainers_source_files} ${quickcontainers_header_files} QML_FILES ${qan_qml_files} - RESOURCE_PREFIX / - OUTPUT_DIRECTORY QuickQanava ) -set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/QuickQanava) +set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml) target_include_directories(QuickQanava PUBLIC diff --git a/src/EdgeOrthoDoublePath.qml b/src/EdgeOrthoDoublePath.qml new file mode 100644 index 00000000..4b2bd716 --- /dev/null +++ b/src/EdgeOrthoDoublePath.qml @@ -0,0 +1,100 @@ +/* + Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2025, Siemens Energy Global GmbH & Co. KG + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the author or Destrat.io nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL AUTHOR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +//----------------------------------------------------------------------------- +// This file is a part of the QuickQanava software library. +// +// \file EdgeOrthoDoublePath.qml +// \author morgan.bengtsson.ext@siemens-energy.com +// \date 2025 07 21 +//----------------------------------------------------------------------------- + +import QtQuick +import QtQuick.Shapes + +ShapePath { + id: edgeShapePath + + // Set in EdgeTemplate.qml createObject() from global qanEdgeOrthoPathComponent + property var edgeTemplate: undefined + property var edgeItem: edgeTemplate.edgeItem + + startX: edgeItem.p1.x + startY: edgeItem.p1.y + capStyle: ShapePath.FlatCap + strokeWidth: edgeItem?.style?.lineWidth ?? 2 + strokeColor: edgeTemplate.color + strokeStyle: edgeTemplate.dashed + dashPattern: edgeItem?.style?.dashPattern ?? [4, 2] + fillColor: Qt.rgba(0,0,0,0) + + property real edgeLengthY: Math.abs(edgeItem.p2.y - edgeItem.p1.y) + property real edgeLengthX: Math.abs(edgeItem.p2.x - edgeItem.p1.x) + property int horizontalDir: Math.sign(edgeItem.p1.x - edgeItem.p2.x); + property int verticalDir: Math.sign(edgeItem.p1.y - edgeItem.p2.y); + property bool isPoint: edgeItem.c1.x === edgeItem.c2.x && edgeItem.c1.y === edgeItem.c2.y + property bool horizontalEdge: (edgeItem.c1.x === edgeItem.c2.x) && !isPoint + property bool verticalEdge: (edgeItem.c1.y === edgeItem.c2.y) && !isPoint + property real radius: Math.min(edgeItem?.style?.orthoRadius ?? 10, (horizontalEdge ? edgeLengthY : edgeLengthX) / 2) + + PathLine { + x: edgeShapePath.edgeItem.c1.x + edgeShapePath.horizontalEdge * edgeShapePath.horizontalDir * edgeShapePath.radius + y: edgeShapePath.edgeItem.c1.y + edgeShapePath.verticalEdge * edgeShapePath.verticalDir * edgeShapePath.radius + } + PathArc { + x: edgeShapePath.edgeItem.c1.x - edgeShapePath.verticalEdge * edgeShapePath.horizontalDir * edgeShapePath.radius + y: edgeShapePath.edgeItem.c1.y - edgeShapePath.horizontalEdge * edgeShapePath.verticalDir * edgeShapePath.radius + radiusX: edgeShapePath.radius + radiusY: edgeShapePath.radius + useLargeArc: false + direction: (edgeShapePath.verticalEdge + ? edgeShapePath.verticalDir !== edgeShapePath.horizontalDir + : edgeShapePath.verticalDir === edgeShapePath.horizontalDir) + ? PathArc.Clockwise + : PathArc.Counterclockwise; + } + PathLine { + x: edgeShapePath.edgeItem.c2.x + edgeShapePath.verticalEdge * edgeShapePath.horizontalDir * edgeShapePath.radius + y: edgeShapePath.edgeItem.c2.y + edgeShapePath.horizontalEdge * edgeShapePath.verticalDir * edgeShapePath.radius + } + PathArc { + x: edgeShapePath.edgeItem.c2.x - edgeShapePath.horizontalEdge * edgeShapePath.horizontalDir * edgeShapePath.radius + y: edgeShapePath.edgeItem.c2.y - edgeShapePath.verticalEdge * edgeShapePath.verticalDir * edgeShapePath.radius + radiusX: edgeShapePath.radius + radiusY: edgeShapePath.radius + useLargeArc: false + direction: (edgeShapePath.horizontalEdge + ? edgeShapePath.verticalDir !== edgeShapePath.horizontalDir + : edgeShapePath.verticalDir === edgeShapePath.horizontalDir) + ? PathArc.Clockwise + : PathArc.Counterclockwise; + } + PathLine { + x: edgeShapePath.edgeItem.p2.x + y: edgeShapePath.edgeItem.p2.y + } +} diff --git a/src/EdgeTemplate.qml b/src/EdgeTemplate.qml index 5b08f5e6..a35014f9 100644 --- a/src/EdgeTemplate.qml +++ b/src/EdgeTemplate.qml @@ -1,5 +1,6 @@ /* Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2025, Siemens Energy Global GmbH & Co. KG Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -137,6 +138,7 @@ Item { property var curvedLine : undefined property var straightLine : undefined property var orthoLine : undefined + property var orthoDoubleLine: undefined property var lineType: edgeTemplate.lineType property var lineWidth: edgeItem?.style?.lineWidth + 2. ?? 4. property var lineColor: edgeItem && @@ -150,6 +152,7 @@ Item { switch (lineType) { case Qan.EdgeStyle.Undefined: // falltrought case Qan.EdgeStyle.Straight: + if (orthoDoubleLine) orthoDoubleLine.destroy() if (orthoLine) orthoLine.destroy() if (curvedLine) curvedLine.destroy() edgeSelectionShape.data = straightLine = qanEdgeStraightPathComponent.createObject(edgeSelectionShape, { @@ -159,6 +162,7 @@ Item { }); break; case Qan.EdgeStyle.Ortho: + if (orthoDoubleLine) orthoDoubleLine.destroy() if (straightLine) straightLine.destroy() if (curvedLine) curvedLine.destroy() edgeSelectionShape.data = orthoLine = qanEdgeOrthoPathComponent.createObject(edgeSelectionShape, { @@ -167,7 +171,18 @@ Item { strokeColor: lineColor }) break; + case Qan.EdgeStyle.OrthoDouble: + if (orthoLine) orthoLine.destroy() + if (straightLine) straightLine.destroy() + if (curvedLine) curvedLine.destroy() + edgeSelectionShape.data = orthoDoubleLine = qanEdgeOrthoDoublePathComponent.createObject(edgeSelectionShape, { + edgeTemplate: edgeTemplate, + strokeWidth: lineWidth, + strokeColor: lineColor + }) + break; case Qan.EdgeStyle.Curved: + if (orthoDoubleLine) orthoDoubleLine.destroy() if (straightLine) straightLine.destroy() if (orthoLine) orthoLine.destroy() edgeSelectionShape.data = curvedLine = qanEdgeCurvedPathComponent.createObject(edgeSelectionShape, { @@ -189,21 +204,31 @@ Item { property var curvedLine : undefined property var straightLine : undefined property var orthoLine : undefined + property var orthoDoubleLine: undefined property var lineType: edgeTemplate.lineType onLineTypeChanged: { switch (lineType) { case Qan.EdgeStyle.Undefined: // falltrought case Qan.EdgeStyle.Straight: + if (orthoDoubleLine) orthoDoubleLine.destroy() if (orthoLine) orthoLine.destroy() if (curvedLine) curvedLine.destroy() edgeShape.data = straightLine = qanEdgeStraightPathComponent.createObject(edgeShape, {edgeTemplate: edgeTemplate}); break; case Qan.EdgeStyle.Ortho: + if (orthoDoubleLine) orthoDoubleLine.destroy() if (straightLine) straightLine.destroy() if (curvedLine) curvedLine.destroy() edgeShape.data = orthoLine = qanEdgeOrthoPathComponent.createObject(edgeShape, {edgeTemplate: edgeTemplate}) break; + case Qan.EdgeStyle.OrthoDouble: + if (orthoLine) orthoLine.destroy() + if (straightLine) straightLine.destroy() + if (curvedLine) curvedLine.destroy() + edgeShape.data = orthoDoubleLine = qanEdgeOrthoDoublePathComponent.createObject(edgeShape, {edgeTemplate: edgeTemplate}) + break; case Qan.EdgeStyle.Curved: + if (orthoDoubleLine) orthoDoubleLine.destroy() if (straightLine) straightLine.destroy() if (orthoLine) orthoLine.destroy() edgeShape.data = curvedLine = qanEdgeCurvedPathComponent.createObject(edgeShape, {edgeTemplate: edgeTemplate}) diff --git a/src/QuickQanava.h b/src/QuickQanava.h index 7577ead5..c5d30eb2 100755 --- a/src/QuickQanava.h +++ b/src/QuickQanava.h @@ -1,5 +1,6 @@ /* Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2025, Siemens Energy Global GmbH & Co. KG Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -74,17 +75,18 @@ struct QuickQanava { engine->rootContext()->setContextProperty("defaultEdgeStyle", QVariant::fromValue(qan::Edge::style())); engine->rootContext()->setContextProperty("defaultGroupStyle", QVariant::fromValue(qan::Group::style())); - engine->rootContext()->setContextProperty("qanEdgeStraightPathComponent", new QQmlComponent(engine, "qrc:/QuickQanava/EdgeStraightPath.qml", engine)); - engine->rootContext()->setContextProperty("qanEdgeOrthoPathComponent", new QQmlComponent(engine, "qrc:/QuickQanava/EdgeOrthoPath.qml", engine)); - engine->rootContext()->setContextProperty("qanEdgeCurvedPathComponent", new QQmlComponent(engine, "qrc:/QuickQanava/EdgeCurvedPath.qml", engine)); + engine->rootContext()->setContextProperty("qanEdgeStraightPathComponent", new QQmlComponent(engine, "qrc:/qt/qml/QuickQanava/EdgeStraightPath.qml", engine)); + engine->rootContext()->setContextProperty("qanEdgeOrthoPathComponent", new QQmlComponent(engine, "qrc:/qt/qml/QuickQanava/EdgeOrthoPath.qml", engine)); + engine->rootContext()->setContextProperty("qanEdgeOrthoDoublePathComponent", new QQmlComponent(engine, "qrc:/qt/qml/QuickQanava/EdgeOrthoDoublePath.qml", engine)); + engine->rootContext()->setContextProperty("qanEdgeCurvedPathComponent", new QQmlComponent(engine, "qrc:/qt/qml/QuickQanava/EdgeCurvedPath.qml", engine)); - engine->rootContext()->setContextProperty("qanEdgeSrcArrowPathComponent", new QQmlComponent(engine, "qrc:/QuickQanava/EdgeSrcArrowPath.qml", engine)); - engine->rootContext()->setContextProperty("qanEdgeSrcCirclePathComponent", new QQmlComponent(engine, "qrc:/QuickQanava/EdgeSrcCirclePath.qml", engine)); - engine->rootContext()->setContextProperty("qanEdgeSrcRectPathComponent", new QQmlComponent(engine, "qrc:/QuickQanava/EdgeSrcRectPath.qml", engine)); + engine->rootContext()->setContextProperty("qanEdgeSrcArrowPathComponent", new QQmlComponent(engine, "qrc:/qt/qml/QuickQanava/EdgeSrcArrowPath.qml", engine)); + engine->rootContext()->setContextProperty("qanEdgeSrcCirclePathComponent", new QQmlComponent(engine, "qrc:/qt/qml/QuickQanava/EdgeSrcCirclePath.qml", engine)); + engine->rootContext()->setContextProperty("qanEdgeSrcRectPathComponent", new QQmlComponent(engine, "qrc:/qt/qml/QuickQanava/EdgeSrcRectPath.qml", engine)); - engine->rootContext()->setContextProperty("qanEdgeDstArrowPathComponent", new QQmlComponent(engine, "qrc:/QuickQanava/EdgeDstArrowPath.qml", engine)); - engine->rootContext()->setContextProperty("qanEdgeDstCirclePathComponent", new QQmlComponent(engine, "qrc:/QuickQanava/EdgeDstCirclePath.qml", engine)); - engine->rootContext()->setContextProperty("qanEdgeDstRectPathComponent", new QQmlComponent(engine, "qrc:/QuickQanava/EdgeDstRectPath.qml", engine)); + engine->rootContext()->setContextProperty("qanEdgeDstArrowPathComponent", new QQmlComponent(engine, "qrc:/qt/qml/QuickQanava/EdgeDstArrowPath.qml", engine)); + engine->rootContext()->setContextProperty("qanEdgeDstCirclePathComponent", new QQmlComponent(engine, "qrc:/qt/qml/QuickQanava/EdgeDstCirclePath.qml", engine)); + engine->rootContext()->setContextProperty("qanEdgeDstRectPathComponent", new QQmlComponent(engine, "qrc:/qt/qml/QuickQanava/EdgeDstRectPath.qml", engine)); } } // initialize() }; diff --git a/src/RectNodeTemplate.qml b/src/RectNodeTemplate.qml index 636755b8..a0d6b6f4 100644 --- a/src/RectNodeTemplate.qml +++ b/src/RectNodeTemplate.qml @@ -1,5 +1,6 @@ /* Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2025, Siemens Energy Global GmbH & Co. KG Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -67,20 +68,20 @@ Item { anchors.fill: parent source: { if (!nodeItem?.style) // Defaul to solid no effect with unconfigured nodes - return "qrc:/QuickQanava/RectSolidBackground.qml"; + return "qrc:/qt/qml/QuickQanava/RectSolidBackground.qml"; switch (nodeItem.style.fillType) { // Otherwise, select the delegate according to current style configuration case Qan.NodeStyle.FillSolid: switch (nodeItem.style.effectType) { - case Qan.NodeStyle.EffectNone: return "qrc:/QuickQanava/RectSolidBackground.qml"; - case Qan.NodeStyle.EffectShadow: return "qrc:/QuickQanava/RectSolidShadowBackground.qml"; - case Qan.NodeStyle.EffectGlow: return "qrc:/QuickQanava/RectSolidGlowBackground.qml"; + case Qan.NodeStyle.EffectNone: return "qrc:/qt/qml/QuickQanava/RectSolidBackground.qml"; + case Qan.NodeStyle.EffectShadow: return "qrc:/qt/qml/QuickQanava/RectSolidShadowBackground.qml"; + case Qan.NodeStyle.EffectGlow: return "qrc:/qt/qml/QuickQanava/RectSolidGlowBackground.qml"; } break; case Qan.NodeStyle.FillGradient: switch (nodeItem.style.effectType) { - case Qan.NodeStyle.EffectNone: return "qrc:/QuickQanava/RectGradientBackground.qml"; - case Qan.NodeStyle.EffectShadow: return "qrc:/QuickQanava/RectGradientShadowBackground.qml"; - case Qan.NodeStyle.EffectGlow: return "qrc:/QuickQanava/RectGradientGlowBackground.qml"; + case Qan.NodeStyle.EffectNone: return "qrc:/qt/qml/QuickQanava/RectGradientBackground.qml"; + case Qan.NodeStyle.EffectShadow: return "qrc:/qt/qml/QuickQanava/RectGradientShadowBackground.qml"; + case Qan.NodeStyle.EffectGlow: return "qrc:/qt/qml/QuickQanava/RectGradientGlowBackground.qml"; } break; } // case fillType diff --git a/src/qanConnector.cpp b/src/qanConnector.cpp index 2e71c220..63836c3a 100644 --- a/src/qanConnector.cpp +++ b/src/qanConnector.cpp @@ -1,5 +1,6 @@ /* Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2025, Siemens Energy Global GmbH & Co. KG Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -75,7 +76,7 @@ QQmlComponent* Connector::delegate(QQmlEngine& engine, QObject* parent) noexcep { static std::unique_ptr delegate; if (!delegate) - delegate = std::make_unique(&engine, "qrc:/QuickQanava/VisualConnector.qml", + delegate = std::make_unique(&engine, "qrc:/qt/qml/QuickQanava/VisualConnector.qml", QQmlComponent::PreferSynchronous, parent); return delegate.get(); } diff --git a/src/qanEdge.cpp b/src/qanEdge.cpp index 8e130b97..df9c78d1 100644 --- a/src/qanEdge.cpp +++ b/src/qanEdge.cpp @@ -1,5 +1,6 @@ /* Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2025, Siemens Energy Global GmbH & Co. KG Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -75,7 +76,7 @@ QQmlComponent* Edge::delegate(QQmlEngine& engine, QObject* parent) noexcept { static std::unique_ptr delegate; if (!delegate) - delegate = std::make_unique(&engine, "qrc:/QuickQanava/Edge.qml", + delegate = std::make_unique(&engine, "qrc:/qt/qml/QuickQanava/Edge.qml", QQmlComponent::PreferSynchronous, parent); return delegate.get(); } diff --git a/src/qanEdgeItem.cpp b/src/qanEdgeItem.cpp index 4a6eb8c1..9bcfc41a 100644 --- a/src/qanEdgeItem.cpp +++ b/src/qanEdgeItem.cpp @@ -1,5 +1,6 @@ /* Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2025, Siemens Energy Global GmbH & Co. KG Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -255,6 +256,7 @@ void EdgeItem::updateItem() noexcept case qan::EdgeStyle::LineType::Straight: generateStraightEnds(cache); break; case qan::EdgeStyle::LineType::Curved: generateStraightEnds(cache); break; case qan::EdgeStyle::LineType::Ortho: generateOrthoEnds(cache); break; + case qan::EdgeStyle::LineType::OrthoDouble: generateOrthoDoubleEnds(cache); break; } if (cache.isValid()) { switch (cache.lineType) { // 3. @@ -262,6 +264,7 @@ void EdgeItem::updateItem() noexcept case qan::EdgeStyle::LineType::Straight: /* Nil */ break; case qan::EdgeStyle::LineType::Curved: generateCurvedControlPoints(cache); break; case qan::EdgeStyle::LineType::Ortho: /* Nil */ break; // Ortho C1 control point is generated in generateOrthoEnds() + case qan::EdgeStyle::LineType::OrthoDouble: /* Nil */ break; // Ortho C1, C2 control point are generated in generateOrthoEndsRounded() } generateArrowGeometry(cache); generateLabelPosition(cache); @@ -613,6 +616,54 @@ void EdgeItem::generateOrthoEnds(GeometryCache& cache) const noexcept } } +void EdgeItem::generateOrthoDoubleEnds(GeometryCache &cache) const noexcept +{ + // PRECONDITIONS: + // cache should be valid + // cache srcBs and dstBs must not be empty (valid bounding shapes are necessary) + if ( !cache.isValid() ) + return; + + const bool top = cache.dstBrCenter.y() < cache.srcBr.y(); + const bool right = cache.dstBrCenter.x() > cache.srcBr.x(); + + const auto margin = getStyle()->getOrthoRadius() * 2; + + // Promotes horizontal layout, only vertical if dst is under/above + margin + const bool vert = cache.dstBr.left() < cache.srcBr.right() + margin && cache.dstBr.right() + margin > cache.srcBr.left(); + + cache.valid = !cache.srcBr.intersects(cache.dstBr.adjusted(-margin, -margin, margin, margin)); + + if (vert) { + if (top) { + // Top vertical + cache.p1 = QPointF{cache.srcBrCenter.x(), cache.srcBr.top()}; + cache.p2 = QPointF{cache.dstBrCenter.x(), cache.dstBr.bottom()}; + } else { + // Bottom vertical + cache.p1 = QPointF{cache.srcBrCenter.x(), cache.srcBr.bottom()}; + cache.p2 = QPointF{cache.dstBrCenter.x(), cache.dstBr.top()}; + } + const auto center = QLineF{cache.p1, cache.p2}.center(); + cache.c1 = QPointF{cache.p1.x(), center.y()}; + cache.c2 = QPointF{cache.p2.x(), center.y()}; + } else { + if (right) { + // Right horizontal + cache.p1 = QPointF{cache.srcBr.right(), cache.srcBrCenter.y()}; + cache.p2 = QPointF{cache.dstBr.left(), cache.dstBrCenter.y()}; + } + else { + // Left horizontal + cache.p1 = QPointF{cache.srcBr.left(), cache.srcBrCenter.y()}; + cache.p2 = QPointF{cache.dstBr.right(), cache.dstBrCenter.y()}; + } + const auto center = QLineF{cache.p1, cache.p2}.center(); + cache.c1 = QPointF{center.x(), cache.p1.y()}; + cache.c2 = QPointF{center.x(), cache.p2.y()}; + } +} + void EdgeItem::generateArrowGeometry(GeometryCache& cache) const noexcept { // PRECONDITIONS: @@ -688,6 +739,11 @@ void EdgeItem::generateArrowGeometry(GeometryCache& cache) const noexcept cache.srcAngle = generateStraightArrowAngle(cache.c1, cache.p1, srcShape, arrowLength); break; + case qan::EdgeStyle::LineType::OrthoDouble: + cache.dstAngle = generateStraightArrowAngle(cache.c2, cache.p2, dstShape, arrowLength); + cache.srcAngle = generateStraightArrowAngle(cache.c1, cache.p1, srcShape, arrowLength); + break; + case qan::EdgeStyle::LineType::Curved: // Generate source arrow angle (p2 <-> p1 and c2 <-> c1) cache.srcAngle = generateCurvedArrowAngle(cache.p2, cache.p1, @@ -993,7 +1049,13 @@ void EdgeItem::applyGeometry(const GeometryCache& cache) noexcept if (cache.lineType == qan::EdgeStyle::LineType::Ortho) { _c1 = mapFromItem(graphContainerItem, cache.c1); emit controlPointsChanged(); - } else if (cache.lineType == qan::EdgeStyle::LineType::Curved) { // Apply control point geometry + } + else if (cache.lineType == qan::EdgeStyle::LineType::OrthoDouble) { + _c1 = mapFromItem(graphContainerItem, cache.c1); + _c2 = mapFromItem(graphContainerItem, cache.c2); + emit controlPointsChanged(); + } + else if (cache.lineType == qan::EdgeStyle::LineType::Curved) { // Apply control point geometry _c1 = mapFromItem(graphContainerItem, cache.c1); _c2 = mapFromItem(graphContainerItem, cache.c2); emit controlPointsChanged(); @@ -1225,6 +1287,7 @@ void EdgeItem::setStyle(EdgeStyle* style) noexcept this, &EdgeItem::styleModified); } emit styleChanged(); + styleModified(); updateItem(); // Force initial style settings } } @@ -1328,7 +1391,19 @@ bool EdgeItem::contains(const QPointF& point) const r = (d2 > -0.001 && d2 < 6.001); } break; + case qan::EdgeStyle::LineType::OrthoDouble: + const std::array segments{QLineF{_p1, _c1}, QLineF{_c1, _c2}, QLineF{_c2, _p2}}; + + for (const auto &seg : segments) { + d = distanceFromLine(point, seg); + r = (d > -0.001 && d < 6.001); + if (r) { + break; + } + } + break; } + return r; } diff --git a/src/qanEdgeItem.h b/src/qanEdgeItem.h index 0dc4618f..2629e00d 100644 --- a/src/qanEdgeItem.h +++ b/src/qanEdgeItem.h @@ -1,5 +1,6 @@ /* Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2025, Siemens Energy Global GmbH & Co. KG Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -227,6 +228,9 @@ public slots: //! \brief Generate P1 and P2 for ortho edge style. void generateOrthoEnds(GeometryCache& cache) const noexcept; + //! \brief Generate P1 and P2 for ortho rounded edge style. + void generateOrthoDoubleEnds(GeometryCache& cache) const noexcept; + /*! \brief FIXME * * \note Line geometry may (cache.p1 and cache.p2) could be modified to fit arrow geometry. diff --git a/src/qanGraph.cpp b/src/qanGraph.cpp index 43d4a5c0..3729bde4 100644 --- a/src/qanGraph.cpp +++ b/src/qanGraph.cpp @@ -1,5 +1,6 @@ /* Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2025, Siemens Energy Global GmbH & Co. KG Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -74,14 +75,14 @@ Graph::~Graph() void Graph::classBegin() { - setPortDelegate(createComponent(QStringLiteral("qrc:/QuickQanava/Port.qml"))); - setHorizontalDockDelegate(createComponent(QStringLiteral("qrc:/QuickQanava/HorizontalDock.qml"))); - setVerticalDockDelegate(createComponent(QStringLiteral("qrc:/QuickQanava/VerticalDock.qml"))); - setGroupDelegate(createComponent(QStringLiteral("qrc:/QuickQanava/Group.qml"))); + setPortDelegate(createComponent(QStringLiteral("qrc:/qt/qml/QuickQanava/Port.qml"))); + setHorizontalDockDelegate(createComponent(QStringLiteral("qrc:/qt/qml/QuickQanava/HorizontalDock.qml"))); + setVerticalDockDelegate(createComponent(QStringLiteral("qrc:/qt/qml/QuickQanava/VerticalDock.qml"))); + setGroupDelegate(createComponent(QStringLiteral("qrc:/qt/qml/QuickQanava/Group.qml"))); // Note: Do not set a default node delegate, otherwise it would be used instead // of qan::Node::delegate(), just let the user specify one. - setEdgeDelegate(createComponent(QStringLiteral("qrc:/QuickQanava/Edge.qml"))); - setSelectionDelegate(createComponent(QStringLiteral("qrc:/QuickQanava/SelectionItem.qml"))); + setEdgeDelegate(createComponent(QStringLiteral("qrc:/qt/qml/QuickQanava/Edge.qml"))); + setSelectionDelegate(createComponent(QStringLiteral("qrc:/qt/qml/QuickQanava/SelectionItem.qml"))); const auto engine = qmlEngine(this); if (engine != nullptr) { @@ -97,7 +98,7 @@ void Graph::componentComplete() const auto engine = qmlEngine(this); if (engine != nullptr) { // Visual connector initialization - auto connectorComponent = std::make_unique(engine, QStringLiteral("qrc:/QuickQanava/VisualConnector.qml")); + auto connectorComponent = std::make_unique(engine, QStringLiteral("qrc:/qt/qml/QuickQanava/VisualConnector.qml")); if (connectorComponent) { qan::Style* style = qan::Connector::style(nullptr); if (style != nullptr) { @@ -495,7 +496,7 @@ void Graph::setSelectionDelegate(std::unique_ptr selectionDelegat delegateChanged = true; } } else { // Use QuickQanava default selection delegate - _selectionDelegate = createComponent(QStringLiteral("qrc:/QuickQanava/SelectionItem.qml")); + _selectionDelegate = createComponent(QStringLiteral("qrc:/qt/qml/QuickQanava/SelectionItem.qml")); delegateChanged = true; } if (delegateChanged) { // Update all existing delegates... @@ -1076,6 +1077,7 @@ bool qan::Graph::groupNode(qan::Group* group, qan::Node* node, qan::TableCell group->getGroupItem() != nullptr && node->getItem() != nullptr ) { group->getGroupItem()->groupNodeItem(node->getItem(), groupCell, transform); + emit node->groupChanged(); emit nodeGrouped(node, group); } return true; diff --git a/src/qanGroup.cpp b/src/qanGroup.cpp index fb5f3bb0..08a6cd70 100644 --- a/src/qanGroup.cpp +++ b/src/qanGroup.cpp @@ -1,5 +1,6 @@ /* Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2025, Siemens Energy Global GmbH & Co. KG Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -111,7 +112,7 @@ QQmlComponent* Group::delegate(QQmlEngine& engine, QObject* parent) noexcept { static std::unique_ptr delegate; if (!delegate) - delegate = std::make_unique(&engine, "qrc:/QuickQanava/Group.qml", + delegate = std::make_unique(&engine, "qrc:/qt/qml/QuickQanava/Group.qml", QQmlComponent::PreferSynchronous, parent); return delegate.get(); } diff --git a/src/qanNode.cpp b/src/qanNode.cpp index 4f0681cb..6061a2be 100644 --- a/src/qanNode.cpp +++ b/src/qanNode.cpp @@ -1,5 +1,6 @@ /* Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2025, Siemens Energy Global GmbH & Co. KG Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -95,7 +96,7 @@ QQmlComponent* Node::delegate(QQmlEngine& engine, QObject* parent) noexcept Q_UNUSED(parent) static std::unique_ptr delegate; if (!delegate) - delegate = std::make_unique(&engine, "qrc:/QuickQanava/Node.qml", + delegate = std::make_unique(&engine, "qrc:/qt/qml/QuickQanava/Node.qml", QQmlComponent::PreferSynchronous); return delegate.get(); } diff --git a/src/qanNode.h b/src/qanNode.h index c0c0e5c2..a3daecb5 100644 --- a/src/qanNode.h +++ b/src/qanNode.h @@ -1,5 +1,6 @@ /* Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2025, Siemens Energy Global GmbH & Co. KG Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -218,7 +219,7 @@ class Node : public gtpo::node _cell; signals: + void groupChanged(); void cellChanged(); //@} //------------------------------------------------------------------------- diff --git a/src/qanStyle.cpp b/src/qanStyle.cpp index a611bb89..1f56c608 100644 --- a/src/qanStyle.cpp +++ b/src/qanStyle.cpp @@ -1,5 +1,6 @@ /* Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2025, Siemens Energy Global GmbH & Co. KG Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -265,6 +266,17 @@ bool EdgeStyle::setArrowSize(qreal arrowSize) noexcept return false; } +auto EdgeStyle::setOrthoRadius(qreal orthoRadius) noexcept -> bool +{ + if (_orthoRadius != orthoRadius) { + _orthoRadius = orthoRadius; + emit orthoRadiusChanged(); + emit styleModified(); + return true; + } + return false; +} + auto EdgeStyle::setSrcShape(ArrowShape srcShape) noexcept -> bool { if (_srcShape != srcShape) { diff --git a/src/qanStyle.h b/src/qanStyle.h index ccb4db57..884c8d7d 100644 --- a/src/qanStyle.h +++ b/src/qanStyle.h @@ -1,5 +1,6 @@ /* Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2025, Siemens Energy Global GmbH & Co. KG Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -375,7 +376,8 @@ class EdgeStyle : public qan::Style Undefined = 0, Straight = 1, Curved = 2, - Ortho = 3 + Ortho = 3, + OrthoDouble = 4 }; Q_ENUM(LineType) @@ -416,6 +418,15 @@ class EdgeStyle : public qan::Style signals: void arrowSizeChanged(); +public: + Q_PROPERTY(qreal orthoRadius READ getOrthoRadius WRITE setOrthoRadius NOTIFY orthoRadiusChanged FINAL) + bool setOrthoRadius(qreal orthoRadius) noexcept; + inline qreal getOrthoRadius() const noexcept { return _orthoRadius; } +protected: + qreal _orthoRadius = 10.0; +signals: + void orthoRadiusChanged(); + public: //! \copydoc Define shape of source arrow, default None. diff --git a/src/qanTableGroup.cpp b/src/qanTableGroup.cpp index 3b6555dd..aa16dea0 100644 --- a/src/qanTableGroup.cpp +++ b/src/qanTableGroup.cpp @@ -1,5 +1,6 @@ /* Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2025, Siemens Energy Global GmbH & Co. KG Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -66,7 +67,7 @@ QQmlComponent* TableGroup::delegate(QQmlEngine& engine, QObject* parent) noexce { static std::unique_ptr delegate; if (!delegate) - delegate = std::make_unique(&engine, "qrc:/QuickQanava/TableGroup.qml", + delegate = std::make_unique(&engine, "qrc:/qt/qml/QuickQanava/TableGroup.qml", QQmlComponent::PreferSynchronous, parent); return delegate.get(); } diff --git a/src/qanTableGroupItem.cpp b/src/qanTableGroupItem.cpp index f4bfb188..21b6e41e 100644 --- a/src/qanTableGroupItem.cpp +++ b/src/qanTableGroupItem.cpp @@ -1,5 +1,6 @@ /* Copyright (c) 2008-2024, Benoit AUTHEMAN All rights reserved. + Copyright (c) 2025, Siemens Energy Global GmbH & Co. KG Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -249,7 +250,7 @@ qan::TableBorder* TableGroupItem::createBorder() return nullptr; } // Component is parented to graph, will be destroyed when graph is destroyed - TableGroupItem::_borderComponent = new QQmlComponent(engine, "qrc:/QuickQanava/TableBorder.qml", + TableGroupItem::_borderComponent = new QQmlComponent(engine, "qrc:/qt/qml/QuickQanava/TableBorder.qml", QQmlComponent::PreferSynchronous, getGraph()); } if (!TableGroupItem::_borderComponent)