1741 lines
57 KiB
C++
1741 lines
57 KiB
C++
#include "DiagramScene.h"
|
||
#include "items/BlockItem.h"
|
||
#include "items/ArrowItem.h"
|
||
#include "items/JunctionItem.h"
|
||
#include "items/HeaderFooterItem.h"
|
||
|
||
#include <QGraphicsSceneMouseEvent>
|
||
#include <QKeyEvent>
|
||
#include <QTimer>
|
||
#include <QHash>
|
||
#include <QSet>
|
||
#include <QDebug>
|
||
#include <QJsonObject>
|
||
#include <QJsonArray>
|
||
#include <QJsonDocument>
|
||
#include <QVariantMap>
|
||
#include <QGraphicsRectItem>
|
||
#include <QSizeF>
|
||
#include <QBrush>
|
||
#include <QColor>
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <optional>
|
||
|
||
DiagramScene::DiagramScene(QObject* parent)
|
||
: QGraphicsScene(parent)
|
||
{
|
||
m_currentPrefix.clear();
|
||
// Ограничиваем рабочую область A4 (210x297 мм) в пикселях при 96 dpi (~793x1122).
|
||
// По умолчанию используем альбомную ориентацию (ширина больше высоты).
|
||
const qreal w = 1122;
|
||
const qreal h = 793;
|
||
setSceneRect(-w * 0.5, -h * 0.5, w, h);
|
||
ensureFrame();
|
||
ensureHeaderFooter();
|
||
pushSnapshot();
|
||
}
|
||
|
||
BlockItem* DiagramScene::createBlockAt(const QPointF& scenePos) {
|
||
auto* b = new BlockItem(QString(), nullptr, m_nextBlockId++);
|
||
addItem(b);
|
||
b->setNumber(assignNumber(b));
|
||
connectBlockSignals(b);
|
||
const QRectF r = m_contentRect.isNull() ? sceneRect() : m_contentRect;
|
||
const QPointF desired = scenePos - QPointF(100, 50); // центрируем
|
||
const QRectF br = b->boundingRect().translated(desired);
|
||
QPointF clamped = desired;
|
||
if (br.left() < r.left()) clamped.setX(desired.x() + (r.left() - br.left()));
|
||
if (br.right() > r.right()) clamped.setX(clamped.x() - (br.right() - r.right()));
|
||
if (br.top() < r.top()) clamped.setY(desired.y() + (r.top() - br.top()));
|
||
if (br.bottom() > r.bottom()) clamped.setY(clamped.y() - (br.bottom() - r.bottom()));
|
||
b->setPos(clamped);
|
||
pushSnapshot();
|
||
return b;
|
||
}
|
||
|
||
BlockItem* DiagramScene::createBlockWithId(const QPointF& scenePos, int id, const QString& title) {
|
||
m_nextBlockId = std::max(m_nextBlockId, id + 1);
|
||
auto* b = new BlockItem(title, nullptr, id);
|
||
addItem(b);
|
||
b->setPos(scenePos);
|
||
if (b->number().isEmpty()) b->setNumber(assignNumber(b));
|
||
connectBlockSignals(b);
|
||
return b;
|
||
}
|
||
|
||
void DiagramScene::applyMeta(const QVariantMap& meta) {
|
||
m_meta = meta;
|
||
const bool colorfulMode = meta.value("colorfulMode", false).toBool();
|
||
const bool darkMode = meta.value("resolvedDarkMode",
|
||
meta.value("darkMode", false)).toBool();
|
||
|
||
const QColor fg = darkMode ? QColor(235, 235, 235) : QColor(20, 20, 20);
|
||
const QColor bg = darkMode ? QColor(26, 26, 26) : QColor(255, 255, 255);
|
||
QColor arrowColor = fg;
|
||
QColor blockBorderColor = fg;
|
||
QColor blockFontColor = fg;
|
||
if (colorfulMode) {
|
||
const QColor a(meta.value("arrowColor").toString());
|
||
const QColor b(meta.value("blockBorderColor").toString());
|
||
const QColor f(meta.value("blockFontColor").toString());
|
||
if (a.isValid()) arrowColor = a;
|
||
if (b.isValid()) blockBorderColor = b;
|
||
if (f.isValid()) blockFontColor = f;
|
||
}
|
||
|
||
setBackgroundBrush(QBrush(bg));
|
||
BlockItem::setVisualTheme(fg,
|
||
bg,
|
||
blockBorderColor,
|
||
blockFontColor,
|
||
darkMode ? QColor(60, 60, 80) : QColor(240, 240, 255));
|
||
ArrowItem::setVisualTheme(arrowColor, blockFontColor);
|
||
JunctionItem::setVisualTheme(arrowColor,
|
||
darkMode ? QColor(120, 180, 255) : QColor(40, 100, 255));
|
||
|
||
const QString currency = meta.value("currency").toString();
|
||
const QString placement = meta.value("currencyPlacement").toString();
|
||
BlockItem::setCurrencyFormat(currency, placement);
|
||
for (QGraphicsItem* it : items()) {
|
||
if (auto* b = qgraphicsitem_cast<BlockItem*>(it)) {
|
||
b->update();
|
||
} else if (auto* a = qgraphicsitem_cast<ArrowItem*>(it)) {
|
||
a->updatePath();
|
||
} else if (auto* j = qgraphicsitem_cast<JunctionItem*>(it)) {
|
||
j->update();
|
||
}
|
||
}
|
||
|
||
const QString size = meta.value("pageSize").toString();
|
||
const QString orient = meta.value("pageOrientation").toString();
|
||
auto sizeToMm = [](const QString& s) {
|
||
if (s == "A1") return QSizeF(594, 841);
|
||
if (s == "A2") return QSizeF(420, 594);
|
||
if (s == "A3") return QSizeF(297, 420);
|
||
return QSizeF(210, 297);
|
||
};
|
||
auto toPx = [](qreal mm) {
|
||
return mm * 96.0 / 25.4;
|
||
};
|
||
QSizeF mm = sizeToMm(size.isEmpty() ? "A4" : size);
|
||
QSizeF px(toPx(mm.width()), toPx(mm.height()));
|
||
if (orient == tr("Landscape") || orient == "Landscape") {
|
||
px = QSizeF(std::max(px.width(), px.height()), std::min(px.width(), px.height()));
|
||
} else if (orient == tr("Portrait") || orient == "Portrait") {
|
||
px = QSizeF(std::min(px.width(), px.height()), std::max(px.width(), px.height()));
|
||
}
|
||
const QRectF newRect(-px.width() * 0.5, -px.height() * 0.5, px.width(), px.height());
|
||
if (sceneRect() != newRect) {
|
||
setSceneRect(newRect);
|
||
}
|
||
|
||
const bool withHeader = meta.value("withHeader", true).toBool();
|
||
const bool withFooter = meta.value("withFooter", true).toBool();
|
||
const qreal h = sceneRect().height();
|
||
const qreal headerH = withHeader ? h * 0.12 : 0.0;
|
||
const qreal footerH = withFooter ? h * 0.08 : 0.0;
|
||
m_contentRect = sceneRect().adjusted(0, headerH, 0, -footerH);
|
||
|
||
ensureFrame();
|
||
ensureHeaderFooter();
|
||
if (m_headerFooter) {
|
||
m_headerFooter->setShowHeaderFooter(withHeader, withFooter);
|
||
m_headerFooter->setMeta(m_meta);
|
||
m_headerFooter->setPageRect(sceneRect());
|
||
m_headerFooter->update();
|
||
}
|
||
}
|
||
|
||
void DiagramScene::updateMeta(const QVariantMap& patch) {
|
||
QVariantMap merged = m_meta;
|
||
for (auto it = patch.cbegin(); it != patch.cend(); ++it) {
|
||
merged[it.key()] = it.value();
|
||
}
|
||
applyMeta(merged);
|
||
emit metaChanged(m_meta);
|
||
}
|
||
|
||
QString DiagramScene::currentNodeLabel() const {
|
||
if (m_currentBlockId < 0) return tr("TOP");
|
||
if (!m_currentPrefix.isEmpty()) return m_currentPrefix;
|
||
return tr("A0");
|
||
}
|
||
|
||
QString DiagramScene::currentDiagramTitle() const {
|
||
if (m_currentBlockId < 0) {
|
||
return m_meta.value("project").toString();
|
||
}
|
||
if (!m_hierarchy.isEmpty()) {
|
||
const Snapshot& parent = m_hierarchy.last().snapshot;
|
||
for (const auto& b : parent.blocks) {
|
||
if (b.id == m_currentBlockId) return b.title;
|
||
}
|
||
}
|
||
return m_meta.value("title").toString();
|
||
}
|
||
|
||
DiagramScene::Edge DiagramScene::hitTestEdge(const QPointF& scenePos, QPointF* outScenePoint) const {
|
||
const QRectF r = m_contentRect.isNull() ? sceneRect() : m_contentRect;
|
||
const qreal tol = 12.0;
|
||
Edge edge = Edge::None;
|
||
QPointF proj = scenePos;
|
||
|
||
if (scenePos.y() >= r.top() && scenePos.y() <= r.bottom()) {
|
||
if (std::abs(scenePos.x() - r.left()) <= tol) {
|
||
edge = Edge::Left;
|
||
proj = QPointF(r.left(), std::clamp(scenePos.y(), r.top(), r.bottom()));
|
||
} else if (std::abs(scenePos.x() - r.right()) <= tol) {
|
||
edge = Edge::Right;
|
||
proj = QPointF(r.right(), std::clamp(scenePos.y(), r.top(), r.bottom()));
|
||
}
|
||
}
|
||
if (scenePos.x() >= r.left() && scenePos.x() <= r.right()) {
|
||
if (std::abs(scenePos.y() - r.top()) <= tol) {
|
||
edge = Edge::Top;
|
||
proj = QPointF(std::clamp(scenePos.x(), r.left(), r.right()), r.top());
|
||
} else if (std::abs(scenePos.y() - r.bottom()) <= tol) {
|
||
edge = Edge::Bottom;
|
||
proj = QPointF(std::clamp(scenePos.x(), r.left(), r.right()), r.bottom());
|
||
}
|
||
}
|
||
|
||
if (outScenePoint) *outScenePoint = proj;
|
||
return edge;
|
||
}
|
||
|
||
bool DiagramScene::tryStartArrowDrag(const QPointF& scenePos, Qt::KeyboardModifiers mods) {
|
||
const auto itemsUnder = items(scenePos);
|
||
// интерфейсный круг
|
||
for (QGraphicsItem* it : itemsUnder) {
|
||
if (auto* a = qgraphicsitem_cast<ArrowItem*>(it)) {
|
||
if (!a->isInterfaceStub()) continue;
|
||
auto edge = a->interfaceEdge();
|
||
if (!edge) continue;
|
||
|
||
// Shift+drag to move along edge
|
||
if (mods.testFlag(Qt::ShiftModifier)) {
|
||
m_dragInterfaceStub = a;
|
||
return true;
|
||
}
|
||
|
||
a->setInterfaceIsStub(false);
|
||
a->setTempEndPoint(scenePos);
|
||
a->setLabelLocked(true);
|
||
m_dragArrow = a;
|
||
m_dragFromBlock.clear();
|
||
m_dragFromJunction.clear();
|
||
m_dragFromPort = edge->port;
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// проверяем, не стартуем ли с существующего junction
|
||
for (QGraphicsItem* it : itemsUnder) {
|
||
if (auto* j = qgraphicsitem_cast<JunctionItem*>(it)) {
|
||
if (!j->hitTest(scenePos, 8.0)) continue;
|
||
auto* a = new ArrowItem();
|
||
addItem(a);
|
||
ArrowItem::Endpoint from;
|
||
from.junction = j;
|
||
a->setFrom(from);
|
||
a->setTempEndPoint(scenePos);
|
||
m_dragArrow = a;
|
||
m_dragFromBlock.clear();
|
||
m_dragFromJunction = j;
|
||
m_dragFromPort = BlockItem::Port::Output;
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// ищем блок под курсором
|
||
for (QGraphicsItem* it : itemsUnder) {
|
||
auto* b = qgraphicsitem_cast<BlockItem*>(it);
|
||
if (!b) continue;
|
||
|
||
BlockItem::Port port{};
|
||
QPointF localPos;
|
||
if (!b->hitTestPort(scenePos, &port, &localPos)) continue;
|
||
if (port != BlockItem::Port::Output) continue; // стартуем только с выходной стороны
|
||
|
||
// Стартуем стрелку из порта
|
||
m_dragFromBlock = b;
|
||
m_dragFromJunction.clear();
|
||
m_dragFromPort = port;
|
||
|
||
auto* a = new ArrowItem();
|
||
addItem(a);
|
||
|
||
ArrowItem::Endpoint from;
|
||
from.block = b;
|
||
from.port = port;
|
||
from.localPos = localPos;
|
||
a->setFrom(from);
|
||
a->setTempEndPoint(scenePos);
|
||
m_dragArrow = a;
|
||
return true;
|
||
}
|
||
|
||
QPointF edgePoint;
|
||
Edge edge = hitTestEdge(scenePos, &edgePoint);
|
||
if (edge != Edge::None && edge != Edge::Right) { // разрешаем старт с левой/верхней/нижней границы
|
||
auto* a = new ArrowItem();
|
||
addItem(a);
|
||
ArrowItem::Endpoint from;
|
||
from.scenePos = edgePoint;
|
||
from.port = BlockItem::Port::Input; // ориентация не важна для свободной точки
|
||
a->setFrom(from);
|
||
a->setTempEndPoint(scenePos);
|
||
m_dragArrow = a;
|
||
m_dragFromBlock.clear();
|
||
m_dragFromJunction.clear();
|
||
m_dragFromPort = BlockItem::Port::Output;
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
bool DiagramScene::tryFinishArrowDrag(const QPointF& scenePos) {
|
||
if (!m_dragArrow) return false;
|
||
auto markSubdiagramOriginTunnel = [this](ArrowItem* arrow) {
|
||
if (!arrow || m_currentBlockId < 0) return;
|
||
auto from = arrow->from();
|
||
if (!from.tunneled) {
|
||
from.tunneled = true;
|
||
arrow->setFrom(from);
|
||
}
|
||
};
|
||
|
||
const auto itemsUnder = items(scenePos);
|
||
// попадание в интерфейсный круг
|
||
for (QGraphicsItem* it : itemsUnder) {
|
||
if (auto* iface = qgraphicsitem_cast<ArrowItem*>(it)) {
|
||
if (!iface->isInterfaceStub()) continue;
|
||
auto edge = iface->interfaceEdge();
|
||
if (!edge) continue;
|
||
// only allow connecting into output-side stubs (targets)
|
||
if (edge->port != BlockItem::Port::Output) continue;
|
||
ArrowItem::Endpoint stubEp = *edge;
|
||
if (!stubEp.scenePos) stubEp.scenePos = scenePos;
|
||
ArrowItem::Endpoint src = m_dragArrow->from();
|
||
|
||
iface->setInterfaceIsStub(false);
|
||
iface->setFrom(src);
|
||
iface->setTo(stubEp);
|
||
iface->setLabel(iface->label());
|
||
iface->setLabelLocked(true);
|
||
iface->finalize();
|
||
|
||
removeItem(m_dragArrow);
|
||
delete m_dragArrow;
|
||
m_dragArrow = nullptr;
|
||
m_dragFromBlock.clear();
|
||
m_dragFromJunction.clear();
|
||
pushSnapshot();
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// сначала пробуем попасть в блок
|
||
for (QGraphicsItem* it : itemsUnder) {
|
||
auto* b = qgraphicsitem_cast<BlockItem*>(it);
|
||
if (!b) continue;
|
||
|
||
BlockItem::Port port{};
|
||
QPointF localPos;
|
||
if (!b->hitTestPort(scenePos, &port, &localPos)) continue;
|
||
|
||
// запретим соединять в тот же самый порт того же блока (упрощение)
|
||
if (b == m_dragFromBlock && port == m_dragFromPort) continue;
|
||
|
||
if (m_dragArrow->isInterface()) {
|
||
auto edge = m_dragArrow->interfaceEdge();
|
||
if (edge) {
|
||
ArrowItem::Endpoint stubEp = *edge;
|
||
if (!stubEp.scenePos) stubEp.scenePos = scenePos;
|
||
ArrowItem::Endpoint blockEp;
|
||
blockEp.block = b;
|
||
blockEp.port = port;
|
||
blockEp.localPos = localPos;
|
||
if (stubEp.port == BlockItem::Port::Output) {
|
||
m_dragArrow->setFrom(blockEp);
|
||
m_dragArrow->setTo(stubEp);
|
||
} else {
|
||
m_dragArrow->setFrom(stubEp);
|
||
m_dragArrow->setTo(blockEp);
|
||
}
|
||
markSubdiagramOriginTunnel(m_dragArrow);
|
||
m_dragArrow->setLabelLocked(true);
|
||
m_dragArrow->finalize();
|
||
}
|
||
} else {
|
||
ArrowItem::Endpoint to;
|
||
to.block = b;
|
||
to.port = port;
|
||
to.localPos = localPos;
|
||
m_dragArrow->setTo(to);
|
||
markSubdiagramOriginTunnel(m_dragArrow);
|
||
m_dragArrow->finalize();
|
||
}
|
||
|
||
m_dragArrow = nullptr;
|
||
m_dragFromBlock.clear();
|
||
m_dragFromJunction.clear();
|
||
pushSnapshot();
|
||
return true;
|
||
}
|
||
|
||
// затем — уже существующий junction
|
||
for (QGraphicsItem* it : itemsUnder) {
|
||
auto* j = qgraphicsitem_cast<JunctionItem*>(it);
|
||
if (!j) continue;
|
||
if (!j->hitTest(scenePos, 8.0)) continue;
|
||
if (j == m_dragFromJunction) continue;
|
||
|
||
if (m_dragArrow->isInterface()) {
|
||
auto edge = m_dragArrow->interfaceEdge();
|
||
if (edge) {
|
||
ArrowItem::Endpoint stubEp = *edge;
|
||
if (!stubEp.scenePos) stubEp.scenePos = scenePos;
|
||
ArrowItem::Endpoint jun;
|
||
jun.junction = j;
|
||
jun.port = BlockItem::Port::Input;
|
||
if (stubEp.port == BlockItem::Port::Output) {
|
||
m_dragArrow->setFrom(jun);
|
||
m_dragArrow->setTo(stubEp);
|
||
} else {
|
||
m_dragArrow->setFrom(stubEp);
|
||
m_dragArrow->setTo(jun);
|
||
}
|
||
markSubdiagramOriginTunnel(m_dragArrow);
|
||
m_dragArrow->setLabelLocked(true);
|
||
m_dragArrow->finalize();
|
||
}
|
||
} else {
|
||
ArrowItem::Endpoint to;
|
||
to.junction = j;
|
||
to.port = BlockItem::Port::Input;
|
||
m_dragArrow->setTo(to);
|
||
markSubdiagramOriginTunnel(m_dragArrow);
|
||
m_dragArrow->finalize();
|
||
}
|
||
|
||
m_dragArrow = nullptr;
|
||
m_dragFromBlock.clear();
|
||
m_dragFromJunction.clear();
|
||
pushSnapshot();
|
||
return true;
|
||
}
|
||
|
||
// Попали в существующую стрелку — создаем junction и расщепляем.
|
||
ArrowItem* targetArrow = nullptr;
|
||
QPointF splitPoint;
|
||
qreal bestDist = 8.0;
|
||
for (QGraphicsItem* it : itemsUnder) {
|
||
auto* arrow = qgraphicsitem_cast<ArrowItem*>(it);
|
||
if (!arrow || arrow == m_dragArrow) continue;
|
||
auto proj = arrow->hitTest(scenePos, bestDist);
|
||
if (proj) {
|
||
bestDist = std::hypot(proj->x() - scenePos.x(), proj->y() - scenePos.y());
|
||
splitPoint = *proj;
|
||
targetArrow = arrow;
|
||
}
|
||
}
|
||
if (targetArrow) {
|
||
auto* junction = new JunctionItem(nullptr, m_nextJunctionId++);
|
||
addItem(junction);
|
||
junction->setPos(splitPoint);
|
||
|
||
ArrowItem::Endpoint mid;
|
||
mid.junction = junction;
|
||
mid.port = BlockItem::Port::Input;
|
||
|
||
const ArrowItem::Endpoint origTo = targetArrow->to();
|
||
targetArrow->setTo(mid);
|
||
targetArrow->finalize();
|
||
|
||
ArrowItem* sourceRoot = targetArrow->labelSourceRoot();
|
||
const QString inheritedLabel = targetArrow->label();
|
||
const bool hideInherited = !inheritedLabel.isEmpty();
|
||
|
||
auto* forward = new ArrowItem();
|
||
addItem(forward);
|
||
forward->setFrom(mid);
|
||
forward->setTo(origTo);
|
||
forward->setLabel(inheritedLabel);
|
||
forward->setLabelHidden(hideInherited);
|
||
forward->setLabelInherited(true);
|
||
forward->setLabelSource(sourceRoot);
|
||
forward->finalize();
|
||
|
||
m_dragArrow->setTo(mid);
|
||
m_dragArrow->setLabel(inheritedLabel);
|
||
m_dragArrow->setLabelHidden(hideInherited);
|
||
m_dragArrow->setLabelInherited(true);
|
||
m_dragArrow->setLabelSource(sourceRoot);
|
||
markSubdiagramOriginTunnel(m_dragArrow);
|
||
m_dragArrow->finalize();
|
||
|
||
m_dragArrow = nullptr;
|
||
m_dragFromBlock.clear();
|
||
m_dragFromJunction.clear();
|
||
pushSnapshot();
|
||
return true;
|
||
}
|
||
|
||
// если не попали в блок — попробуем закрепиться на границе сцены
|
||
QPointF edgePoint;
|
||
Edge edge = hitTestEdge(scenePos, &edgePoint);
|
||
if (edge != Edge::None) {
|
||
const auto fromEp = m_dragArrow->from();
|
||
if (fromEp.scenePos.has_value() && !fromEp.block && !fromEp.junction) {
|
||
// запрет: нельзя начинать и заканчивать стрелку на границе
|
||
return false;
|
||
}
|
||
// правая граница может быть только приемником (уже гарантировано)
|
||
ArrowItem::Endpoint to;
|
||
to.scenePos = edgePoint;
|
||
to.port = BlockItem::Port::Input;
|
||
m_dragArrow->setTo(to);
|
||
markSubdiagramOriginTunnel(m_dragArrow);
|
||
m_dragArrow->finalize();
|
||
m_dragArrow = nullptr;
|
||
m_dragFromBlock.clear();
|
||
m_dragFromJunction.clear();
|
||
pushSnapshot();
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
bool DiagramScene::tryBranchAtArrow(const QPointF& scenePos) {
|
||
ArrowItem* targetArrow = nullptr;
|
||
QPointF splitPoint;
|
||
qreal bestDist = 8.0;
|
||
|
||
const auto itemsUnder = items(scenePos);
|
||
for (QGraphicsItem* it : itemsUnder) {
|
||
auto* arrow = qgraphicsitem_cast<ArrowItem*>(it);
|
||
if (!arrow || arrow == m_dragArrow) continue;
|
||
auto proj = arrow->hitTest(scenePos, bestDist);
|
||
if (proj) {
|
||
bestDist = std::hypot(proj->x() - scenePos.x(), proj->y() - scenePos.y());
|
||
splitPoint = *proj;
|
||
targetArrow = arrow;
|
||
}
|
||
}
|
||
|
||
if (!targetArrow) return false;
|
||
|
||
auto* junction = new JunctionItem(nullptr, m_nextJunctionId++);
|
||
addItem(junction);
|
||
junction->setPos(splitPoint);
|
||
|
||
ArrowItem::Endpoint mid;
|
||
mid.junction = junction;
|
||
mid.port = BlockItem::Port::Input;
|
||
|
||
const ArrowItem::Endpoint origTo = targetArrow->to();
|
||
targetArrow->setTo(mid);
|
||
targetArrow->finalize();
|
||
|
||
ArrowItem* sourceRoot = targetArrow->labelSourceRoot();
|
||
const QString inheritedLabel = targetArrow->label();
|
||
const bool hideInherited = !inheritedLabel.isEmpty();
|
||
|
||
auto* forward = new ArrowItem();
|
||
addItem(forward);
|
||
forward->setFrom(mid);
|
||
forward->setTo(origTo);
|
||
forward->setLabel(inheritedLabel);
|
||
forward->setLabelHidden(hideInherited);
|
||
forward->setLabelInherited(true);
|
||
forward->setLabelSource(sourceRoot);
|
||
forward->finalize();
|
||
|
||
auto* branch = new ArrowItem();
|
||
addItem(branch);
|
||
branch->setFrom(mid);
|
||
branch->setLabel(inheritedLabel);
|
||
branch->setLabelHidden(hideInherited);
|
||
branch->setLabelInherited(true);
|
||
branch->setLabelSource(sourceRoot);
|
||
branch->setTempEndPoint(scenePos);
|
||
m_dragArrow = branch;
|
||
m_dragFromBlock.clear();
|
||
m_dragFromJunction = junction;
|
||
m_dragFromPort = BlockItem::Port::Output;
|
||
return true;
|
||
}
|
||
|
||
DiagramScene::Snapshot DiagramScene::captureSnapshot() const {
|
||
Snapshot s;
|
||
QString state = m_meta.value("state").toString();
|
||
if (state.isEmpty()) {
|
||
if (m_meta.value("publication", false).toBool()) state = "publication";
|
||
else if (m_meta.value("recommended", false).toBool()) state = "recommended";
|
||
else if (m_meta.value("draft", false).toBool()) state = "draft";
|
||
else state = "working";
|
||
}
|
||
s.state = state;
|
||
s.hasState = true;
|
||
|
||
QSet<BlockItem*> blockSet;
|
||
QSet<JunctionItem*> junctionSet;
|
||
for (QGraphicsItem* it : items()) {
|
||
if (auto* b = qgraphicsitem_cast<BlockItem*>(it)) blockSet.insert(b);
|
||
if (auto* j = qgraphicsitem_cast<JunctionItem*>(it)) junctionSet.insert(j);
|
||
}
|
||
|
||
auto encodeEp = [&](const ArrowItem::Endpoint& ep) {
|
||
Snapshot::Endpoint out;
|
||
if (ep.block && blockSet.contains(ep.block) && ep.block->scene() == this) {
|
||
out.kind = Snapshot::Endpoint::Kind::Block;
|
||
out.id = ep.block->id();
|
||
out.port = ep.port;
|
||
if (ep.localPos) out.localPos = *ep.localPos;
|
||
} else if (ep.junction && junctionSet.contains(ep.junction) && ep.junction->scene() == this) {
|
||
out.kind = Snapshot::Endpoint::Kind::Junction;
|
||
out.id = ep.junction->id();
|
||
out.port = ep.port;
|
||
} else if (ep.scenePos) {
|
||
out.kind = Snapshot::Endpoint::Kind::Scene;
|
||
out.scenePos = *ep.scenePos;
|
||
out.port = ep.port;
|
||
}
|
||
out.tunneled = ep.tunneled;
|
||
return out;
|
||
};
|
||
|
||
for (QGraphicsItem* it : items()) {
|
||
if (auto* b = qgraphicsitem_cast<BlockItem*>(it)) {
|
||
const bool hasDecomp = childHasContent(b->id());
|
||
b->setHasDecomposition(hasDecomp);
|
||
Snapshot::Block blk;
|
||
blk.id = b->id();
|
||
blk.title = b->title();
|
||
blk.pos = b->pos();
|
||
blk.hasDecomp = hasDecomp;
|
||
blk.number = b->number();
|
||
blk.price = b->price();
|
||
if (auto col = b->customColor()) {
|
||
blk.color = col->name();
|
||
}
|
||
s.blocks.push_back(blk);
|
||
}
|
||
}
|
||
|
||
for (QGraphicsItem* it : items()) {
|
||
if (auto* j = qgraphicsitem_cast<JunctionItem*>(it)) {
|
||
s.junctions.push_back({j->id(), j->pos()});
|
||
}
|
||
}
|
||
|
||
for (QGraphicsItem* it : items()) {
|
||
if (auto* a = qgraphicsitem_cast<ArrowItem*>(it)) {
|
||
Snapshot::Arrow ar;
|
||
ar.from = encodeEp(a->from());
|
||
ar.to = encodeEp(a->to());
|
||
if (ar.from.kind == Snapshot::Endpoint::Kind::None || ar.to.kind == Snapshot::Endpoint::Kind::None) {
|
||
continue; // skip incomplete arrows
|
||
}
|
||
ar.bend = a->bendOffset();
|
||
ar.top = a->topOffset();
|
||
ar.bottom = a->bottomOffset();
|
||
ar.label = a->label();
|
||
ar.labelHidden = a->isLabelHidden();
|
||
ar.labelOffset = a->labelOffset();
|
||
ar.isInterface = a->isInterface();
|
||
ar.isInterfaceStub = a->isInterfaceStub();
|
||
ar.labelLocked = a->isLabelLocked();
|
||
ar.callMechanism = a->isCallMechanism();
|
||
ar.callRefId = a->callRefId();
|
||
if (a->interfaceEdge()) {
|
||
ar.interfaceEdge = encodeEp(*a->interfaceEdge());
|
||
}
|
||
if (auto col = a->customColor()) {
|
||
ar.color = col->name();
|
||
}
|
||
s.arrows.push_back(std::move(ar));
|
||
}
|
||
}
|
||
return s;
|
||
}
|
||
|
||
void DiagramScene::restoreSnapshot(const Snapshot& snap, bool resetHistoryState) {
|
||
m_restoringSnapshot = true;
|
||
cancelCurrentDrag();
|
||
clear();
|
||
m_headerFooter = nullptr;
|
||
ensureFrame();
|
||
ensureHeaderFooter();
|
||
|
||
m_nextBlockId = 1;
|
||
m_nextJunctionId = 1;
|
||
|
||
QHash<int, BlockItem*> blockMap;
|
||
QHash<int, JunctionItem*> junctionMap;
|
||
|
||
for (const auto& b : snap.blocks) {
|
||
auto* blk = createBlockWithId(b.pos, b.id, b.title);
|
||
blk->setHasDecomposition(b.hasDecomp || childHasContent(b.id));
|
||
if (!b.number.isEmpty()) {
|
||
blk->setNumber(b.number);
|
||
} else {
|
||
blk->setNumber(assignNumber(blk));
|
||
}
|
||
if (b.price.has_value()) {
|
||
blk->setPrice(b.price);
|
||
}
|
||
if (b.color.has_value()) {
|
||
const QColor col(*b.color);
|
||
if (col.isValid()) blk->setCustomColor(col);
|
||
}
|
||
blockMap.insert(b.id, blk);
|
||
m_nextBlockId = std::max(m_nextBlockId, b.id + 1);
|
||
}
|
||
for (const auto& j : snap.junctions) {
|
||
m_nextJunctionId = std::max(m_nextJunctionId, j.id + 1);
|
||
auto* jun = new JunctionItem(nullptr, j.id);
|
||
addItem(jun);
|
||
jun->setPos(j.pos);
|
||
junctionMap.insert(j.id, jun);
|
||
}
|
||
|
||
auto decodeEp = [&](const Snapshot::Endpoint& ep) {
|
||
ArrowItem::Endpoint out;
|
||
switch (ep.kind) {
|
||
case Snapshot::Endpoint::Kind::Block:
|
||
out.block = blockMap.value(ep.id, nullptr);
|
||
out.port = ep.port;
|
||
out.localPos = ep.localPos;
|
||
break;
|
||
case Snapshot::Endpoint::Kind::Junction:
|
||
out.junction = junctionMap.value(ep.id, nullptr);
|
||
out.port = ep.port;
|
||
break;
|
||
case Snapshot::Endpoint::Kind::Scene:
|
||
out.scenePos = ep.scenePos;
|
||
out.port = ep.port;
|
||
break;
|
||
case Snapshot::Endpoint::Kind::None:
|
||
break;
|
||
}
|
||
out.tunneled = ep.tunneled;
|
||
return out;
|
||
};
|
||
|
||
for (const auto& a : snap.arrows) {
|
||
auto* ar = new ArrowItem();
|
||
addItem(ar);
|
||
const auto fromEp = decodeEp(a.from);
|
||
const auto toEp = decodeEp(a.to);
|
||
if (a.isInterface && a.interfaceEdge.kind != Snapshot::Endpoint::Kind::None) {
|
||
ar->setInterfaceStub(decodeEp(a.interfaceEdge), a.label);
|
||
ar->setInterfaceIsStub(a.isInterfaceStub);
|
||
if (!a.isInterfaceStub) {
|
||
ar->setFrom(fromEp);
|
||
ar->setTo(toEp);
|
||
}
|
||
} else {
|
||
ar->setFrom(fromEp);
|
||
ar->setTo(toEp);
|
||
}
|
||
ar->setOffsets(a.bend, a.top, a.bottom);
|
||
ar->setLabel(a.label);
|
||
ar->setLabelOffset(a.labelOffset);
|
||
ar->setLabelHidden(a.labelHidden);
|
||
ar->setLabelInherited(a.labelInherited);
|
||
ar->setLabelLocked(a.labelLocked);
|
||
ar->setCallMechanism(a.callMechanism);
|
||
ar->setCallRefId(a.callRefId);
|
||
if (a.color.has_value()) {
|
||
const QColor col(*a.color);
|
||
if (col.isValid()) ar->setCustomColor(col);
|
||
}
|
||
ar->finalize();
|
||
}
|
||
|
||
if (snap.hasState) {
|
||
const QString state = snap.state.isEmpty() ? QStringLiteral("working") : snap.state;
|
||
m_meta["state"] = state;
|
||
m_meta["working"] = (state == "working");
|
||
m_meta["draft"] = (state == "draft");
|
||
m_meta["recommended"] = (state == "recommended");
|
||
m_meta["publication"] = (state == "publication");
|
||
}
|
||
|
||
m_restoringSnapshot = false;
|
||
if (m_headerFooter) {
|
||
m_headerFooter->setMeta(m_meta);
|
||
m_headerFooter->update();
|
||
}
|
||
updateCallMechanismLabels();
|
||
if (resetHistoryState) {
|
||
resetHistory(captureSnapshot());
|
||
}
|
||
}
|
||
|
||
void DiagramScene::pushSnapshot() {
|
||
if (m_restoringSnapshot) return;
|
||
Snapshot s = captureSnapshot();
|
||
if (m_historyIndex + 1 < m_history.size()) {
|
||
m_history.resize(m_historyIndex + 1);
|
||
}
|
||
m_history.push_back(std::move(s));
|
||
m_historyIndex = m_history.size() - 1;
|
||
// refresh dog-ears based on known children
|
||
for (QGraphicsItem* it : items()) {
|
||
if (auto* blk = qgraphicsitem_cast<BlockItem*>(it)) {
|
||
blk->setHasDecomposition(childHasContent(blk->id()));
|
||
}
|
||
}
|
||
emit changed();
|
||
}
|
||
|
||
void DiagramScene::scheduleSnapshot() {
|
||
if (m_restoringSnapshot || m_snapshotScheduled) return;
|
||
m_snapshotScheduled = true;
|
||
QTimer::singleShot(0, this, [this]{
|
||
m_snapshotScheduled = false;
|
||
pushSnapshot();
|
||
updateCallMechanismLabels();
|
||
});
|
||
}
|
||
|
||
void DiagramScene::connectBlockSignals(BlockItem* b) {
|
||
if (!b) return;
|
||
connect(b, &BlockItem::titleChanged, this, [this]{ updateCallMechanismLabels(); });
|
||
connect(b, &BlockItem::numberChanged, this, [this]{ updateCallMechanismLabels(); });
|
||
}
|
||
|
||
void DiagramScene::updateCallMechanismLabels() {
|
||
QHash<int, BlockItem*> blockById;
|
||
for (QGraphicsItem* it : items()) {
|
||
if (auto* b = qgraphicsitem_cast<BlockItem*>(it)) {
|
||
blockById.insert(b->id(), b);
|
||
}
|
||
}
|
||
for (QGraphicsItem* it : items()) {
|
||
auto* a = qgraphicsitem_cast<ArrowItem*>(it);
|
||
if (!a || !a->isCallMechanism()) continue;
|
||
BlockItem* ref = blockById.value(a->callRefId(), nullptr);
|
||
if (!ref) continue;
|
||
const QString num = ref->number();
|
||
const QString title = ref->title();
|
||
const QString label = num.isEmpty() ? title : QStringLiteral("%1 %2").arg(num, title);
|
||
a->setLabel(label);
|
||
a->setLabelHidden(false);
|
||
}
|
||
}
|
||
|
||
void DiagramScene::purgeBrokenCallMechanisms() {
|
||
QSet<BlockItem*> blocks;
|
||
for (QGraphicsItem* it : items()) {
|
||
if (auto* b = qgraphicsitem_cast<BlockItem*>(it)) blocks.insert(b);
|
||
}
|
||
QVector<ArrowItem*> toRemove;
|
||
for (QGraphicsItem* it : items()) {
|
||
if (auto* a = qgraphicsitem_cast<ArrowItem*>(it)) {
|
||
if (!a->isCallMechanism()) continue;
|
||
auto from = a->from();
|
||
auto to = a->to();
|
||
bool ok = true;
|
||
if (from.block && !blocks.contains(from.block)) ok = false;
|
||
if (to.block && !blocks.contains(to.block)) ok = false;
|
||
if (!ok) toRemove.push_back(a);
|
||
}
|
||
}
|
||
for (ArrowItem* a : toRemove) {
|
||
removeItem(a);
|
||
delete a;
|
||
}
|
||
}
|
||
|
||
void DiagramScene::undo() {
|
||
if (m_historyIndex <= 0) return;
|
||
m_historyIndex -= 1;
|
||
restoreSnapshot(m_history[m_historyIndex], false);
|
||
}
|
||
|
||
bool DiagramScene::goDownIntoSelected() {
|
||
const auto sel = selectedItems();
|
||
if (sel.size() != 1) return false;
|
||
auto* b = qgraphicsitem_cast<BlockItem*>(sel.first());
|
||
if (!b) return false;
|
||
|
||
return goDownIntoBlock(b);
|
||
}
|
||
|
||
bool DiagramScene::goUp() {
|
||
if (m_hierarchy.isEmpty()) return false;
|
||
const int childBlockId = m_currentBlockId;
|
||
Snapshot child = captureSnapshot();
|
||
if (m_currentBlockId >= 0) {
|
||
m_children[currentPathKey()] = child;
|
||
}
|
||
const HierNode parent = m_hierarchy.takeLast();
|
||
if (m_currentBlockId >= 0) {
|
||
for (QGraphicsItem* it : items()) {
|
||
if (auto* blk = qgraphicsitem_cast<BlockItem*>(it)) {
|
||
if (blk->id() == m_currentBlockId) {
|
||
blk->setHasDecomposition(true);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
m_currentBlockId = parent.blockId;
|
||
m_currentPrefix = parent.prefix;
|
||
restoreSnapshot(parent.snapshot);
|
||
|
||
if (childBlockId >= 0) {
|
||
BlockItem* parentBlock = nullptr;
|
||
for (QGraphicsItem* it : items()) {
|
||
if (auto* blk = qgraphicsitem_cast<BlockItem*>(it)) {
|
||
if (blk->id() == childBlockId) {
|
||
parentBlock = blk;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (parentBlock) {
|
||
const QRectF frame = m_contentRect.isNull() ? sceneRect() : m_contentRect;
|
||
auto normClamp = [](qreal v) { return std::clamp(v, 0.0, 1.0); };
|
||
auto frameEdgePoint = [&](BlockItem::Port port, qreal t) {
|
||
switch (port) {
|
||
case BlockItem::Port::Input: return QPointF(frame.left(), frame.top() + t * frame.height());
|
||
case BlockItem::Port::Output: return QPointF(frame.right(), frame.top() + t * frame.height());
|
||
case BlockItem::Port::Control: return QPointF(frame.left() + t * frame.width(), frame.top());
|
||
case BlockItem::Port::Mechanism: return QPointF(frame.left() + t * frame.width(), frame.bottom());
|
||
}
|
||
return QPointF();
|
||
};
|
||
auto edgeKey = [](const BlockItem::Port port, const QPointF& p) {
|
||
return QStringLiteral("%1:%2:%3").arg(int(port)).arg(p.x(), 0, 'f', 1).arg(p.y(), 0, 'f', 1);
|
||
};
|
||
auto labelKey = [](const BlockItem::Port port, const QString& lbl) {
|
||
return QStringLiteral("%1:%2").arg(int(port)).arg(lbl);
|
||
};
|
||
QSet<QString> tunneledEdgeKeys;
|
||
QSet<QString> tunneledLabelKeys;
|
||
for (const auto& ar : child.arrows) {
|
||
if (!(ar.isInterface && ar.isInterfaceStub)) continue;
|
||
if (!ar.interfaceEdge.tunneled) continue;
|
||
tunneledEdgeKeys.insert(edgeKey(ar.interfaceEdge.port, ar.interfaceEdge.scenePos.value_or(QPointF())));
|
||
if (!ar.label.isEmpty()) tunneledLabelKeys.insert(labelKey(ar.interfaceEdge.port, ar.label));
|
||
}
|
||
|
||
const QRectF br = parentBlock->mapRectToScene(parentBlock->boundingRect());
|
||
auto tY = [&](const QPointF& p) { return normClamp((p.y() - br.top()) / br.height()); };
|
||
auto tX = [&](const QPointF& p) { return normClamp((p.x() - br.left()) / br.width()); };
|
||
auto scenePosFor = [](const ArrowItem::Endpoint& ep) {
|
||
if (ep.block) {
|
||
if (ep.localPos) return ep.block->mapToScene(*ep.localPos);
|
||
return ep.block->portScenePos(ep.port);
|
||
}
|
||
if (ep.junction) return ep.junction->scenePos();
|
||
if (ep.scenePos) return *ep.scenePos;
|
||
return QPointF();
|
||
};
|
||
|
||
for (QGraphicsItem* it : items()) {
|
||
auto* a = qgraphicsitem_cast<ArrowItem*>(it);
|
||
if (!a) continue;
|
||
auto f = a->from();
|
||
auto t = a->to();
|
||
bool changed = false;
|
||
if (t.block == parentBlock) {
|
||
const QPointF portPos = scenePosFor(t);
|
||
const qreal tt = (t.port == BlockItem::Port::Control || t.port == BlockItem::Port::Mechanism) ? tX(portPos) : tY(portPos);
|
||
const QPointF edgeScene = frameEdgePoint(t.port, tt);
|
||
const bool tun = tunneledEdgeKeys.contains(edgeKey(t.port, edgeScene)) ||
|
||
(!a->label().isEmpty() && tunneledLabelKeys.contains(labelKey(t.port, a->label())));
|
||
if (t.tunneled != tun) { t.tunneled = tun; changed = true; }
|
||
}
|
||
if (f.block == parentBlock) {
|
||
const QPointF portPos = scenePosFor(f);
|
||
const qreal tt = (f.port == BlockItem::Port::Control || f.port == BlockItem::Port::Mechanism) ? tX(portPos) : tY(portPos);
|
||
const QPointF edgeScene = frameEdgePoint(f.port, tt);
|
||
const bool tun = tunneledEdgeKeys.contains(edgeKey(f.port, edgeScene)) ||
|
||
(!a->label().isEmpty() && tunneledLabelKeys.contains(labelKey(f.port, a->label())));
|
||
if (f.tunneled != tun) { f.tunneled = tun; changed = true; }
|
||
}
|
||
if (changed) {
|
||
a->setFrom(f);
|
||
a->setTo(t);
|
||
a->finalize();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
bool DiagramScene::goDownIntoBlock(BlockItem* b) {
|
||
if (!b) return false;
|
||
const QString childKey = childKeyFor(b->id());
|
||
Snapshot current = captureSnapshot();
|
||
m_hierarchy.push_back({m_currentBlockId, current, m_currentPrefix});
|
||
m_currentBlockId = b->id();
|
||
m_currentPrefix = b->number().isEmpty() ? m_currentPrefix : b->number();
|
||
Snapshot child;
|
||
if (m_children.contains(childKey)) {
|
||
child = m_children.value(childKey);
|
||
QVector<Snapshot::Arrow> preserved;
|
||
preserved.reserve(child.arrows.size());
|
||
for (const auto& ar : child.arrows) {
|
||
if (!(ar.isInterface && ar.isInterfaceStub)) preserved.push_back(ar);
|
||
}
|
||
child.arrows = std::move(preserved);
|
||
} else {
|
||
QString state = m_meta.value("state").toString();
|
||
if (state.isEmpty()) {
|
||
if (m_meta.value("publication", false).toBool()) state = "publication";
|
||
else if (m_meta.value("recommended", false).toBool()) state = "recommended";
|
||
else if (m_meta.value("draft", false).toBool()) state = "draft";
|
||
else state = "working";
|
||
}
|
||
child.state = state;
|
||
child.hasState = true;
|
||
}
|
||
|
||
// build interface stubs from current parent connections
|
||
const QRectF frame = m_contentRect.isNull() ? sceneRect() : m_contentRect;
|
||
auto normClamp = [](qreal v) { return std::clamp(v, 0.0, 1.0); };
|
||
auto frameEdgePoint = [&](BlockItem::Port port, qreal t, bool inward) {
|
||
const qreal inset = 80.0;
|
||
switch (port) {
|
||
case BlockItem::Port::Input: {
|
||
QPointF edge(frame.left(), frame.top() + t * frame.height());
|
||
return inward ? edge + QPointF(inset, 0) : edge;
|
||
}
|
||
case BlockItem::Port::Output: {
|
||
QPointF edge(frame.right(), frame.top() + t * frame.height());
|
||
return inward ? edge - QPointF(inset, 0) : edge;
|
||
}
|
||
case BlockItem::Port::Control: {
|
||
QPointF edge(frame.left() + t * frame.width(), frame.top());
|
||
return inward ? edge + QPointF(0, inset) : edge;
|
||
}
|
||
case BlockItem::Port::Mechanism: {
|
||
QPointF edge(frame.left() + t * frame.width(), frame.bottom());
|
||
return inward ? edge - QPointF(0, inset) : edge;
|
||
}
|
||
}
|
||
return QPointF();
|
||
};
|
||
|
||
auto scenePosFor = [](const ArrowItem::Endpoint& ep) {
|
||
if (ep.block) {
|
||
if (ep.localPos) return ep.block->mapToScene(*ep.localPos);
|
||
return ep.block->portScenePos(ep.port);
|
||
}
|
||
if (ep.junction) return ep.junction->scenePos();
|
||
if (ep.scenePos) return *ep.scenePos;
|
||
return QPointF();
|
||
};
|
||
|
||
auto makeSceneEndpoint = [](const QPointF& pos, BlockItem::Port port) {
|
||
Snapshot::Endpoint ep;
|
||
ep.kind = Snapshot::Endpoint::Kind::Scene;
|
||
ep.scenePos = pos;
|
||
ep.port = port;
|
||
return ep;
|
||
};
|
||
|
||
auto edgeKey = [](const Snapshot::Endpoint& ep) {
|
||
QPointF p = ep.scenePos.value_or(QPointF());
|
||
// coarse rounding to avoid mismatch on re-entry
|
||
return QStringLiteral("%1:%2:%3").arg(int(ep.port)).arg(p.x(), 0, 'f', 1).arg(p.y(), 0, 'f', 1);
|
||
};
|
||
auto labelKey = [](const Snapshot::Arrow& ar) {
|
||
return QStringLiteral("%1:%2").arg(int(ar.interfaceEdge.port)).arg(ar.label);
|
||
};
|
||
|
||
QSet<QString> usedInterfaces;
|
||
for (const auto& ar : child.arrows) {
|
||
if (!ar.isInterface) continue;
|
||
if (!ar.label.isEmpty()) {
|
||
usedInterfaces.insert(labelKey(ar));
|
||
} else {
|
||
usedInterfaces.insert(edgeKey(ar.interfaceEdge));
|
||
}
|
||
}
|
||
|
||
const QRectF br = b->mapRectToScene(b->boundingRect());
|
||
auto tY = [&](const QPointF& p) { return normClamp((p.y() - br.top()) / br.height()); };
|
||
auto tX = [&](const QPointF& p) { return normClamp((p.x() - br.left()) / br.width()); };
|
||
|
||
for (QGraphicsItem* it : items()) {
|
||
auto* a = qgraphicsitem_cast<ArrowItem*>(it);
|
||
if (!a) continue;
|
||
const auto f = a->from();
|
||
const auto t = a->to();
|
||
if (t.block == b) {
|
||
const QPointF portPos = scenePosFor(t);
|
||
const qreal tt = (t.port == BlockItem::Port::Control || t.port == BlockItem::Port::Mechanism) ? tX(portPos) : tY(portPos);
|
||
const QPointF edgeScene = frameEdgePoint(t.port, tt, false);
|
||
Snapshot::Arrow ar;
|
||
ar.from = makeSceneEndpoint(edgeScene, t.port);
|
||
ar.to = ar.from;
|
||
ar.label = t.tunneled ? QString() : a->label();
|
||
ar.isInterface = true;
|
||
ar.isInterfaceStub = true;
|
||
ar.labelLocked = true;
|
||
ar.interfaceEdge = ar.from;
|
||
ar.from.tunneled = t.tunneled;
|
||
ar.to.tunneled = t.tunneled;
|
||
ar.interfaceEdge.tunneled = t.tunneled;
|
||
const QString key = ar.label.isEmpty() ? edgeKey(ar.interfaceEdge) : labelKey(ar);
|
||
if (!usedInterfaces.contains(key)) {
|
||
usedInterfaces.insert(key);
|
||
child.arrows.push_back(ar);
|
||
}
|
||
} else if (f.block == b) {
|
||
const QPointF portPos = scenePosFor(f);
|
||
const qreal tt = (f.port == BlockItem::Port::Control || f.port == BlockItem::Port::Mechanism) ? tX(portPos) : tY(portPos);
|
||
const QPointF edgeScene = frameEdgePoint(f.port, tt, false);
|
||
Snapshot::Arrow ar;
|
||
ar.from = makeSceneEndpoint(edgeScene, f.port);
|
||
ar.to = ar.from;
|
||
ar.label = f.tunneled ? QString() : a->label();
|
||
ar.isInterface = true;
|
||
ar.isInterfaceStub = true;
|
||
ar.labelLocked = true;
|
||
ar.interfaceEdge = ar.to;
|
||
ar.from.tunneled = f.tunneled;
|
||
ar.to.tunneled = f.tunneled;
|
||
ar.interfaceEdge.tunneled = f.tunneled;
|
||
const QString key = ar.label.isEmpty() ? edgeKey(ar.interfaceEdge) : labelKey(ar);
|
||
if (!usedInterfaces.contains(key)) {
|
||
usedInterfaces.insert(key);
|
||
child.arrows.push_back(ar);
|
||
}
|
||
}
|
||
}
|
||
restoreSnapshot(child);
|
||
return true;
|
||
}
|
||
|
||
void DiagramScene::mousePressEvent(QGraphicsSceneMouseEvent* e) {
|
||
if (e->button() == Qt::MiddleButton) {
|
||
const auto itemsUnder = items(e->scenePos());
|
||
for (QGraphicsItem* it : itemsUnder) {
|
||
if (auto* b = qgraphicsitem_cast<BlockItem*>(it)) {
|
||
if (goDownIntoBlock(b)) {
|
||
e->accept();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
if (goUp()) {
|
||
e->accept();
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (e->button() == Qt::LeftButton) {
|
||
if (e->modifiers().testFlag(Qt::ControlModifier)) {
|
||
if (tryBranchAtArrow(e->scenePos())) {
|
||
e->accept();
|
||
return;
|
||
}
|
||
}
|
||
// если кликнули на порт — начинаем протягивание стрелки
|
||
if (tryStartArrowDrag(e->scenePos(), e->modifiers())) {
|
||
e->accept();
|
||
return;
|
||
}
|
||
// начало перемещения существующих элементов — запомним позиции
|
||
m_pressPositions.clear();
|
||
const auto sel = selectedItems();
|
||
for (QGraphicsItem* it : sel) {
|
||
if (it->flags() & QGraphicsItem::ItemIsMovable) {
|
||
m_pressPositions.insert(it, it->pos());
|
||
}
|
||
}
|
||
m_itemDragActive = !m_pressPositions.isEmpty();
|
||
}
|
||
|
||
if (e->button() == Qt::RightButton) {
|
||
// правой кнопкой — добавить блок
|
||
const QRectF r = m_contentRect.isNull() ? sceneRect() : m_contentRect;
|
||
if (r.contains(e->scenePos())) {
|
||
createBlockAt(e->scenePos());
|
||
e->accept();
|
||
return;
|
||
}
|
||
}
|
||
|
||
QGraphicsScene::mousePressEvent(e);
|
||
}
|
||
|
||
void DiagramScene::mouseMoveEvent(QGraphicsSceneMouseEvent* e) {
|
||
if (m_dragInterfaceStub && (e->buttons() & Qt::LeftButton)) {
|
||
if (auto edge = m_dragInterfaceStub->interfaceEdge()) {
|
||
const QRectF r = m_contentRect.isNull() ? sceneRect() : m_contentRect;
|
||
QPointF p = e->scenePos();
|
||
switch (edge->port) {
|
||
case BlockItem::Port::Input:
|
||
p.setX(r.left());
|
||
p.setY(std::clamp(p.y(), r.top(), r.bottom()));
|
||
break;
|
||
case BlockItem::Port::Output:
|
||
p.setX(r.right());
|
||
p.setY(std::clamp(p.y(), r.top(), r.bottom()));
|
||
break;
|
||
case BlockItem::Port::Control:
|
||
p.setY(r.top());
|
||
p.setX(std::clamp(p.x(), r.left(), r.right()));
|
||
break;
|
||
case BlockItem::Port::Mechanism:
|
||
p.setY(r.bottom());
|
||
p.setX(std::clamp(p.x(), r.left(), r.right()));
|
||
break;
|
||
}
|
||
ArrowItem::Endpoint ep = *edge;
|
||
ep.scenePos = p;
|
||
m_dragInterfaceStub->setInterfaceStub(ep, m_dragInterfaceStub->label());
|
||
}
|
||
e->accept();
|
||
return;
|
||
}
|
||
if (m_dragArrow) {
|
||
m_dragArrow->setTempEndPoint(e->scenePos());
|
||
e->accept();
|
||
return;
|
||
}
|
||
QGraphicsScene::mouseMoveEvent(e);
|
||
}
|
||
|
||
void DiagramScene::mouseReleaseEvent(QGraphicsSceneMouseEvent* e) {
|
||
if (m_dragInterfaceStub && e->button() == Qt::LeftButton) {
|
||
m_dragInterfaceStub = nullptr;
|
||
pushSnapshot();
|
||
e->accept();
|
||
return;
|
||
}
|
||
if (m_dragArrow && e->button() == Qt::LeftButton) {
|
||
if (!tryFinishArrowDrag(e->scenePos())) {
|
||
// не получилось — удаляем временную стрелку
|
||
cancelCurrentDrag();
|
||
}
|
||
e->accept();
|
||
}
|
||
if (m_itemDragActive && e->button() == Qt::LeftButton) {
|
||
maybeSnapshotMovedItems();
|
||
m_itemDragActive = false;
|
||
}
|
||
purgeBrokenCallMechanisms();
|
||
QGraphicsScene::mouseReleaseEvent(e);
|
||
}
|
||
|
||
void DiagramScene::keyPressEvent(QKeyEvent* e) {
|
||
if (e->key() == Qt::Key_Escape) {
|
||
cancelCurrentDrag();
|
||
e->accept();
|
||
return;
|
||
}
|
||
if (e->key() == Qt::Key_Delete || e->key() == Qt::Key_Backspace) {
|
||
deleteSelection();
|
||
e->accept();
|
||
return;
|
||
}
|
||
if (e->matches(QKeySequence::Undo)) {
|
||
undo();
|
||
e->accept();
|
||
return;
|
||
}
|
||
if ((e->key() == Qt::Key_Down || e->key() == Qt::Key_PageDown) && e->modifiers().testFlag(Qt::ControlModifier)) {
|
||
if (goDownIntoSelected()) {
|
||
e->accept();
|
||
return;
|
||
}
|
||
}
|
||
if ((e->key() == Qt::Key_Up || e->key() == Qt::Key_PageUp) && e->modifiers().testFlag(Qt::ControlModifier)) {
|
||
if (goUp()) {
|
||
e->accept();
|
||
return;
|
||
}
|
||
}
|
||
QGraphicsScene::keyPressEvent(e);
|
||
}
|
||
|
||
bool DiagramScene::hasCallMechanism(const BlockItem* target) const {
|
||
if (!target) return false;
|
||
const auto all = items();
|
||
for (QGraphicsItem* it : all) {
|
||
if (auto* a = qgraphicsitem_cast<ArrowItem*>(it)) {
|
||
if (!a->isCallMechanism()) continue;
|
||
const auto from = a->from();
|
||
if (from.block && from.block == target) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool DiagramScene::startCallMechanism(BlockItem* origin, BlockItem* refBlock, const QString& label) {
|
||
if (!origin || !refBlock) return false;
|
||
if (origin == refBlock) return false;
|
||
if (hasCallMechanism(origin)) return false;
|
||
cancelCurrentDrag();
|
||
auto* a = new ArrowItem();
|
||
addItem(a);
|
||
ArrowItem::Endpoint from;
|
||
from.scenePos = origin->portScenePos(BlockItem::Port::Mechanism) + QPointF(0, 40);
|
||
from.port = BlockItem::Port::Mechanism;
|
||
ArrowItem::Endpoint to;
|
||
to.block = origin;
|
||
to.port = BlockItem::Port::Mechanism;
|
||
a->setFrom(from);
|
||
a->setTo(to);
|
||
a->setCallMechanism(true);
|
||
a->setCallRefId(refBlock->id());
|
||
a->setLabel(label);
|
||
a->setLabelHidden(false);
|
||
a->setLabelLocked(true);
|
||
a->setLabelInherited(false);
|
||
a->finalize();
|
||
pushSnapshot();
|
||
return true;
|
||
}
|
||
|
||
void DiagramScene::cancelCurrentDrag() {
|
||
if (m_dragArrow) {
|
||
if (m_dragArrow->isInterface()) {
|
||
m_dragArrow->resetInterfaceStub();
|
||
} else {
|
||
removeItem(m_dragArrow);
|
||
delete m_dragArrow;
|
||
}
|
||
m_dragArrow = nullptr;
|
||
m_dragFromBlock.clear();
|
||
m_dragFromJunction.clear();
|
||
}
|
||
m_dragInterfaceStub = nullptr;
|
||
}
|
||
|
||
void DiagramScene::deleteSelection() {
|
||
if (m_dragArrow) {
|
||
cancelCurrentDrag();
|
||
return;
|
||
}
|
||
|
||
QSet<QGraphicsItem*> toDelete;
|
||
const auto sel = selectedItems();
|
||
for (QGraphicsItem* it : sel) {
|
||
if (qgraphicsitem_cast<BlockItem*>(it) || qgraphicsitem_cast<JunctionItem*>(it) || qgraphicsitem_cast<ArrowItem*>(it)) {
|
||
toDelete.insert(it);
|
||
}
|
||
}
|
||
|
||
// also delete arrows connected to selected blocks/junctions
|
||
for (QGraphicsItem* it : items()) {
|
||
auto* arrow = qgraphicsitem_cast<ArrowItem*>(it);
|
||
if (!arrow) continue;
|
||
const auto f = arrow->from();
|
||
const auto t = arrow->to();
|
||
if ((f.block && toDelete.contains(f.block)) ||
|
||
(t.block && toDelete.contains(t.block)) ||
|
||
(f.junction && toDelete.contains(f.junction)) ||
|
||
(t.junction && toDelete.contains(t.junction))) {
|
||
toDelete.insert(arrow);
|
||
}
|
||
}
|
||
|
||
QVector<ArrowItem*> arrowsToDelete;
|
||
QVector<QGraphicsItem*> othersToDelete;
|
||
for (QGraphicsItem* it : toDelete) {
|
||
if (it->type() == ArrowItem::Type) {
|
||
if (auto* a = static_cast<ArrowItem*>(it)) arrowsToDelete.push_back(a);
|
||
} else {
|
||
othersToDelete.push_back(it);
|
||
}
|
||
}
|
||
for (ArrowItem* a : arrowsToDelete) {
|
||
if (a->isInterface()) {
|
||
if (a->isInterfaceStub()) {
|
||
auto edge = a->interfaceEdge();
|
||
if (edge) {
|
||
edge->tunneled = true;
|
||
a->setInterfaceStub(*edge, QString());
|
||
a->setLabelHidden(true);
|
||
a->setLabelInherited(false);
|
||
a->setLabelSource(nullptr);
|
||
a->setInterfaceIsStub(true);
|
||
a->setLabelLocked(true);
|
||
a->finalize();
|
||
} else {
|
||
a->resetInterfaceStub();
|
||
}
|
||
} else {
|
||
a->resetInterfaceStub();
|
||
}
|
||
} else {
|
||
delete a;
|
||
}
|
||
}
|
||
for (QGraphicsItem* it : othersToDelete) {
|
||
delete it;
|
||
}
|
||
|
||
if (!toDelete.isEmpty()) {
|
||
purgeBrokenCallMechanisms();
|
||
pushSnapshot();
|
||
}
|
||
}
|
||
|
||
void DiagramScene::requestSnapshot() {
|
||
pushSnapshot();
|
||
}
|
||
|
||
void DiagramScene::maybeSnapshotMovedItems() {
|
||
bool moved = false;
|
||
for (auto it = m_pressPositions.cbegin(); it != m_pressPositions.cend(); ++it) {
|
||
if (!it.key()) continue;
|
||
const QPointF cur = it.key()->pos();
|
||
const QPointF old = it.value();
|
||
if (!qFuzzyCompare(cur.x(), old.x()) || !qFuzzyCompare(cur.y(), old.y())) {
|
||
moved = true;
|
||
break;
|
||
}
|
||
}
|
||
m_pressPositions.clear();
|
||
if (moved) pushSnapshot();
|
||
}
|
||
|
||
bool DiagramScene::childHasContent(int blockId) const {
|
||
const QString key = childKeyFor(blockId);
|
||
if (!m_children.contains(key)) return false;
|
||
const auto& snap = m_children.value(key);
|
||
if (!snap.blocks.isEmpty()) return true;
|
||
if (!snap.junctions.isEmpty()) return true;
|
||
for (const auto& ar : snap.arrows) {
|
||
if (!(ar.isInterface && ar.isInterfaceStub)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
QString DiagramScene::currentPathKey() const {
|
||
QStringList parts;
|
||
for (const auto& node : m_hierarchy) {
|
||
if (node.blockId >= 0) parts.push_back(QString::number(node.blockId));
|
||
}
|
||
if (m_currentBlockId >= 0) parts.push_back(QString::number(m_currentBlockId));
|
||
return parts.join('/');
|
||
}
|
||
|
||
QString DiagramScene::childKeyFor(int blockId) const {
|
||
const QString base = currentPathKey();
|
||
if (base.isEmpty()) return QString::number(blockId);
|
||
return base + "/" + QString::number(blockId);
|
||
}
|
||
|
||
QString DiagramScene::assignNumber(BlockItem* b) {
|
||
const QString prefix = m_currentPrefix;
|
||
const auto all = items();
|
||
|
||
if (prefix.isEmpty()) {
|
||
int rootCount = 0;
|
||
for (QGraphicsItem* it : all) {
|
||
auto* blk = qgraphicsitem_cast<BlockItem*>(it);
|
||
if (!blk || blk == b) continue;
|
||
const QString num = blk->number();
|
||
if (num.isEmpty() || num.contains('.')) continue;
|
||
rootCount++;
|
||
}
|
||
return "A" + QString("0").repeated(rootCount + 1);
|
||
}
|
||
|
||
if (prefix == "A0") {
|
||
int maxIdx = 0;
|
||
for (QGraphicsItem* it : all) {
|
||
auto* blk = qgraphicsitem_cast<BlockItem*>(it);
|
||
if (!blk || blk == b) continue;
|
||
const QString num = blk->number();
|
||
if (num.startsWith("A") && !num.contains('.')) {
|
||
bool ok = false;
|
||
int idx = num.mid(1).toInt(&ok);
|
||
if (ok) maxIdx = std::max(maxIdx, idx);
|
||
}
|
||
}
|
||
int next = std::max(1, maxIdx + 1);
|
||
return "A" + QString::number(next);
|
||
}
|
||
|
||
int maxIdx = 0;
|
||
const QString want = prefix + ".";
|
||
for (QGraphicsItem* it : all) {
|
||
auto* blk = qgraphicsitem_cast<BlockItem*>(it);
|
||
if (!blk || blk == b) continue;
|
||
const QString num = blk->number();
|
||
if (num.startsWith(want)) {
|
||
bool ok = false;
|
||
int idx = num.mid(want.size()).toInt(&ok);
|
||
if (ok) maxIdx = std::max(maxIdx, idx);
|
||
}
|
||
}
|
||
return prefix + "." + QString::number(maxIdx + 1);
|
||
}
|
||
|
||
void DiagramScene::propagateLabelFrom(ArrowItem* root) {
|
||
if (!root || !root->scene()) return;
|
||
ArrowItem* source = root->labelSourceRoot();
|
||
const QString lbl = source ? source->label() : QString();
|
||
for (QGraphicsItem* it : root->scene()->items()) {
|
||
auto* a = qgraphicsitem_cast<ArrowItem*>(it);
|
||
if (!a || a == source) continue;
|
||
if (!a->isLabelInherited()) continue;
|
||
if (a->labelSourceRoot() == source) {
|
||
a->setLabel(lbl);
|
||
a->setLabelHidden(!lbl.isEmpty());
|
||
}
|
||
}
|
||
}
|
||
|
||
void DiagramScene::resetHistory(const Snapshot& base) {
|
||
m_history.clear();
|
||
m_history.push_back(base);
|
||
m_historyIndex = 0;
|
||
}
|
||
|
||
QJsonObject endpointToJson(const DiagramScene::Snapshot::Endpoint& ep) {
|
||
QJsonObject o;
|
||
o["kind"] = int(ep.kind);
|
||
o["id"] = ep.id;
|
||
o["port"] = int(ep.port);
|
||
o["tunneled"] = ep.tunneled;
|
||
if (ep.localPos) {
|
||
o["localPos"] = QJsonArray{ep.localPos->x(), ep.localPos->y()};
|
||
}
|
||
if (ep.scenePos) {
|
||
o["scenePos"] = QJsonArray{ep.scenePos->x(), ep.scenePos->y()};
|
||
}
|
||
return o;
|
||
}
|
||
|
||
DiagramScene::Snapshot::Endpoint endpointFromJson(const QJsonObject& o) {
|
||
DiagramScene::Snapshot::Endpoint ep;
|
||
ep.kind = DiagramScene::Snapshot::Endpoint::Kind(o.value("kind").toInt());
|
||
ep.id = o.value("id").toInt(-1);
|
||
ep.port = BlockItem::Port(o.value("port").toInt(int(BlockItem::Port::Input)));
|
||
ep.tunneled = o.value("tunneled").toBool(false);
|
||
if (o.contains("localPos")) {
|
||
const auto arr = o.value("localPos").toArray();
|
||
if (arr.size() == 2) ep.localPos = QPointF(arr[0].toDouble(), arr[1].toDouble());
|
||
}
|
||
if (o.contains("scenePos")) {
|
||
const auto arr = o.value("scenePos").toArray();
|
||
if (arr.size() == 2) ep.scenePos = QPointF(arr[0].toDouble(), arr[1].toDouble());
|
||
}
|
||
return ep;
|
||
}
|
||
|
||
QVariantMap DiagramScene::exportToVariant() {
|
||
auto snapshotToVariant = [](const Snapshot& snap) -> QVariantMap {
|
||
QJsonObject root;
|
||
QJsonArray blocks;
|
||
for (const auto& b : snap.blocks) {
|
||
QJsonObject o;
|
||
o["id"] = b.id;
|
||
o["title"] = b.title;
|
||
o["pos"] = QJsonArray{b.pos.x(), b.pos.y()};
|
||
o["hasDecomp"] = b.hasDecomp;
|
||
o["number"] = b.number;
|
||
if (b.price.has_value()) o["price"] = *b.price;
|
||
if (b.color.has_value()) o["color"] = *b.color;
|
||
blocks.append(o);
|
||
}
|
||
root["blocks"] = blocks;
|
||
|
||
QJsonArray junctions;
|
||
for (const auto& j : snap.junctions) {
|
||
QJsonObject o;
|
||
o["id"] = j.id;
|
||
o["pos"] = QJsonArray{j.pos.x(), j.pos.y()};
|
||
junctions.append(o);
|
||
}
|
||
root["junctions"] = junctions;
|
||
|
||
QJsonArray arrows;
|
||
for (const auto& a : snap.arrows) {
|
||
QJsonObject o;
|
||
o["from"] = endpointToJson(a.from);
|
||
o["to"] = endpointToJson(a.to);
|
||
o["bend"] = a.bend;
|
||
o["top"] = a.top;
|
||
o["bottom"] = a.bottom;
|
||
o["label"] = a.label;
|
||
o["labelOffset"] = QJsonArray{a.labelOffset.x(), a.labelOffset.y()};
|
||
o["labelHidden"] = a.labelHidden;
|
||
o["labelInherited"] = a.labelInherited;
|
||
o["isInterface"] = a.isInterface;
|
||
o["isInterfaceStub"] = a.isInterfaceStub;
|
||
o["labelLocked"] = a.labelLocked;
|
||
o["interfaceEdge"] = endpointToJson(a.interfaceEdge);
|
||
o["callMechanism"] = a.callMechanism;
|
||
o["callRefId"] = a.callRefId;
|
||
if (a.color.has_value()) o["color"] = *a.color;
|
||
arrows.append(o);
|
||
}
|
||
root["arrows"] = arrows;
|
||
if (snap.hasState) root["state"] = snap.state;
|
||
return root.toVariantMap();
|
||
};
|
||
|
||
if (m_currentBlockId >= 0) {
|
||
m_children[currentPathKey()] = captureSnapshot();
|
||
}
|
||
|
||
Snapshot rootSnap = captureSnapshot();
|
||
if (!m_hierarchy.isEmpty()) {
|
||
rootSnap = m_hierarchy.first().snapshot;
|
||
}
|
||
|
||
QVariantMap out;
|
||
out["diagram"] = snapshotToVariant(rootSnap);
|
||
|
||
QJsonObject children;
|
||
for (auto it = m_children.cbegin(); it != m_children.cend(); ++it) {
|
||
children[it.key()] = QJsonObject::fromVariantMap(snapshotToVariant(it.value()));
|
||
}
|
||
out["children"] = children.toVariantMap();
|
||
return out;
|
||
}
|
||
|
||
bool DiagramScene::importFromVariant(const QVariantMap& map) {
|
||
const QVariantMap rootMeta = map.value("meta").toMap();
|
||
auto toSnap = [&rootMeta](const QVariantMap& vm, DiagramScene* self) -> std::optional<Snapshot> {
|
||
QJsonObject root = QJsonObject::fromVariantMap(vm);
|
||
if (!root.contains("blocks") || !root.contains("arrows")) return std::nullopt;
|
||
Snapshot snap;
|
||
if (root.contains("state")) {
|
||
snap.state = root.value("state").toString();
|
||
snap.hasState = true;
|
||
} else {
|
||
QString fallback = rootMeta.value("state").toString();
|
||
if (fallback.isEmpty()) {
|
||
if (rootMeta.value("publication", false).toBool()) fallback = "publication";
|
||
else if (rootMeta.value("recommended", false).toBool()) fallback = "recommended";
|
||
else if (rootMeta.value("draft", false).toBool()) fallback = "draft";
|
||
else if (rootMeta.value("working", false).toBool()) fallback = "working";
|
||
}
|
||
if (!fallback.isEmpty()) {
|
||
snap.state = fallback;
|
||
snap.hasState = true;
|
||
}
|
||
}
|
||
for (const auto& vb : root.value("blocks").toArray()) {
|
||
const auto o = vb.toObject();
|
||
const auto posArr = o.value("pos").toArray();
|
||
if (posArr.size() != 2) continue;
|
||
DiagramScene::Snapshot::Block blk;
|
||
blk.id = o.value("id").toInt();
|
||
blk.title = o.value("title").toString();
|
||
blk.pos = QPointF(posArr[0].toDouble(), posArr[1].toDouble());
|
||
blk.hasDecomp = o.value("hasDecomp").toBool(false);
|
||
blk.number = o.value("number").toString();
|
||
if (o.contains("price") && !o.value("price").isNull()) {
|
||
blk.price = o.value("price").toDouble();
|
||
}
|
||
const QString blkColor = o.value("color").toString();
|
||
if (!blkColor.isEmpty()) blk.color = blkColor;
|
||
snap.blocks.push_back(blk);
|
||
}
|
||
for (const auto& vj : root.value("junctions").toArray()) {
|
||
const auto o = vj.toObject();
|
||
const auto posArr = o.value("pos").toArray();
|
||
if (posArr.size() != 2) continue;
|
||
snap.junctions.push_back({o.value("id").toInt(), QPointF(posArr[0].toDouble(), posArr[1].toDouble())});
|
||
}
|
||
for (const auto& va : root.value("arrows").toArray()) {
|
||
const auto o = va.toObject();
|
||
Snapshot::Arrow ar;
|
||
ar.from = endpointFromJson(o.value("from").toObject());
|
||
ar.to = endpointFromJson(o.value("to").toObject());
|
||
ar.bend = o.value("bend").toDouble();
|
||
ar.top = o.value("top").toDouble();
|
||
ar.bottom = o.value("bottom").toDouble();
|
||
ar.label = o.value("label").toString();
|
||
const auto lo = o.value("labelOffset").toArray();
|
||
if (lo.size() == 2) ar.labelOffset = QPointF(lo[0].toDouble(), lo[1].toDouble());
|
||
ar.labelHidden = o.value("labelHidden").toBool(false);
|
||
ar.labelInherited = o.value("labelInherited").toBool(false);
|
||
ar.isInterface = o.value("isInterface").toBool(false);
|
||
ar.isInterfaceStub = o.value("isInterfaceStub").toBool(false);
|
||
ar.labelLocked = o.value("labelLocked").toBool(false);
|
||
ar.interfaceEdge = endpointFromJson(o.value("interfaceEdge").toObject());
|
||
ar.callMechanism = o.value("callMechanism").toBool(false);
|
||
ar.callRefId = o.value("callRefId").toInt(-1);
|
||
const QString arrowColor = o.value("color").toString();
|
||
if (!arrowColor.isEmpty()) ar.color = arrowColor;
|
||
snap.arrows.push_back(ar);
|
||
}
|
||
return snap;
|
||
};
|
||
|
||
auto diagIt = map.find("diagram");
|
||
if (diagIt == map.end()) return false;
|
||
auto maybeRoot = toSnap(diagIt.value().toMap(), this);
|
||
if (!maybeRoot) return false;
|
||
m_children.clear();
|
||
auto childrenIt = map.find("children");
|
||
if (childrenIt != map.end()) {
|
||
const QVariantMap childrenMap = childrenIt.value().toMap();
|
||
for (auto it = childrenMap.cbegin(); it != childrenMap.cend(); ++it) {
|
||
auto maybeChild = toSnap(it.value().toMap(), this);
|
||
if (maybeChild) m_children[it.key()] = *maybeChild;
|
||
}
|
||
}
|
||
|
||
m_hierarchy.clear();
|
||
m_currentPrefix.clear();
|
||
m_currentBlockId = -1;
|
||
restoreSnapshot(*maybeRoot);
|
||
const int currentId = QJsonObject::fromVariantMap(diagIt.value().toMap()).value("currentBlockId").toInt(-1);
|
||
m_currentBlockId = currentId;
|
||
return true;
|
||
}
|
||
|
||
void DiagramScene::ensureFrame() {
|
||
bool exists = false;
|
||
for (QGraphicsItem* it : items()) {
|
||
if (it->data(0).toString() == QStringLiteral("static-frame")) {
|
||
exists = true;
|
||
if (auto* rect = qgraphicsitem_cast<QGraphicsRectItem*>(it)) {
|
||
rect->setRect(m_contentRect.isNull() ? sceneRect() : m_contentRect);
|
||
const bool darkMode = m_meta.value("resolvedDarkMode",
|
||
m_meta.value("darkMode", false)).toBool();
|
||
const QColor frameColor = darkMode ? QColor(220, 220, 220) : QColor(80, 80, 80);
|
||
rect->setPen(QPen(frameColor, 1.5, Qt::DashLine));
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
if (!exists) {
|
||
const bool darkMode = m_meta.value("resolvedDarkMode",
|
||
m_meta.value("darkMode", false)).toBool();
|
||
const QColor frameColor = darkMode ? QColor(220, 220, 220) : QColor(80, 80, 80);
|
||
auto* frame = addRect(m_contentRect.isNull() ? sceneRect() : m_contentRect,
|
||
QPen(frameColor, 1.5, Qt::DashLine),
|
||
Qt::NoBrush);
|
||
frame->setZValue(-5);
|
||
frame->setEnabled(false);
|
||
frame->setAcceptedMouseButtons(Qt::NoButton);
|
||
frame->setData(0, QVariant(QStringLiteral("static-frame")));
|
||
}
|
||
}
|
||
|
||
void DiagramScene::ensureHeaderFooter() {
|
||
if (!m_headerFooter) {
|
||
m_headerFooter = new HeaderFooterItem(this);
|
||
addItem(m_headerFooter);
|
||
}
|
||
m_headerFooter->setPageRect(sceneRect());
|
||
m_headerFooter->setMeta(m_meta);
|
||
}
|