diff options
Diffstat (limited to 'src/plugins/qmldesigner/components/formeditor/formeditoritem.cpp')
-rw-r--r-- | src/plugins/qmldesigner/components/formeditor/formeditoritem.cpp | 1342 |
1 files changed, 1014 insertions, 328 deletions
diff --git a/src/plugins/qmldesigner/components/formeditor/formeditoritem.cpp b/src/plugins/qmldesigner/components/formeditor/formeditoritem.cpp index 96a4cc5adf..6a4acacb97 100644 --- a/src/plugins/qmldesigner/components/formeditor/formeditoritem.cpp +++ b/src/plugins/qmldesigner/components/formeditor/formeditoritem.cpp @@ -1,4 +1,4 @@ -/**************************************************************************** +/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ @@ -26,16 +26,22 @@ #include "formeditoritem.h" #include "formeditorscene.h" +#include <variantproperty.h> #include <bindingproperty.h> #include <modelnode.h> #include <nodehints.h> #include <nodemetainfo.h> +#include <theme.h> + +#include <utils/algorithm.h> #include <utils/theme/theme.h> #include <utils/qtcassert.h> #include <QDebug> +#include <QFontDatabase> +#include <QtGlobal> #include <QPainter> #include <QPainterPath> #include <QStyleOptionGraphicsItem> @@ -47,6 +53,37 @@ namespace QmlDesigner { const int flowBlockSize = 200; +const int blockRadius = 18; +const int blockAdjust = 40; +const int startItemOffset = 96; +const int labelFontSize = 16; + +void drawIcon(QPainter *painter, + int x, + int y, + const QString &iconSymbol, + int fontSize, + int iconSize, + const QColor &penColor) +{ + static QFontDatabase a; + + const QString fontName = "qtds_propertyIconFont.ttf"; + + Q_ASSERT(a.hasFamily(fontName)); + + if (a.hasFamily(fontName)) { + QFont font(fontName); + font.setPixelSize(fontSize); + + painter->save(); + painter->setPen(penColor); + painter->setFont(font); + painter->drawText(QRectF(x, y, iconSize, iconSize), iconSymbol); + + painter->restore(); + } +} FormEditorScene *FormEditorItem::scene() const { return qobject_cast<FormEditorScene*>(QGraphicsItem::scene()); @@ -92,7 +129,7 @@ void FormEditorItem::setup() QRectF FormEditorItem::boundingRect() const { - return m_boundingRect.adjusted(-2, -2, 2, 2); + return m_boundingRect; } QPainterPath FormEditorItem::shape() const @@ -113,9 +150,9 @@ void FormEditorItem::updateGeometry() prepareGeometryChange(); m_selectionBoundingRect = qmlItemNode().instanceBoundingRect().adjusted(0, 0, 1., 1.); m_paintedBoundingRect = qmlItemNode().instancePaintedBoundingRect(); - m_boundingRect = m_paintedBoundingRect.united(m_selectionBoundingRect); + m_boundingRect = qmlItemNode().instanceBoundingRect(); setTransform(qmlItemNode().instanceTransformWithContentTransform()); - //the property for zValue is called z in QGraphicsObject + // the property for zValue is called z in QGraphicsObject if (qmlItemNode().instanceValue("z").isValid() && !qmlItemNode().isRootModelNode()) setZValue(qmlItemNode().instanceValue("z").toDouble()); } @@ -362,7 +399,7 @@ QList<FormEditorItem *> FormEditorItem::offspringFormEditorItemsRecursive(const { QList<FormEditorItem*> formEditorItemList; - foreach (QGraphicsItem *item, formEditorItem->childItems()) { + for (QGraphicsItem *item : formEditorItem->childItems()) { FormEditorItem *formEditorItem = fromQGraphicsItem(item); if (formEditorItem) { formEditorItemList.append(formEditorItem); @@ -419,6 +456,7 @@ void FormEditorItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, } } + painter->setClipping(false); if (!qmlItemNode().isRootModelNode()) paintBoundingRect(painter); @@ -491,7 +529,7 @@ QList<FormEditorItem*> FormEditorItem::childFormEditorItems() const { QList<FormEditorItem*> formEditorItemList; - foreach (QGraphicsItem *item, childItems()) { + for (QGraphicsItem *item : childItems()) { FormEditorItem *formEditorItem = fromQGraphicsItem(item); if (formEditorItem) formEditorItemList.append(formEditorItem); @@ -529,15 +567,17 @@ void FormEditorFlowItem::setDataModelPosition(const QPointF &position) { qmlItemNode().setFlowItemPosition(position); updateGeometry(); + +/* TODO for (QGraphicsItem *item : scene()->items()) { if (item == this) continue; - auto formEditorItem = qgraphicsitem_cast<FormEditorItem*>(item); + auto formEditorItem = qgraphicsitem_cast<FormEditorItem *>(item); if (formEditorItem) formEditorItem->updateGeometry(); } - +*/ } void FormEditorFlowItem::setDataModelPositionInBaseState(const QPointF &position) @@ -551,17 +591,15 @@ void FormEditorFlowItem::updateGeometry() const QPointF pos = qmlItemNode().flowPosition(); setTransform(QTransform::fromTranslate(pos.x(), pos.y())); - QmlFlowItemNode flowItem(qmlItemNode()); - + // Call updateGeometry() on all related transitions + QmlFlowTargetNode flowItem(qmlItemNode()); if (flowItem.isValid() && flowItem.flowView().isValid()) { const auto nodes = flowItem.flowView().transitions(); for (const ModelNode &node : nodes) { - FormEditorItem *item = scene()->itemForQmlItemNode(node); - if (item) + if (FormEditorItem *item = scene()->itemForQmlItemNode(node)) item->updateGeometry(); } } - } QPointF FormEditorFlowItem::instancePosition() const @@ -569,6 +607,47 @@ QPointF FormEditorFlowItem::instancePosition() const return qmlItemNode().flowPosition(); } + +void FormEditorFlowActionItem::setDataModelPosition(const QPointF &position) +{ + qmlItemNode().setPosition(position); + updateGeometry(); + +/* TODO + for (QGraphicsItem *item : scene()->items()) { + if (item == this) + continue; + + auto formEditorItem = qgraphicsitem_cast<FormEditorItem *>(item); + if (formEditorItem) + formEditorItem->updateGeometry(); + } +*/ +} + +void FormEditorFlowActionItem::setDataModelPositionInBaseState(const QPointF &position) +{ + qmlItemNode().setPostionInBaseState(position); + updateGeometry(); +} + +void FormEditorFlowActionItem::updateGeometry() +{ + FormEditorItem::updateGeometry(); + //const QPointF pos = qmlItemNode().flowPosition(); + //setTransform(QTransform::fromTranslate(pos.x(), pos.y())); + + // Call updateGeometry() on all related transitions + QmlFlowItemNode flowItem = QmlFlowActionAreaNode(qmlItemNode()).flowItemParent(); + if (flowItem.isValid() && flowItem.flowView().isValid()) { + const auto nodes = flowItem.flowView().transitions(); + for (const ModelNode &node : nodes) { + if (FormEditorItem *item = scene()->itemForQmlItemNode(node)) + item->updateGeometry(); + } + } +} + void FormEditorFlowActionItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { if (!painter->isActive()) @@ -578,6 +657,7 @@ void FormEditorFlowActionItem::paint(QPainter *painter, const QStyleOptionGraphi return; painter->save(); + painter->setRenderHint(QPainter::Antialiasing); QPen pen; pen.setJoinStyle(Qt::MiterJoin); @@ -591,7 +671,6 @@ void FormEditorFlowActionItem::paint(QPainter *painter, const QStyleOptionGraphi if (qmlItemNode().modelNode().hasAuxiliaryData("color")) flowColor = qmlItemNode().modelNode().auxiliaryData("color").value<QColor>(); - const qreal scaleFactor = viewportTransform().m11(); qreal width = 2; if (qmlItemNode().modelNode().hasAuxiliaryData("width")) @@ -622,10 +701,9 @@ void FormEditorFlowActionItem::paint(QPainter *painter, const QStyleOptionGraphi fillColor = qmlItemNode().modelNode().auxiliaryData("fillColor").value<QColor>(); if (fillColor.alpha() > 0) - painter->fillRect(boundingRect(), fillColor); - - painter->drawRect(boundingRect()); + painter->setBrush(fillColor); + painter->drawRoundedRect(boundingRect(), blockRadius, blockRadius); painter->restore(); } @@ -655,443 +733,898 @@ void FormEditorTransitionItem::setDataModelPositionInBaseState(const QPointF &) } +static bool isValid(const QList<QmlItemNode> &list) +{ + for (const auto &item : list) + if (!item.isValid()) + return false; + + return !list.isEmpty(); +} + +static bool isModelNodeValid(const QList<QmlItemNode> &list) +{ + for (const auto &item : list) + if (!item.modelNode().isValid()) + return false; + + return !list.isEmpty(); +} + class ResolveConnection { public: - ResolveConnection(const QmlItemNode &node) : - from({}) - ,to(node.modelNode().bindingProperty("to").resolveToModelNode()) - ,areaNode(ModelNode()) + ResolveConnection(const QmlItemNode &node) + : from() + , to() + , areaNode(ModelNode()) { - if (node.modelNode().hasBindingProperty("from")) - from = node.modelNode().bindingProperty("from").resolveToModelNode(); - const QmlFlowItemNode to = node.modelNode().bindingProperty("to").resolveToModelNode(); - - if (from.isValid()) { - for (const QmlFlowActionAreaNode &area : from.flowActionAreas()) { - ModelNode target = area.targetTransition(); - if (target == node.modelNode()) { - areaNode = area; - } else { - const ModelNode decisionNode = area.decisionNodeForTransition(node.modelNode()); - if (decisionNode.isValid()) { - from = decisionNode; - areaNode = ModelNode(); - } + if (node.modelNode().hasBindingProperty("from")) { + if (node.modelNode().bindingProperty("from").isList()) + from = Utils::transform<QList>(node.modelNode().bindingProperty("from").resolveToModelNodeList(), + [](const ModelNode &node) { + return QmlItemNode(node); + }); + else + from = QList<QmlItemNode>({node.modelNode().bindingProperty("from").resolveToModelNode()}); + } + + if (node.modelNode().hasBindingProperty("to")) { + if (node.modelNode().bindingProperty("to").isList()) + to = Utils::transform<QList>(node.modelNode().bindingProperty("to").resolveToModelNodeList(), + [](const ModelNode &node) { + return QmlItemNode(node); + }); + else + to = QList<QmlItemNode>({node.modelNode().bindingProperty("to").resolveToModelNode()}); + } + + if (from.empty()) { + for (const ModelNode &wildcard : QmlFlowViewNode(node.rootModelNode()).wildcards()) { + if (wildcard.bindingProperty("target").resolveToModelNode() == node.modelNode()) { + from.clear(); + from.append(wildcard); + isWildcardLine = true; } } - if (from.modelNode().hasAuxiliaryData("joinConnection")) - joinConnection = from.modelNode().auxiliaryData("joinConnection").toBool(); - } else { - if (from == node.rootModelNode()) { - isStartLine = true; - } else { - for (const ModelNode wildcard : QmlFlowViewNode(node.rootModelNode()).wildcards()) { - if (wildcard.bindingProperty("target").resolveToModelNode() == node.modelNode()) { - from = wildcard; - isWildcardLine = true; - } + } + + // Only assign area node if there is exactly one from (QmlFlowItemNode) + if (from.size() == 1) { + const QmlItemNode tmp = from.back(); + const QmlFlowItemNode f(tmp.modelNode()); + + if (f.isValid()) { + for (const QmlFlowActionAreaNode &area : f.flowActionAreas()) { + ModelNode target = area.targetTransition(); + if (target == node.modelNode()) + areaNode = area; } + + const ModelNode decisionNode = QmlFlowItemNode::decisionNodeForTransition(node.modelNode()); + if (decisionNode.isValid()) { + from.clear(); + from.append(decisionNode); + areaNode = ModelNode(); + } + + if (f.modelNode().hasAuxiliaryData("joinConnection")) + joinConnection = f.modelNode().auxiliaryData("joinConnection").toBool(); + } else { + if (f == node.rootModelNode()) + isStartLine = true; } } } bool joinConnection = false; - bool isStartLine = false; - bool isWildcardLine = false; - QmlFlowItemNode from; - QmlFlowItemNode to; + QList<QmlItemNode> from; + QList<QmlItemNode> to; QmlFlowActionAreaNode areaNode; }; -void FormEditorTransitionItem::updateGeometry() +enum ConnectionType { - FormEditorItem::updateGeometry(); - - ResolveConnection resolved(qmlItemNode()); - - QPointF fromP = QmlItemNode(resolved.from).flowPosition(); - QRectF sizeTo = resolved.to.instanceBoundingRect(); - - QPointF toP = QmlItemNode(resolved.to).flowPosition(); - - if (QmlItemNode(resolved.to).isFlowDecision()) - sizeTo = QRectF(0, 0, flowBlockSize, flowBlockSize); - - qreal x1 = fromP.x(); - qreal x2 = toP.x(); - - if (x2 < x1) { - qreal s = x1; - x1 = x2; - x2 = s; - } - - qreal y1 = fromP.y(); - qreal y2 = toP.y(); + Default = 0, + Bezier +}; - if (y2 < y1) { - qreal s = y1; - y1 = y2; - y2 = s; +class ConnectionConfiguration +{ +public: + ConnectionConfiguration(const QmlItemNode &node, + const ResolveConnection &resolveConnection, + const qreal scaleFactor, + bool hitTest = false) + : width(2) + , adjustedWidth(width / scaleFactor) + , color(QColor("#e71919")) + , lineBrush(QBrush(color)) + , penStyle(Qt::SolidLine) + , dashPattern() + , drawStart(true) + , drawEnd(true) + , joinEnd(false) + , outOffset(0) + , inOffset(0) + , breakOffset(50) + , radius(8) + , bezier(50) + , type(ConnectionType::Default) + , label() + , fontSize(labelFontSize / scaleFactor) + , labelOffset(14 / scaleFactor) + , labelPosition(50.0) + , labelFlags(Qt::AlignHCenter | Qt::AlignVCenter | Qt::TextDontClip) + , labelFlipSide(false) + , hitTesting(hitTest) + { + // width + if (node.modelNode().hasAuxiliaryData("width")) + width = node.modelNode().auxiliaryData("width").toInt(); + // adjusted width + if (node.modelNode().isSelected()) + width += 2; + if (hitTest) + width = width * 8 / scaleFactor; + // color + if (resolveConnection.isStartLine) + color = QColor("blue"); + if (resolveConnection.isWildcardLine) + color = QColor("green"); + if (node.rootModelNode().hasAuxiliaryData("transitionColor")) + color = node.rootModelNode().auxiliaryData("transitionColor").value<QColor>(); + if (node.modelNode().hasAuxiliaryData("color")) + color = node.modelNode().auxiliaryData("color").value<QColor>(); + // linbe brush + lineBrush = QBrush(color); + + // pen style + + // dash + if (node.modelNode().hasAuxiliaryData("dash") && node.modelNode().auxiliaryData("dash").toBool()) + penStyle = Qt::DashLine; + // in/out offset + if (node.modelNode().hasAuxiliaryData("outOffset")) + outOffset = node.modelNode().auxiliaryData("outOffset").toInt(); + if (node.modelNode().hasAuxiliaryData("inOffset")) + inOffset = node.modelNode().auxiliaryData("inOffset").toInt(); + // break offset + if (node.modelNode().hasAuxiliaryData("breakPoint")) + breakOffset = node.modelNode().auxiliaryData("breakPoint").toInt(); + // radius + if (node.rootModelNode().hasAuxiliaryData("transitionRadius")) + radius = node.rootModelNode().auxiliaryData("transitionRadius").toInt(); + if (node.modelNode().hasAuxiliaryData("radius")) + radius = node.modelNode().auxiliaryData("radius").toInt(); + // bezier + if (node.rootModelNode().hasAuxiliaryData("transitionBezier")) + bezier = node.rootModelNode().auxiliaryData("transitionBezier").toInt(); + if (node.modelNode().hasAuxiliaryData("bezier")) + bezier = node.modelNode().auxiliaryData("bezier").toInt(); + // type + if (node.rootModelNode().hasAuxiliaryData("transitionType")) + type = static_cast<ConnectionType>(node.rootModelNode().auxiliaryData("transitionType").toInt()); + if (node.modelNode().hasAuxiliaryData("type")) + type = static_cast<ConnectionType>(node.modelNode().auxiliaryData("type").toInt()); + // label + if (node.modelNode().hasBindingProperty("condition")) + label = node.modelNode().bindingProperty("condition").expression(); + if (node.modelNode().hasVariantProperty("question")) + label = node.modelNode().variantProperty("question").value().toString(); + // font size + + // label offset + + // label position + if (node.modelNode().hasAuxiliaryData("labelPosition")) + labelPosition = node.modelNode().auxiliaryData("labelPosition").toReal(); + // label flip side + if (node.modelNode().hasAuxiliaryData("labelFlipSide")) + labelFlipSide = node.modelNode().auxiliaryData("labelFlipSide").toBool(); } - x2 += sizeTo.width(); - y2 += sizeTo.height(); - - setX(x1); - setY(y1); - m_selectionBoundingRect = QRectF(0,0,x2-x1,y2-y1); - m_paintedBoundingRect = m_selectionBoundingRect; - m_boundingRect = m_selectionBoundingRect; - setZValue(10); -} - -QPointF FormEditorTransitionItem::instancePosition() const -{ - return qmlItemNode().flowPosition(); -} + qreal width; + qreal adjustedWidth; + QColor color; // TODO private/setter + QBrush lineBrush; + Qt::PenStyle penStyle; + QVector<qreal> dashPattern; + bool drawStart; + bool drawEnd; + // Dirty hack for joining/merging arrow heads on many-to-one transitions + bool joinEnd; + int outOffset; + int inOffset; + int breakOffset; + int radius; + int bezier; + ConnectionType type; + QString label; + int fontSize; + qreal labelOffset; + qreal labelPosition; + int labelFlags; + bool labelFlipSide; + bool hitTesting; +}; static bool verticalOverlap(const QRectF &from, const QRectF &to) { - if (from.top() < to.bottom() && (from.top() + from.height()) > to.top()) + if (from.top() < to.bottom() && from.bottom() > to.top()) return true; - if (to.top() < from.bottom() && (to.top() + to.height()) > from.top()) + if (to.top() < from.bottom() && to.bottom() > from.top()) return true; return false; } - static bool horizontalOverlap(const QRectF &from, const QRectF &to) { - if (from.left() < to.right() && (from.left() + from.width()) > to.left()) + if (from.left() < to.right() && from.right() > to.left()) return true; - if (to.left() < from.right() && (to.left() + to.width()) > from.left()) + if (to.left() < from.right() && to.right() > from.left()) return true; return false; } -static void paintConnection(QPainter *painter, - const QRectF &from, - const QRectF &to, - qreal width, - qreal adjustedWidth, - const QColor &color, - bool dash, - int startOffset, - int endOffset, - int breakOffset) +static QPainterPath roundedCorner(const QPointF &s, + const QPointF &m, + const QPointF &e, + int radius) +{ + const QVector2D sm(m - s); + const QVector2D me(e - m); + const float smLength = sm.length(); + const float meLength = me.length(); + const int actualRadius = qMin(radius, static_cast<int>(qMin(smLength, meLength))); + const QVector2D smNorm = sm.normalized(); + const QVector2D meNorm = me.normalized(); + QRectF rect(m, QSizeF(actualRadius * 2, actualRadius * 2)); + + QPainterPath path(s); + + if (smNorm.y() < 0 && meNorm.x() > 0) { + rect.moveTopLeft(m); + path.arcTo(rect, 180, -90); + } else if (smNorm.x() < 0 && meNorm.y() > 0) { + rect.moveTopLeft(m); + path.arcTo(rect, 90, 90); + } else if (smNorm.y() > 0 && meNorm.x() > 0) { + rect.moveBottomLeft(m); + path.arcTo(rect, 180, 90); + } else if (smNorm.x() < 0 && meNorm.y() < 0) { + rect.moveBottomLeft(m); + path.arcTo(rect, 270, -90); + } else if (smNorm.x() > 0 && meNorm.y() > 0) { + rect.moveTopRight(m); + path.arcTo(rect, 90, -90); + } else if (smNorm.y() < 0 && meNorm.x() < 0) { + rect.moveTopRight(m); + path.arcTo(rect, 0, 90); + } else if (smNorm.y() > 0 && meNorm.x() < 0) { + rect.moveBottomRight(m); + path.arcTo(rect, 0, -90); + } else if (smNorm.x() > 0 && meNorm.y() < 0) { + rect.moveBottomRight(m); + path.arcTo(rect, 270, 90); + } + + path.lineTo(e); + return path; +} + +// This function determines whether the vertices are in cw or ccw order. +// It finds the lowest and rightmost vertex, and computes the cross-product +// of the vectors along its incident edges. +// Written by Joseph O'Rourke, 25 August 1995. orourke@cs.smith.edu +// 1: ccw +// 0: default +// -1: cw + +static int counterClockWise(const std::vector<QPointF> &points) { - painter->save(); - painter->setRenderHint(QPainter::Antialiasing); + if (points.empty()) + return 0; - QPen pen; - pen.setCosmetic(true); - pen.setJoinStyle(Qt::MiterJoin); - pen.setCapStyle(Qt::RoundCap); + // FindLR finds the lowest, rightmost point. + auto findLR = [](const std::vector<QPointF> &points) { + int i = 0; + int m = 0; + QPointF min = points.front(); - pen.setColor(color); + for (const auto p : points) { + if ((p.y() < min.y()) || ((p.y() == min.y()) && (p.x() > min.x()))) { + m = i; + min = p; + } + ++i; + } + return m; + }; - if (dash) - pen.setStyle(Qt::DashLine); + const int m = findLR(points); + const int n = points.size(); + + // Determine previous and next point to m (the lowest, rightmost point). + const QPointF a = points[(m + (n - 1)) % n]; + const QPointF b = points[m]; + const QPointF c = points[(m + 1) % n]; + + const int area = a.x() * b.y() - a.y() * b.x() + + a.y() * c.x() - a.x() * c.y() + + b.x() * c.y() - c.x() * b.y(); + + if (area > 0) + return 1; + else if (area < 0) + return -1; else - pen.setStyle(Qt::SolidLine); - pen.setWidthF(width); - painter->setPen(pen); + return 0; +} - //const bool forceVertical = false; - //const bool forceHorizontal = false; +static QPainterPath quadBezier(const QPointF &s, + const QPointF &c, + const QPointF &e, + int bezier, + int breakOffset) +{ + const QLineF se(s, e); + const QPointF breakPoint = se.pointAt(breakOffset / 100.0); + QLineF breakLine; - const int padding = 2 * width + 2 * adjustedWidth; + if (counterClockWise({s, c, e}) == 1) + breakLine = QLineF(breakPoint, breakPoint + QPointF(se.dy(), -se.dx())); + else + breakLine = QLineF(breakPoint, breakPoint + QPointF(-se.dy(), se.dx())); - const int arrowLength = 4 * adjustedWidth; - const int arrowWidth = 8 * adjustedWidth; + breakLine.setLength(se.length()); - const bool boolExitRight = from.right() < to.center().x(); - const bool boolExitBottom = from.bottom() < to.center().y(); + const QPointF controlPoint = breakLine.pointAt(bezier / 100.0); - bool horizontalFirst = true; + QPainterPath path(s); + path.quadTo(controlPoint, e); - /* - if (verticalOverlap(from, to) && !horizontalOverlap(from, to)) - horizontalFirst = false; - */ + return path; +} - const qreal middleFactor = breakOffset / 100.0; +static QPainterPath cubicBezier(const QPointF &s, + const QPointF &c1, + const QPointF &c2, + const QPointF &e, + int bezier) +{ + QPainterPath path(s); + const QPointF adjustedC1 = QLineF(s, c1).pointAt(bezier / 100.0); + const QPointF adjustedC2 = QLineF(e, c2).pointAt(bezier / 100.0); - QPointF startP; + path.cubicTo(adjustedC1, adjustedC2, e); - bool extraLine = false; + return path; +} - if (horizontalFirst) { - if (to.center().x() > from.left() && to.center().x() < from.right()) { - horizontalFirst = false; - extraLine = true; - } else if (verticalOverlap(from, to)) { - horizontalFirst = true; - extraLine = true; +static QPainterPath lShapedConnection(const QPointF &start, + const QPointF &mid, + const QPointF &end, + const ConnectionConfiguration &config) +{ + if (config.type == ConnectionType::Default) { + if (config.radius == 0) { + QPainterPath path(start); + path.lineTo(mid); + path.lineTo(end); + return path; + } else { + return roundedCorner(start, mid, end, config.radius); } - } else { - if (to.center().y() > from.top() && to.center().y() < from.bottom()) { - horizontalFirst = true; - extraLine = true; - } else if (horizontalOverlap(from, to)) { - horizontalFirst = false; - extraLine = true; - } + return quadBezier(start, mid, end, config.bezier, config.breakOffset); } +} - if (horizontalFirst) { - const qreal startY = from.center().y() + startOffset; - qreal startX = from.x() - padding; - if (boolExitRight) - startX = from.right() + padding; +static QPainterPath sShapedConnection(const QPointF &start, + const QPointF &mid1, + const QPointF &mid2, + const QPointF &end, + const ConnectionConfiguration &config) +{ + if (config.type == ConnectionType::Default) { + if (config.radius == 0) { + QPainterPath path(start); + path.lineTo(mid1); + path.lineTo(mid2); + path.lineTo(end); + return path; + } else { + const QLineF breakLine(mid1, mid2); + QPainterPath path1 = roundedCorner(start, mid1, breakLine.center(), config.radius); + QPainterPath path2 = roundedCorner(breakLine.center(), mid2, end, config.radius); + return path1 + path2; + } + } else { + return cubicBezier(start, mid1, mid2, end, config.bezier); + } +} - startP = QPointF(startX, startY); +class Connection +{ +public: + Connection(const ResolveConnection &resolveConnection, + const QPointF &position, + const QmlItemNode &from, + const QmlItemNode &to, + const ConnectionConfiguration &connectionConfig) + : config(connectionConfig) + { + if (from.isFlowDecision()) { + int size = flowBlockSize; + if (from.modelNode().hasAuxiliaryData("blockSize")) + size = from.modelNode().auxiliaryData("blockSize").toInt(); + + fromRect = QRectF(0, 0, size, size); + + QTransform transform; + transform.translate(fromRect.center().x(), fromRect.center().y()); + transform.rotate(45); + transform.translate(-fromRect.center().x(), -fromRect.center().y()); + + fromRect = transform.mapRect(fromRect); + } else if (from.isFlowWildcard()) { + int size = flowBlockSize; + if (from.modelNode().hasAuxiliaryData("blockSize")) + size = from.modelNode().auxiliaryData("blockSize").toInt(); + fromRect = QRectF(0, 0, size, size); + } else if (from.isFlowView()) { + fromRect = QRectF(0, 0, flowBlockSize, flowBlockSize); + } else { + fromRect = from.instanceBoundingRect(); + } - qreal endY = to.top() - padding; + fromRect.translate(from.flowPosition()); - if (from.bottom() > to.y()) - endY = to.bottom() + padding; + if (!resolveConnection.joinConnection && resolveConnection.areaNode.isValid()) { + fromRect = QmlItemNode(resolveConnection.areaNode).instanceBoundingRect(); + fromRect.translate(from.flowPosition()); + fromRect.translate(resolveConnection.areaNode.instancePosition()); + } - if (!extraLine) { + if (to.isFlowDecision()) { + int size = flowBlockSize; + if (to.modelNode().hasAuxiliaryData("blockSize")) + size = to.modelNode().auxiliaryData("blockSize").toInt(); + toRect = QRectF(0, 0, size, size); - const qreal endX = to.center().x() + endOffset; + QTransform transform; + transform.translate(toRect.center().x(), toRect.center().y()); + transform.rotate(45); + transform.translate(-toRect.center().x(), -toRect.center().y()); - const QPointF midP(endX, startY); + toRect = transform.mapRect(toRect); + } else { + toRect = to.instanceBoundingRect(); + } - const QPointF endP(endX, endY); + toRect.translate(to.flowPosition()); - painter->drawLine(startP, midP); - painter->drawLine(midP, endP); + if (resolveConnection.isStartLine) { + fromRect = QRectF(0, 0, 96, 96); // TODO Use const startItemOffset + fromRect.translate(to.flowPosition() + QPoint(-180, toRect.height() / 2 - 96 / 2)); + fromRect.translate(0, config.outOffset); + } - int flip = 1; + fromRect.translate(-position); + toRect.translate(-position); - if (midP.y() < endP.y()) - flip = -1; + bool horizontalFirst = true; + extraLine = false; - pen.setStyle(Qt::SolidLine); - painter->setPen(pen); - painter->drawLine(endP + flip * QPoint(arrowWidth / 2, arrowLength), endP); - painter->drawLine(endP + flip * QPoint(-arrowWidth / 2, arrowLength), endP); + if (horizontalFirst) { + if (toRect.center().x() > fromRect.left() && toRect.center().x() < fromRect.right()) { + horizontalFirst = false; + extraLine = true; + } else if (verticalOverlap(fromRect, toRect)) { + horizontalFirst = true; + extraLine = true; + } } else { + if (toRect.center().y() > fromRect.top() && toRect.center().y() < fromRect.bottom()) { + horizontalFirst = true; + extraLine = true; + } else if (horizontalOverlap(fromRect, toRect)) { + horizontalFirst = false; + extraLine = true; + } + } - qreal endX = to.left() - padding; - - if (from.right() > to.x()) - endX = to.right() + padding; - - const qreal midX = startX * middleFactor + endX * (1-middleFactor); - const QPointF midP(midX, startY); - const QPointF midP2(midX, to.center().y() + endOffset); - const QPointF endP(endX, to.center().y() + endOffset); - painter->drawLine(startP, midP); - painter->drawLine(midP, midP2); - painter->drawLine(midP2, endP); + const bool boolExitRight = fromRect.right() < toRect.center().x(); + const bool boolExitBottom = fromRect.bottom() < toRect.center().y(); - int flip = 1; + const int padding = 4 * config.adjustedWidth; - if (midP2.x() < endP.x()) - flip = -1; + if (horizontalFirst) { + const qreal startX = boolExitRight ? fromRect.right() + padding : fromRect.x() - padding; + const qreal startY = fromRect.center().y() + config.outOffset; + start = QPointF(startX, startY); - pen.setStyle(Qt::SolidLine); - painter->setPen(pen); - painter->drawLine(endP + flip * QPoint(arrowWidth / 2, arrowWidth / 2), endP); - painter->drawLine(endP + flip * QPoint(arrowLength, -arrowWidth / 2), endP); + if (!extraLine) { + const qreal endY = (fromRect.bottom() > toRect.y()) ? toRect.bottom() + padding : toRect.top() - padding; + end = QPointF(toRect.center().x() + config.inOffset, endY); + mid1 = mid2 = QPointF(end.x(), start.y()); + path = lShapedConnection(start, mid1, end, config); + } else { + const qreal endX = (fromRect.right() > toRect.x()) ? toRect.right() + padding : toRect.left() - padding; + end = QPointF(endX, toRect.center().y() + config.inOffset); + const qreal middleFactor = config.breakOffset / 100.0; + mid1 = QPointF(start.x() * middleFactor + end.x() * (1 - middleFactor), start.y()); + mid2 = QPointF(mid1.x(), end.y()); + path = sShapedConnection(start, mid1, mid2, end, config); + } + } else { + const qreal startX = fromRect.center().x() + config.outOffset; + const qreal startY = boolExitBottom ? fromRect.bottom() + padding : fromRect.top() - padding; + start = QPointF(startX, startY); + + if (!extraLine) { + const qreal endX = (fromRect.right() > toRect.x()) ? toRect.right() + padding : toRect.left() - padding; + end = QPointF(endX, toRect.center().y() + config.inOffset); + mid1 = mid2 = QPointF(start.x(), end.y()); + path = lShapedConnection(start, mid1, end, config); + } else { + const qreal endY = (fromRect.bottom() > toRect.y()) ? toRect.bottom() + padding : toRect.top() - padding; + end = QPointF(toRect.center().x() + config.inOffset, endY); + const qreal middleFactor = config.breakOffset / 100.0; + mid1 = QPointF(start.x(), start.y() * middleFactor + end.y() * (1 - middleFactor)); + mid2 = QPointF(end.x(), mid1.y()); + path = sShapedConnection(start, mid1, mid2, end, config); + } } + } - } else { - const qreal startX = from.center().x() + startOffset; - - qreal startY = from.top() - padding; - if (boolExitBottom) - startY = from.bottom() + padding; - - startP = QPointF(startX, startY); - qreal endX = to.left() - padding; + QRectF fromRect; + QRectF toRect; - if (from.right() > to.x()) - endX = to.right() + padding; + QPointF start; + QPointF end; - if (!extraLine) { - const qreal endY = to.center().y() + endOffset; + QPointF mid1; + QPointF mid2; - const QPointF midP(startX, endY); + bool extraLine; - const QPointF endP(endX, endY); + ConnectionConfiguration config; + QPainterPath path; +}; - painter->drawLine(startP, midP); - painter->drawLine(midP, endP); +static int normalizeAngle(int angle) +{ + int newAngle = angle; + while (newAngle <= -90) newAngle += 180; + while (newAngle > 90) newAngle -= 180; + return newAngle; +} - int flip = 1; +void FormEditorTransitionItem::updateGeometry() +{ + FormEditorItem::updateGeometry(); - if (midP.x() < endP.x()) - flip = -1; + ResolveConnection resolved(qmlItemNode()); - pen.setStyle(Qt::SolidLine); - painter->setPen(pen); - painter->drawLine(endP + flip * QPoint(arrowWidth / 2, arrowWidth / 2), endP); - painter->drawLine(endP + flip * QPoint(arrowLength, -arrowWidth / 2), endP); - } else { + if (!isValid(resolved.from) || !isValid(resolved.to)) + return; - qreal endY = to.top() - padding; + QPointF min(std::numeric_limits<qreal>::max(), std::numeric_limits<qreal>::max()); + QPointF max(std::numeric_limits<qreal>::min(), std::numeric_limits<qreal>::min()); - if (from.bottom() > to.y()) - endY = to.bottom() + padding; + auto minMaxHelper = [&](const QList<QmlItemNode> &items) { + QRectF boundingRect; + for (const auto &i : items) { + const QPointF p = QmlItemNode(i).flowPosition(); - const qreal midY = startY * middleFactor + endY * (1-middleFactor); - const QPointF midP(startX, midY); - const QPointF midP2(to.center().x() + endOffset, midY); - const QPointF endP(to.center().x() + endOffset, endY); + if (p.x() < min.x()) + min.setX(p.x()); - painter->drawLine(startP, midP); - painter->drawLine(midP, midP2); - painter->drawLine(midP2, endP); + if (p.y() < min.y()) + min.setY(p.y()); - int flip = 1; + if (p.x() > max.x()) + max.setX(p.x()); - if (midP2.y() < endP.y()) - flip = -1; + if (p.y() > max.y()) + max.setY(p.y()); - pen.setStyle(Qt::SolidLine); - painter->setPen(pen); - painter->drawLine(endP + flip * QPoint(arrowWidth / 2, arrowLength), endP); - painter->drawLine(endP + flip * QPoint(-arrowWidth / 2, arrowLength), endP); + const QRectF tmp(p, i.instanceSize()); + boundingRect = boundingRect.united(tmp); + } + return boundingRect; + }; + + const QRectF toBoundingRect = minMaxHelper(resolved.to); + // Special treatment for start line bounding rect calculation + const QRectF fromBoundingRect = !resolved.isStartLine ? minMaxHelper(resolved.from) + : toBoundingRect + QMarginsF(startItemOffset, 0, 0, 0); + + QRectF overallBoundingRect(min, max); + overallBoundingRect = overallBoundingRect.united(fromBoundingRect); + overallBoundingRect = overallBoundingRect.united(toBoundingRect); + + setPos(overallBoundingRect.topLeft()); + + // Needed due to the upcoming rects are relative to the set position. If this one is not + // translate to the newly set position, then th resulting bounding box would be to big. + overallBoundingRect.translate(-pos()); + + ConnectionConfiguration config(qmlItemNode(), resolved, viewportTransform().m11()); + + // Local painter is used to get the labels bounding rect by using drawText() + QPixmap pixmap(640, 480); + QPainter localPainter(&pixmap); + QFont font = localPainter.font(); + font.setPixelSize(config.fontSize); + localPainter.setFont(font); + + for (const auto &from : resolved.from) { + for (const auto &to : resolved.to) { + Connection connection(resolved, pos(), from, to, config); + + // Just add the configured transition width to the bounding box to make sure the BB is not cutting + // off half of the transition resulting in a bad selection experience. + QRectF pathBoundingRect = connection.path.boundingRect() + QMarginsF(config.width, config.width, config.width, config.width); + overallBoundingRect = overallBoundingRect.united(connection.fromRect); + overallBoundingRect = overallBoundingRect.united(connection.toRect); + overallBoundingRect = overallBoundingRect.united(pathBoundingRect); + + // Calculate bounding rect for label + // TODO The calculation should be put into a separate function to avoid code duplication as this + // can also be found in drawLabel() + if (!connection.config.label.isEmpty()) { + const qreal percent = connection.config.labelPosition / 100.0; + const QPointF pos = connection.path.pointAtPercent(percent); + const qreal angle = connection.path.angleAtPercent(percent); + + QLineF tmp(pos, QPointF(10, 10)); + tmp.setLength(connection.config.labelOffset); + tmp.setAngle(angle + (connection.config.labelFlipSide ? 270 : 90)); + + QRectF textRect(0, 0, 100, 50); + textRect.moveCenter(tmp.p2()); + + QRectF labelRect; + + QTransform transform; + transform.translate(textRect.center().x(), textRect.center().y()); + transform.rotate(-normalizeAngle(angle)); + transform.translate(-textRect.center().x(), -textRect.center().y()); + + localPainter.setTransform(transform); + localPainter.drawText(textRect, + connection.config.labelFlags, + connection.config.label, + &labelRect); + QRectF labelBoundingBox = transform.mapRect(labelRect); + overallBoundingRect = overallBoundingRect.united(labelBoundingBox); + } } } - pen.setWidthF(width); - pen.setStyle(Qt::SolidLine); - painter->setPen(pen); - painter->setBrush(Qt::white); - painter->drawEllipse(startP, arrowLength / 3, arrowLength / 3); + m_selectionBoundingRect = overallBoundingRect; + m_paintedBoundingRect = m_selectionBoundingRect; + m_boundingRect = m_selectionBoundingRect; + setZValue(10); +} - painter->restore(); +QPointF FormEditorTransitionItem::instancePosition() const +{ + return qmlItemNode().flowPosition(); } -void FormEditorTransitionItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) +static void drawLabel(QPainter *painter, const Connection &connection) { - if (!painter->isActive()) + if (connection.config.label.isEmpty()) return; - if (!qmlItemNode().modelNode().isValid()) - return; + const qreal percent = connection.config.labelPosition / 100.0; + const QPointF pos = connection.path.pointAtPercent(percent); + const qreal angle = connection.path.angleAtPercent(percent); - if (!qmlItemNode().modelNode().hasBindingProperty("to")) - return; + QLineF tmp(pos, QPointF(10, 10)); + tmp.setLength(connection.config.labelOffset); + tmp.setAngle(angle + (connection.config.labelFlipSide ? 270 : 90)); - painter->save(); + QRectF textRect(0, 0, 100, 50); + textRect.moveCenter(tmp.p2()); - ResolveConnection resolved(qmlItemNode()); + painter->save(); + painter->translate(textRect.center()); + painter->rotate(-normalizeAngle(angle)); + painter->translate(-textRect.center()); + painter->drawText(textRect, connection.config.labelFlags, connection.config.label); + painter->restore(); +} - if (!resolved.from.modelNode().isValid()) - return; +static void drawArrow(QPainter *painter, + const QPointF &point, + const qreal &angle, + int arrowLength, + int arrowWidth) +{ + const QPointF peakP(0, 0); + const QPointF leftP(-arrowLength, -arrowWidth * 0.5); + const QPointF rightP(-arrowLength, arrowWidth * 0.5); - QRectF fromRect = QmlItemNode(resolved.from).instanceBoundingRect(); - if (QmlItemNode(resolved.from).isFlowDecision()) - fromRect = QRectF(0, 0, flowBlockSize, flowBlockSize); + painter->save(); - if (QmlItemNode(resolved.from).isFlowWildcard()) - fromRect = QRectF(0, 0, flowBlockSize, flowBlockSize); - fromRect.translate(QmlItemNode(resolved.from).flowPosition()); + painter->translate(point); + painter->rotate(-angle); + painter->drawLine(leftP, peakP); + painter->drawLine(rightP, peakP); - if (resolved.isStartLine) { - fromRect = QRectF(0,0,100,100); - fromRect.translate(QmlItemNode(resolved.to).flowPosition()- QPoint(200, 0)); - } + painter->restore(); +} - if (!resolved.joinConnection && resolved.areaNode.isValid()) { - fromRect = QmlItemNode(resolved.areaNode).instanceBoundingRect(); - fromRect.translate(QmlItemNode(resolved.from).flowPosition()); - fromRect.translate(resolved.areaNode.instancePosition()); - } +static void paintConnection(QPainter *painter, const Connection &connection) +{ + const int arrowLength = 4 * connection.config.adjustedWidth; + const int arrowWidth = 8 * connection.config.adjustedWidth; - QRectF toRect = QmlItemNode(resolved.to).instanceBoundingRect(); - if (QmlItemNode(resolved.to).isFlowDecision()) - toRect = QRectF(0, 0, flowBlockSize,flowBlockSize); + painter->save(); + painter->setRenderHint(QPainter::Antialiasing); - toRect.translate(QmlItemNode(resolved.to).flowPosition()); + // Draw path/connection line + QPen pen; + pen.setCosmetic(true); + pen.setJoinStyle(Qt::MiterJoin); + pen.setCapStyle(Qt::RoundCap); + pen.setBrush(connection.config.lineBrush); - if (resolved.isStartLine) { - fromRect = QRectF(0,0,50,50); - fromRect.translate(QmlItemNode(resolved.to).flowPosition() + QPoint(-120, toRect.height() / 2 - 25)); + if (connection.config.dashPattern.size()) { + pen.setStyle(Qt::CustomDashLine); + pen.setDashPattern(connection.config.dashPattern); + } else { + pen.setStyle(connection.config.penStyle); } - toRect.translate(-pos()); - fromRect.translate(-pos()); + pen.setWidthF(connection.config.width); + painter->setPen(pen); - qreal width = 2; + painter->drawPath(connection.path); - const qreal scaleFactor = viewportTransform().m11(); + pen.setWidthF(connection.config.width); + pen.setStyle(Qt::SolidLine); + pen.setColor(connection.config.color); + painter->setPen(pen); - if (qmlItemNode().modelNode().hasAuxiliaryData("width")) - width = qmlItemNode().modelNode().auxiliaryData("width").toInt(); + // Draw arrow + qreal angle = QLineF(connection.mid2, connection.end).angle(); - qreal adjustedWidth = width / scaleFactor; + if (!connection.config.joinEnd) { + qreal anglePercent = 1.0; + if (connection.extraLine && connection.config.bezier < 80) { + anglePercent = 1.0 - qMin(1.0, (80 - connection.config.bezier) / 10.0) * 0.05; + angle = connection.path.angleAtPercent(anglePercent); + } + } - if (qmlItemNode().modelNode().isSelected()) - width += 2; - if (m_hitTest) - width *= 8; + if (connection.config.drawEnd) + drawArrow(painter, connection.end, angle, arrowLength, arrowWidth); - QColor color = "#e71919"; + // Draw start ellipse + if (connection.config.drawStart) { + painter->setBrush(Qt::white); + painter->drawEllipse(connection.start, arrowLength / 3, arrowLength / 3); + } - if (resolved.isStartLine) - color = "blue"; + // Draw label + drawLabel(painter, connection); - if (resolved.isWildcardLine) - color = "green"; + painter->restore(); +} - bool dash = false; +void FormEditorTransitionItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) +{ + if (!painter->isActive()) + return; - if (qmlItemNode().rootModelNode().hasAuxiliaryData("transitionColor")) - color = qmlItemNode().rootModelNode().auxiliaryData("transitionColor").value<QColor>(); + if (!qmlItemNode().modelNode().isValid()) + return; - if (qmlItemNode().modelNode().hasAuxiliaryData("color")) - color = qmlItemNode().modelNode().auxiliaryData("color").value<QColor>(); + if (!qmlItemNode().modelNode().hasBindingProperty("to")) + return; - if (qmlItemNode().modelNode().hasAuxiliaryData("dash")) - dash = qmlItemNode().modelNode().auxiliaryData("dash").toBool(); + painter->save(); + painter->setRenderHint(QPainter::Antialiasing); - int outOffset = 0; - int inOffset = 0; + ResolveConnection resolved(qmlItemNode()); - if (qmlItemNode().modelNode().hasAuxiliaryData("outOffset")) - outOffset = qmlItemNode().modelNode().auxiliaryData("outOffset").toInt(); + if (!isModelNodeValid(resolved.from)) + return; - if (qmlItemNode().modelNode().hasAuxiliaryData("inOffset")) - inOffset = qmlItemNode().modelNode().auxiliaryData("inOffset").toInt(); + ConnectionConfiguration config(qmlItemNode(), resolved, viewportTransform().m11(), m_hitTest); - int breakOffset = 50; + QFont font = painter->font(); + font.setPixelSize(config.fontSize); + painter->setFont(font); - if (qmlItemNode().modelNode().hasAuxiliaryData("breakPoint")) - breakOffset = qmlItemNode().modelNode().auxiliaryData("breakPoint").toInt(); + for (const auto &f : resolved.from) { + for (const auto &t : resolved.to) { + Connection connection(resolved, pos(), f, t, config); + if (!config.hitTesting) { + // The following if statement is a special treatment for n-to-n, 1-to-n and n-to-1 + // transitions. This block is setting up the connection configuration for drawing. + QPointF start = connection.path.pointAtPercent(0.0); + QPointF end = connection.path.pointAtPercent(1.0); - paintConnection(painter, fromRect, toRect, width, adjustedWidth ,color, dash, outOffset, inOffset, breakOffset); + // many-to-many + if ((resolved.from.size() > 1 && resolved.to.size() > 1)) { + // TODO + } + // many-to-one + else if (resolved.from.size() > 1 && resolved.to.size() == 1) { + connection.config.joinEnd = true; + + if (qmlItemNode().modelNode().isSelected()) { + connection.config.dashPattern << 2 << 3; + } else { + QLinearGradient gradient(start, end); + QColor color = config.color; + color.setAlphaF(0); + gradient.setColorAt(0.25, color); + color.setAlphaF(1); + gradient.setColorAt(1, color); + + connection.config.lineBrush = QBrush(gradient); + connection.config.drawStart = false; + connection.config.drawEnd = true; + connection.config.dashPattern << 1 << 6; + } + } + // one-to-many + else if (resolved.from.size() == 1 && resolved.to.size() > 1) { + + if (qmlItemNode().modelNode().isSelected()) { + connection.config.dashPattern << 2 << 3; + } else { + QLinearGradient gradient(start, end); + QColor color = config.color; + color.setAlphaF(1); + gradient.setColorAt(0, color); + color.setAlphaF(0); + gradient.setColorAt(0.75, color); + + connection.config.lineBrush = QBrush(gradient); + connection.config.drawStart = true; + connection.config.drawEnd = false; + connection.config.dashPattern << 1 << 6; + } + } + } else { + connection.config.penStyle = Qt::SolidLine; + } - if (resolved.isStartLine) { - QPen pen; - pen.setCosmetic(true); + paintConnection(painter, connection); - pen.setColor(color); - painter->setPen(pen); - painter->drawRect(fromRect); - - if (scaleFactor > 0.4) { - painter->drawLine(fromRect.topRight() + QPoint(20,10), fromRect.bottomRight() + QPoint(20,-10)); - painter->drawLine(fromRect.topRight() + QPoint(25,12), fromRect.bottomRight() + QPoint(25,-12)); - painter->drawLine(fromRect.topRight() + QPoint(30,15), fromRect.bottomRight() + QPoint(30,-15)); - painter->drawLine(fromRect.topRight() + QPoint(35,17), fromRect.bottomRight() + QPoint(35,-17)); - painter->drawLine(fromRect.topRight() + QPoint(40,20), fromRect.bottomRight() + QPoint(40,-20)); + if (resolved.isStartLine) { + const QString icon = Theme::getIconUnicode(Theme::startNode); + + QPen pen; + pen.setCosmetic(true); + pen.setColor(config.color); + painter->setPen(pen); + + const int iconAdjust = 48; + const int size = connection.fromRect.width(); + const int iconSize = size - iconAdjust; + const int x = connection.fromRect.topRight().x() - startItemOffset; + const int y = connection.fromRect.topRight().y(); + painter->drawRoundedRect(x, y , size - 10, size, size / 2, iconSize / 2); + drawIcon(painter, x + iconAdjust / 2, y + iconAdjust / 2, icon, iconSize, iconSize, config.color); + } } } @@ -1124,12 +1657,91 @@ QTransform FormEditorItem::viewportTransform() const void FormEditorFlowDecisionItem::updateGeometry() { prepareGeometryChange(); - m_selectionBoundingRect = QRectF(0,0, flowBlockSize, flowBlockSize); + + int size = flowBlockSize; + if (qmlItemNode().modelNode().hasAuxiliaryData("blockSize")) + size = qmlItemNode().modelNode().auxiliaryData("blockSize").toInt(); + + QRectF boundingRect(0, 0, size, size); + QTransform transform; + if (qmlItemNode().isFlowDecision()) { + transform.translate(boundingRect.center().x(), boundingRect.center().y()); + transform.rotate(45); + transform.translate(-boundingRect.center().x(), -boundingRect.center().y()); + + // If drawing the dialog title is requested we need to add it to the bounding rect. + QRectF labelBoundingRect; + int showDialogLabel = false; + if (qmlItemNode().modelNode().hasAuxiliaryData("showDialogLabel")) + showDialogLabel = qmlItemNode().modelNode().auxiliaryData("showDialogLabel").toBool(); + + if (showDialogLabel) { + QString dialogTitle; + if (qmlItemNode().modelNode().hasVariantProperty("dialogTitle")) + dialogTitle = qmlItemNode().modelNode().variantProperty("dialogTitle").value().toString(); + + if (!dialogTitle.isEmpty()) { + // Local painter is used to get the labels bounding rect by using drawText() + QPixmap pixmap(640, 480); + QPainter localPainter(&pixmap); + QFont font = localPainter.font(); + font.setPixelSize(labelFontSize / viewportTransform().m11()); + localPainter.setFont(font); + + int margin = blockAdjust * 0.5; + const QRectF adjustedRect = boundingRect.adjusted(margin, margin, -margin, -margin); + + QRectF textRect(0, 0, 100, 20); + + Qt::Corner corner = Qt::TopRightCorner; + if (qmlItemNode().modelNode().hasAuxiliaryData("dialogLabelPosition")) + corner = qmlItemNode().modelNode().auxiliaryData("dialogLabelPosition").value<Qt::Corner>(); + + int flag = 0; + switch (corner) { + case Qt::TopLeftCorner: + flag = Qt::AlignRight; + textRect.moveBottomRight(adjustedRect.topLeft()); + break; + case Qt::TopRightCorner: + flag = Qt::AlignLeft; + textRect.moveBottomLeft(adjustedRect.topRight()); + break; + case Qt::BottomLeftCorner: + flag = Qt::AlignRight; + textRect.moveTopRight(adjustedRect.bottomLeft()); + break; + case Qt::BottomRightCorner: + flag = Qt::AlignLeft; + textRect.moveTopLeft(adjustedRect.bottomRight()); + break; + } + + localPainter.drawText(textRect, flag | Qt::TextDontClip, dialogTitle, &labelBoundingRect); + } + } + + // Unite the rotate item bounding rect with the label bounding rect. + boundingRect = transform.mapRect(boundingRect); + boundingRect = boundingRect.united(labelBoundingRect); + } + + m_selectionBoundingRect = boundingRect; m_paintedBoundingRect = m_selectionBoundingRect; m_boundingRect = m_paintedBoundingRect; setTransform(qmlItemNode().instanceTransformWithContentTransform()); const QPointF pos = qmlItemNode().flowPosition(); setTransform(QTransform::fromTranslate(pos.x(), pos.y())); + + // Call updateGeometry() on all related transitions + QmlFlowTargetNode flowItem(qmlItemNode()); + if (flowItem.isValid() && flowItem.flowView().isValid()) { + const auto nodes = flowItem.flowView().transitions(); + for (const ModelNode &node : nodes) { + if (FormEditorItem *item = scene()->itemForQmlItemNode(node)) + item->updateGeometry(); + } + } } void FormEditorFlowDecisionItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) @@ -1141,6 +1753,9 @@ void FormEditorFlowDecisionItem::paint(QPainter *painter, const QStyleOptionGrap painter->save(); + painter->setRenderHint(QPainter::Antialiasing); + painter->setRenderHint(QPainter::SmoothPixmapTransform); + QPen pen; pen.setJoinStyle(Qt::MiterJoin); pen.setCosmetic(true); @@ -1179,20 +1794,91 @@ void FormEditorFlowDecisionItem::paint(QPainter *painter, const QStyleOptionGrap if (qmlItemNode().modelNode().hasAuxiliaryData("fillColor")) fillColor = qmlItemNode().modelNode().auxiliaryData("fillColor").value<QColor>(); + painter->save(); + if (fillColor.alpha() > 0) - painter->fillRect(boundingRect(), fillColor); + painter->setBrush(fillColor); + + int radius = blockRadius; + if (qmlItemNode().modelNode().hasAuxiliaryData("blockRadius")) + radius = qmlItemNode().modelNode().auxiliaryData("blockRadius").toInt(); + + int size = flowBlockSize; + if (qmlItemNode().modelNode().hasAuxiliaryData("blockSize")) + size = qmlItemNode().modelNode().auxiliaryData("blockSize").toInt(); + + QRectF boundingRect(0, 0, size, size); + QTransform transform; + int margin = blockAdjust; + if (m_iconType == DecisionIcon) { + transform.translate(boundingRect.center().x(), boundingRect.center().y()); + transform.rotate(45); + transform.translate(-boundingRect.center().x(), -boundingRect.center().y()); + margin *= 0.5; + } - painter->drawLine(boundingRect().left(), boundingRect().center().y(), - boundingRect().center().x(), boundingRect().top()); + const QRectF adjustedRect = boundingRect.adjusted(margin, margin, -margin, -margin); - painter->drawLine(boundingRect().center().x(), boundingRect().top(), - boundingRect().right(), boundingRect().center().y()); + painter->setTransform(transform, true); + painter->drawRoundedRect(adjustedRect, radius, radius); + + const int iconDecrement = 32; + const int iconSize = adjustedRect.width() - iconDecrement; + const int offset = iconDecrement / 2 + margin; + + painter->restore(); + + // Draw the dialog title inside the form view if requested. Decision item only. + int showDialogLabel = false; + if (qmlItemNode().modelNode().hasAuxiliaryData("showDialogLabel")) + showDialogLabel = qmlItemNode().modelNode().auxiliaryData("showDialogLabel").toBool(); + + if (showDialogLabel) { + QString dialogTitle; + if (qmlItemNode().modelNode().hasVariantProperty("dialogTitle")) + dialogTitle = qmlItemNode().modelNode().variantProperty("dialogTitle").value().toString(); + + if (!dialogTitle.isEmpty()) { + + QFont font = painter->font(); + font.setPixelSize(labelFontSize / scaleFactor); + painter->setFont(font); + + QRectF textRect(0, 0, 100, 20); + + Qt::Corner corner = Qt::TopRightCorner; + if (qmlItemNode().modelNode().hasAuxiliaryData("dialogLabelPosition")) + corner = qmlItemNode().modelNode().auxiliaryData("dialogLabelPosition").value<Qt::Corner>(); + + int flag = 0; + switch (corner) { + case Qt::TopLeftCorner: + flag = Qt::AlignRight; + textRect.moveBottomRight(adjustedRect.topLeft()); + break; + case Qt::TopRightCorner: + flag = Qt::AlignLeft; + textRect.moveBottomLeft(adjustedRect.topRight()); + break; + case Qt::BottomLeftCorner: + flag = Qt::AlignRight; + textRect.moveTopRight(adjustedRect.bottomLeft()); + break; + case Qt::BottomRightCorner: + flag = Qt::AlignLeft; + textRect.moveTopLeft(adjustedRect.bottomRight()); + break; + } + + painter->drawText(textRect, flag | Qt::TextDontClip, dialogTitle); + } + } - painter->drawLine(boundingRect().right(), boundingRect().center().y(), - boundingRect().center().x(), boundingRect().bottom()); + const QString icon = (m_iconType == + WildcardIcon) ? Theme::getIconUnicode(Theme::wildcard) + : Theme::getIconUnicode(Theme::decisionNode); - painter->drawLine(boundingRect().center().x(), boundingRect().bottom(), - boundingRect().left(), boundingRect().center().y()); + drawIcon(painter, offset, offset, icon, iconSize, iconSize, flowColor); painter->restore(); } |