From 891067ac38a9f6f50926d885e2b5b11a9216c656 Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Mon, 2 Mar 2026 00:33:40 +0300 Subject: [PATCH] minor patches --- src/MainWindow.cpp | 181 ++++++++++++++++++++++++++--- src/MainWindow.h | 3 +- src/items/ArrowItem.cpp | 123 ++++++++++++++++---- src/items/ArrowItem.h | 8 ++ src/items/BlockItem.cpp | 12 +- src/items/DiagramScene.cpp | 180 +++++++++++++++++++++++++++- src/items/DiagramScene.h | 3 + src/main.cpp | 47 ++++++-- src/plugins/color/ColorsPlugin.cpp | 4 +- 9 files changed, 506 insertions(+), 55 deletions(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index e3c1323..e3fc4b3 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -68,6 +69,23 @@ static QPageLayout::Orientation pageOrientationFromMeta(const QVariantMap& meta) return QPageLayout::Landscape; } +static QVariantMap stripPerDiagramState(QVariantMap meta) { + meta.remove("state"); + meta.remove("working"); + meta.remove("draft"); + meta.remove("recommended"); + meta.remove("publication"); + return meta; +} + +static QVariantMap mergeWithDiagramState(QVariantMap globalMeta, const QVariantMap& diagramMeta) { + for (const char* key : {"state", "working", "draft", "recommended", "publication"}) { + const QString k = QString::fromLatin1(key); + if (diagramMeta.contains(k)) globalMeta[k] = diagramMeta.value(k); + } + return globalMeta; +} + MainWindow::MainWindow(const QString& startupPath, QWidget* parent) : QMainWindow(parent) { @@ -91,10 +109,10 @@ void MainWindow::setupUi() { m_scene = new DiagramScene(this); connect(m_scene, &DiagramScene::changed, this, [this]{ markDirty(true); }); connect(m_scene, &DiagramScene::metaChanged, this, [this](const QVariantMap& meta){ - m_welcomeState = meta; + m_welcomeState = stripPerDiagramState(meta); markDirty(true); }); - m_scene->applyMeta(m_welcomeState); + m_scene->applyMeta(mergeWithDiagramState(m_welcomeState, m_scene->meta())); } m_view = new QGraphicsView(m_scene, this); m_view->setRenderHint(QPainter::Antialiasing, true); @@ -155,6 +173,12 @@ void MainWindow::setupActions() { }); viewMenu->addAction(actZoomOut); + auto* actNodeTree = new QAction(tr("Node tree…"), this); + connect(actNodeTree, &QAction::triggered, this, [this]{ + showNodeTreeDialog(); + }); + viewMenu->addAction(actNodeTree); + m_actDarkMode = new QAction(tr("Dark mode"), this); m_actDarkMode->setCheckable(true); m_actDarkMode->setChecked(m_welcomeState.value("darkMode", false).toBool()); @@ -173,7 +197,7 @@ void MainWindow::setupActions() { m_welcomeState["resolvedDarkMode"] = effectiveDarkMode(); m_actDarkMode->setEnabled(!followSystem); applyAppPalette(m_welcomeState.value("resolvedDarkMode").toBool()); - m_scene->applyMeta(m_welcomeState); + m_scene->applyMeta(mergeWithDiagramState(m_welcomeState, m_scene->meta())); if (mark) markDirty(true); }; connect(m_actDarkMode, &QAction::toggled, this, [applyModes]{ applyModes(true); }); @@ -237,7 +261,7 @@ void MainWindow::setupActions() { QVariantMap root; root["diagram"] = exported.value("diagram"); root["children"] = exported.value("children"); - root["meta"] = m_welcomeState; + root["meta"] = stripPerDiagramState(m_welcomeState); QJsonDocument doc = QJsonDocument::fromVariant(root); QFile f(outPath); if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { @@ -272,15 +296,18 @@ void MainWindow::setupActions() { scope->addItems({tr("Current diagram"), tr("All diagrams")}); auto* pageNumbers = new QCheckBox(tr("Number pages in footer (NUMBER field)"), &dlg); pageNumbers->setChecked(false); + auto* forceLight = new QCheckBox(tr("Use a light theme regardless of the document theme"), &dlg); + forceLight->setChecked(true); form->addRow(tr("Export scope:"), scope); form->addRow(QString(), pageNumbers); + form->addRow(QString(), forceLight); layout->addLayout(form); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg); connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); layout->addWidget(buttons); if (dlg.exec() != QDialog::Accepted) return; - exportPdf(scope->currentIndex() == 1, pageNumbers->isChecked()); + exportPdf(scope->currentIndex() == 1, pageNumbers->isChecked(), forceLight->isChecked()); }); connect(actCallMech, &QAction::triggered, this, [this]{ @@ -415,11 +442,11 @@ bool MainWindow::showWelcome() { ? QString::fromUtf8("\u20BD") : currencySymbolForLocale(loc); auto* currency = new QComboBox(units); - currency->setEditable(false); + currency->setEditable(true); currency->addItem(curSym); if (curSym != "$") currency->addItem("$"); const QString existingCur = m_welcomeState.value("currency").toString(); - if (!existingCur.isEmpty() && currency->findText(existingCur) >= 0) { + if (!existingCur.isEmpty()) { currency->setCurrentText(existingCur); } else { currency->setCurrentIndex(0); @@ -507,10 +534,10 @@ void MainWindow::resetScene() { m_scene = new DiagramScene(this); connect(m_scene, &DiagramScene::changed, this, [this]{ markDirty(true); }); connect(m_scene, &DiagramScene::metaChanged, this, [this](const QVariantMap& meta){ - m_welcomeState = meta; + m_welcomeState = stripPerDiagramState(meta); markDirty(true); }); - m_scene->applyMeta(m_welcomeState); + m_scene->applyMeta(mergeWithDiagramState(m_welcomeState, m_scene->meta())); if (m_view) m_view->setScene(m_scene); if (old) old->deleteLater(); } @@ -518,8 +545,8 @@ void MainWindow::resetScene() { void MainWindow::newDiagram() { if (!showWelcome()) return; resetScene(); - m_scene->createBlockAt(QPointF(-200, -50))->setPos(-300, -150); - m_scene->createBlockAt(QPointF(200, -50))->setPos(200, -150); + const QRectF bounds = m_scene->contentRect().isNull() ? m_scene->sceneRect() : m_scene->contentRect(); + m_scene->createBlockAt(bounds.center()); m_currentFile.clear(); markDirty(false); } @@ -540,7 +567,8 @@ bool MainWindow::promptStartup() { } if (!showWelcome()) return false; resetScene(); - m_scene->createBlockAt(QPointF(-100, -50)); + const QRectF bounds = m_scene->contentRect().isNull() ? m_scene->sceneRect() : m_scene->contentRect(); + m_scene->createBlockAt(bounds.center()); m_currentFile.clear(); markDirty(false); return true; @@ -566,7 +594,7 @@ bool MainWindow::loadDiagramFromPath(const QString& path) { return false; } if (root.contains("meta")) { - m_welcomeState = root.value("meta").toMap(); + m_welcomeState = stripPerDiagramState(root.value("meta").toMap()); } if (m_actDarkMode && m_actFollowSystemTheme) { const QSignalBlocker b2(m_actDarkMode); @@ -577,13 +605,13 @@ bool MainWindow::loadDiagramFromPath(const QString& path) { } m_welcomeState["resolvedDarkMode"] = effectiveDarkMode(); applyAppPalette(m_welcomeState.value("resolvedDarkMode").toBool()); - m_scene->applyMeta(m_welcomeState); + m_scene->applyMeta(mergeWithDiagramState(m_welcomeState, m_scene->meta())); m_currentFile = path; markDirty(false); return true; } -bool MainWindow::exportPdf(bool allDiagrams, bool numberPages) { +bool MainWindow::exportPdf(bool allDiagrams, bool numberPages, bool forceLightTheme) { QString path = QFileDialog::getSaveFileName(this, tr("Export to PDF"), QString(), tr(kPdfFileFilter)); if (path.isEmpty()) return false; if (QFileInfo(path).suffix().isEmpty()) path += ".pdf"; @@ -612,6 +640,10 @@ bool MainWindow::exportPdf(bool allDiagrams, bool numberPages) { } else { meta.remove("footerNumberOverride"); } + if (forceLightTheme) { + meta["darkMode"] = false; + meta["resolvedDarkMode"] = false; + } ds->applyMeta(meta); } const QRect target = writer.pageLayout().paintRectPixels(writer.resolution()); @@ -648,14 +680,14 @@ bool MainWindow::exportPdf(bool allDiagrams, bool numberPages) { collectKeys(QString(), exported.value("diagram").toMap()); DiagramScene tempScene; - tempScene.applyMeta(m_welcomeState); + tempScene.applyMeta(mergeWithDiagramState(m_welcomeState, tempScene.meta())); QVariantMap mapRoot; mapRoot["diagram"] = exported.value("diagram"); mapRoot["children"] = children; bool first = true; if (tempScene.importFromVariant(mapRoot)) { - tempScene.applyMeta(m_welcomeState); + tempScene.applyMeta(mergeWithDiagramState(m_welcomeState, tempScene.meta())); renderScene(&tempScene, false); first = false; } @@ -674,7 +706,7 @@ bool MainWindow::exportPdf(bool allDiagrams, bool numberPages) { mapOne["children"] = relChildren; if (!tempScene.importFromVariant(mapOne)) continue; - tempScene.applyMeta(m_welcomeState); + tempScene.applyMeta(mergeWithDiagramState(m_welcomeState, tempScene.meta())); renderScene(&tempScene, !first); first = false; } @@ -721,6 +753,119 @@ bool MainWindow::openDiagramPath(const QString& path) { return loadDiagramFromPath(path); } +void MainWindow::showNodeTreeDialog() { + if (!m_scene) return; + const QVariantMap exported = m_scene->exportToVariant(); + const QVariantMap rootDiagram = exported.value("diagram").toMap(); + const QVariantMap children = exported.value("children").toMap(); + + auto* dlg = new QDialog(this); + dlg->setWindowTitle(tr("Node tree")); + auto* layout = new QVBoxLayout(dlg); + auto* tree = new QTreeWidget(dlg); + tree->setHeaderHidden(true); + tree->setExpandsOnDoubleClick(false); + + auto itemText = [](const QVariantMap& block) { + const QString num = block.value("number").toString(); + const QString title = block.value("title").toString(); + if (num.isEmpty()) return title; + if (title.isEmpty()) return num; + return QStringLiteral("%1 %2").arg(num, title); + }; + + std::function addSubtree; + addSubtree = [&](QTreeWidgetItem* parent, const QVariantMap& diagram, const QString& prefix) { + const QVariantList blockList = diagram.value("blocks").toList(); + QVector blocks; + blocks.reserve(blockList.size()); + for (const QVariant& v : blockList) blocks.push_back(v); + std::sort(blocks.begin(), blocks.end(), [](const QVariant& a, const QVariant& b) { + const QVariantMap ma = a.toMap(); + const QVariantMap mb = b.toMap(); + const QString na = ma.value("number").toString(); + const QString nb = mb.value("number").toString(); + if (na == nb) return ma.value("id", -1).toInt() < mb.value("id", -1).toInt(); + return na.localeAwareCompare(nb) < 0; + }); + for (const QVariant& vb : blocks) { + const QVariantMap block = vb.toMap(); + const int id = block.value("id", -1).toInt(); + auto* it = new QTreeWidgetItem(); + it->setText(0, itemText(block)); + it->setData(0, Qt::UserRole, prefix); // diagram path containing this block + it->setData(0, Qt::UserRole + 1, id); // block id in that diagram + parent->addChild(it); + if (id < 0) continue; + const QString key = prefix.isEmpty() ? QString::number(id) : (prefix + "/" + QString::number(id)); + if (children.contains(key)) { + addSubtree(it, children.value(key).toMap(), key); + } + } + }; + + const QString rootName = m_welcomeState.value("title").toString().isEmpty() + ? tr("Model") + : m_welcomeState.value("title").toString(); + auto* root = new QTreeWidgetItem(tree); + root->setText(0, QStringLiteral("A-0 %1").arg(rootName)); + root->setData(0, Qt::UserRole, QString()); + root->setData(0, Qt::UserRole + 1, -1); + tree->addTopLevelItem(root); + addSubtree(root, rootDiagram, QString()); + tree->expandAll(); + + connect(tree, &QTreeWidget::itemDoubleClicked, this, [this, tree](QTreeWidgetItem* item, int) { + if (!item || !m_scene) return; + const QString path = item->data(0, Qt::UserRole).toString(); + const int blockId = item->data(0, Qt::UserRole + 1).toInt(); + + while (m_scene->goUp()) {} + + const QStringList parts = path.split('/', Qt::SkipEmptyParts); + for (const QString& part : parts) { + bool ok = false; + const int id = part.toInt(&ok); + if (!ok) break; + BlockItem* target = nullptr; + for (QGraphicsItem* gi : m_scene->items()) { + if (auto* b = qgraphicsitem_cast(gi)) { + if (b->id() == id) { + target = b; + break; + } + } + } + if (!target || !m_scene->goDownIntoBlock(target)) break; + } + + m_scene->clearSelection(); + if (blockId >= 0) { + for (QGraphicsItem* gi : m_scene->items()) { + if (auto* b = qgraphicsitem_cast(gi)) { + if (b->id() == blockId) { + b->setSelected(true); + if (m_view) m_view->centerOn(b); + break; + } + } + } + } else if (m_view) { + m_view->centerOn(m_scene->sceneRect().center()); + } + tree->setCurrentItem(item); + }); + + layout->addWidget(tree); + auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, dlg); + connect(buttons, &QDialogButtonBox::rejected, dlg, &QDialog::reject); + connect(buttons, &QDialogButtonBox::accepted, dlg, &QDialog::accept); + layout->addWidget(buttons); + dlg->resize(640, 520); + dlg->exec(); + dlg->deleteLater(); +} + void MainWindow::closeEvent(QCloseEvent* e) { if (!m_dirty) { e->accept(); diff --git a/src/MainWindow.h b/src/MainWindow.h index d413087..ef6a95c 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -40,10 +40,11 @@ private: void newDiagram(); bool promptStartup(); bool loadDiagramFromPath(const QString& path); - bool exportPdf(bool allDiagrams, bool numberPages); + bool exportPdf(bool allDiagrams, bool numberPages, bool forceLightTheme); bool effectiveDarkMode() const; void applyAppPalette(bool darkMode); void autoInitialsFromAuthor(QLineEdit* author, QLineEdit* initials); + void showNodeTreeDialog(); void markDirty(bool dirty); void updateWindowTitle(); }; diff --git a/src/items/ArrowItem.cpp b/src/items/ArrowItem.cpp index f2682eb..1625f0e 100644 --- a/src/items/ArrowItem.cpp +++ b/src/items/ArrowItem.cpp @@ -30,6 +30,24 @@ static QRectF labelDragHitRect(const QGraphicsSimpleTextItem* item) { // Generous hit area makes label dragging much less fiddly. return item->sceneBoundingRect().adjusted(-16, -10, 16, 10); } + +QRectF ArrowItem::boundingRect() const { + QRectF r = QGraphicsPathItem::boundingRect(); + if (m_labelItem && m_labelItem->isVisible()) { + r = r.united(mapRectFromScene(labelDragHitRect(m_labelItem))); + } + return r; +} + +QPainterPath ArrowItem::shape() const { + QPainterPath s = QGraphicsPathItem::shape(); + if (m_labelItem && m_labelItem->isVisible()) { + QPainterPath labelZone; + labelZone.addRect(mapRectFromScene(labelDragHitRect(m_labelItem))); + s = s.united(labelZone); + } + return s; +} QColor ArrowItem::s_lineColor = QColor(10, 10, 10); QColor ArrowItem::s_textColor = QColor(10, 10, 10); @@ -162,6 +180,18 @@ void ArrowItem::setLabelLocked(bool locked) { m_labelLocked = locked; } +void ArrowItem::setFromTunneled(bool v) { + if (m_from.tunneled == v) return; + m_from.tunneled = v; + updatePath(); +} + +void ArrowItem::setToTunneled(bool v) { + if (m_to.tunneled == v) return; + m_to.tunneled = v; + updatePath(); +} + bool ArrowItem::adjustEndpoint(Endpoint& ep, const QPointF& delta) { if (ep.block) { auto seg = ep.block->portSegment(ep.port); @@ -484,7 +514,7 @@ QPointF ArrowItem::portDirection(const Endpoint& ep, bool isFrom, const QPointF& const qreal db = std::abs(p.y() - r.bottom()); const qreal m = std::min({dl, dr, dt, db}); if (m == dl) return isFrom ? QPointF(1,0) : QPointF(-1,0); - if (m == dr) return isFrom ? QPointF(-1,0) : QPointF(1,0); // right edge is target-only + if (m == dr) return QPointF(-1,0); // right-edge targets must be approached from inside (left) if (m == dt) return isFrom ? QPointF(0,1) : QPointF(0,-1); if (m == db) return isFrom ? QPointF(0,-1) : QPointF(0,1); } @@ -540,13 +570,6 @@ QVector ArrowItem::computePolyline() const { : QPointF(0, ((a - b).y() >= 0 ? 1 : -1)), QPointF(-1,0)) : portDirection(m_to, false, QPointF(-1,0), scene()); - // ensure right border always behaves like an input (approach from left) - if (!m_hasTempEnd && m_to.scenePos && scene()) { - const qreal rightX = scene()->sceneRect().right(); - if (almostEqual(m_to.scenePos->x(), rightX)) { - dirB = QPointF(-1, 0); - } - } auto expandRect = [](const QRectF& r, qreal m) { return QRectF(r.x()-m, r.y()-m, r.width()+2*m, r.height()+2*m); @@ -645,7 +668,22 @@ void ArrowItem::updateLabelItem(const QVector& pts) { } const QPointF p = scenePortPos(*m_interfaceEdge); const QRectF br = m_labelItem->boundingRect(); - m_labelItem->setPos(p + m_labelOffset + QPointF(10, -br.height() * 0.5)); + QPointF sideOffset; + switch (m_interfaceEdge->port) { + case BlockItem::Port::Input: + sideOffset = QPointF(10, -br.height() * 0.5); // right of left frame side + break; + case BlockItem::Port::Output: + sideOffset = QPointF(-10 - br.width(), -br.height() * 0.5); // left of right frame side + break; + case BlockItem::Port::Control: + sideOffset = QPointF(-br.width() * 0.5, 10); // below top frame side + break; + case BlockItem::Port::Mechanism: + sideOffset = QPointF(-br.width() * 0.5, -10 - br.height()); // above bottom frame side + break; + } + m_labelItem->setPos(p + m_labelOffset + sideOffset); m_labelItem->setVisible(true); return; } @@ -689,7 +727,12 @@ void ArrowItem::updateLabelItem(const QVector& pts) { // Обновляем маршрут ортогонально, добавляя/схлопывая сегменты при необходимости. void ArrowItem::updatePath() { QPen themedPen = pen(); - themedPen.setColor(m_customColor.value_or(s_lineColor)); + QColor lineColor = m_customColor.value_or(s_lineColor); + if (m_labelDragging) { + lineColor = lineColor.lighter(150); + } + themedPen.setColor(lineColor); + themedPen.setWidthF(m_labelDragging ? 2.2 : 1.4); if (m_isCallMechanism) { themedPen.setStyle(Qt::DashLine); } @@ -702,18 +745,19 @@ void ArrowItem::updatePath() { QVector pts = computePolyline(); m_lastPolyline = pts; + const QVector& drawPts = pts; QPainterPath p; const bool stubOnly = m_isInterface && m_interfaceStubOnly; - if (!pts.isEmpty() && !stubOnly) { - p.moveTo(pts.first()); - for (int i = 1; i < pts.size(); ++i) { - p.lineTo(pts[i]); + if (!drawPts.isEmpty() && !stubOnly) { + p.moveTo(drawPts.first()); + for (int i = 1; i < drawPts.size(); ++i) { + p.lineTo(drawPts[i]); } } // interface stub circle at edge - if (m_isInterface && m_interfaceEdge && m_interfaceStubOnly) { + if (m_isInterface && m_interfaceEdge && m_interfaceStubOnly && !m_interfaceEdge->tunneled) { const QPointF stubPos = scenePortPos(*m_interfaceEdge); const qreal r = 6.0; p.addEllipse(stubPos, r, r); @@ -721,10 +765,18 @@ void ArrowItem::updatePath() { // наконечник стрелки // Draw arrow head only if the path truly ends (not branching through a junction). - if (!stubOnly && pts.size() >= 2 && !m_to.junction) { - const QPointF tip = pts.last(); - const QPointF from = pts[pts.size() - 2]; + if (!stubOnly && drawPts.size() >= 2 && !m_to.junction) { + const QPointF tip = drawPts.last(); + const QPointF from = drawPts[drawPts.size() - 2]; drawArrowHead(p, tip, from); + if (m_to.tunneled) { + drawTunnelBrackets(p, tip, from); + } + } + if (!stubOnly && drawPts.size() >= 2 && m_from.tunneled) { + const QPointF tip = drawPts.first(); + const QPointF from = tip - normalizedOr(drawPts[1] - tip, QPointF(1, 0)); + drawTunnelBrackets(p, tip, from); } // Обновляем геометрию, чтобы сцена инвалидацировала старую и новую области. @@ -748,7 +800,7 @@ void ArrowItem::hoverMoveEvent(QGraphicsSceneHoverEvent* e) { const QVector pts = computePolyline(); QPointF pos = e->scenePos(); - if (!m_labelLocked && m_labelItem && labelDragHitRect(m_labelItem).contains(pos)) { + if (m_labelItem && labelDragHitRect(m_labelItem).contains(pos)) { setCursor(Qt::SizeAllCursor); QGraphicsPathItem::hoverMoveEvent(e); return; @@ -777,10 +829,11 @@ void ArrowItem::mousePressEvent(QGraphicsSceneMouseEvent* e) { // Keep label geometry in sync before hit-testing drag area. updateLabelItem(m_lastPolyline.isEmpty() ? computePolyline() : m_lastPolyline); - if (!m_labelLocked && m_labelItem && labelDragHitRect(m_labelItem).contains(e->scenePos())) { + if (m_labelItem && labelDragHitRect(m_labelItem).contains(e->scenePos())) { m_labelDragging = true; m_labelDragLastScene = e->scenePos(); setCursor(Qt::SizeAllCursor); + updatePath(); e->accept(); return; } @@ -844,7 +897,7 @@ void ArrowItem::mouseMoveEvent(QGraphicsSceneMouseEvent* e) { const QPointF delta = e->scenePos() - m_labelDragLastScene; m_labelDragLastScene = e->scenePos(); m_labelOffset += delta; - updateLabelItem(m_lastPolyline.isEmpty() ? computePolyline() : m_lastPolyline); + updatePath(); e->accept(); return; } @@ -900,6 +953,7 @@ void ArrowItem::mouseReleaseEvent(QGraphicsSceneMouseEvent* e) { if (m_labelDragging && e->button() == Qt::LeftButton) { m_labelDragging = false; unsetCursor(); + updatePath(); if (auto* ds = qobject_cast(scene())) { ds->requestSnapshot(); } @@ -970,3 +1024,30 @@ void ArrowItem::drawArrowHead(QPainterPath& path, const QPointF& tip, const QPoi path.lineTo(tip); path.lineTo(right); } + +void ArrowItem::drawTunnelBrackets(QPainterPath& path, const QPointF& tip, const QPointF& from) { + const QPointF v = tip - from; + const qreal len = std::hypot(v.x(), v.y()); + if (len < 0.001) return; + + const QPointF dir = v / len; + const QPointF n(-dir.y(), dir.x()); + // Pull marker inward so both brackets remain visible even when tip is on the frame edge. + const QPointF c = tip; + const qreal h = 9.0; + const qreal w = 5.0; + const qreal gap = 8.5; + + // Rotate tunnel marker by 90 degrees around arrow-end center. + const QPointF l0 = c - n * gap - dir * h; + const QPointF l1 = c - n * (gap + w); + const QPointF l2 = c - n * gap + dir * h; + path.moveTo(l0); + path.quadTo(l1, l2); + + const QPointF r0 = c + n * gap - dir * h; + const QPointF r1 = c + n * (gap + w); + const QPointF r2 = c + n * gap + dir * h; + path.moveTo(r0); + path.quadTo(r1, r2); +} diff --git a/src/items/ArrowItem.h b/src/items/ArrowItem.h index 927e7ed..4c8c45b 100644 --- a/src/items/ArrowItem.h +++ b/src/items/ArrowItem.h @@ -19,6 +19,7 @@ public: BlockItem::Port port = BlockItem::Port::Input; std::optional localPos; // точка на стороне блока (в локальных координатах) std::optional scenePos; // произвольная точка на сцене (например, граница) + bool tunneled = false; }; ArrowItem(QGraphicsItem* parent = nullptr); @@ -66,6 +67,10 @@ public: bool isCallMechanism() const { return m_isCallMechanism; } void setCallRefId(int id) { m_callRefId = id; } int callRefId() const { return m_callRefId; } + void setFromTunneled(bool v); + void setToTunneled(bool v); + bool isFromTunneled() const { return m_from.tunneled; } + bool isToTunneled() const { return m_to.tunneled; } void updatePath(); std::optional hitTest(const QPointF& scenePos, qreal radius) const; @@ -104,6 +109,7 @@ private: QPointF m_lastDragScenePos; void drawArrowHead(QPainterPath& path, const QPointF& tip, const QPointF& from); + void drawTunnelBrackets(QPainterPath& path, const QPointF& tip, const QPointF& from); QVector computePolyline() const; static QPointF portDirection(const Endpoint& ep, bool isFrom, const QPointF& fallbackDir, const QGraphicsScene* scene); static QVector simplifyPolyline(const QVector& pts); @@ -111,6 +117,8 @@ private: void updateLabelItem(const QVector& pts); protected: + QRectF boundingRect() const override; + QPainterPath shape() const override; void hoverMoveEvent(QGraphicsSceneHoverEvent* e) override; void mousePressEvent(QGraphicsSceneMouseEvent* e) override; void mouseMoveEvent(QGraphicsSceneMouseEvent* e) override; diff --git a/src/items/BlockItem.cpp b/src/items/BlockItem.cpp index 1a2d844..55d9eb3 100644 --- a/src/items/BlockItem.cpp +++ b/src/items/BlockItem.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -285,7 +286,10 @@ void BlockItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* e) { auto* layout = new QVBoxLayout(dlg); auto* form = new QFormLayout(); - auto* titleEdit = new QLineEdit(m_title, dlg); + auto* titleEdit = new QPlainTextEdit(dlg); + titleEdit->setPlainText(m_title); + titleEdit->setTabChangesFocus(true); + titleEdit->setMinimumHeight(72); form->addRow(tr("Title:"), titleEdit); auto affixes = currencyAffixes(); @@ -312,8 +316,10 @@ void BlockItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* e) { bool changed = false; if (dlg->exec() == QDialog::Accepted) { - if (m_title != titleEdit->text()) { - setTitle(titleEdit->text()); + QString newTitle = titleEdit->toPlainText(); + newTitle.replace('\r', ""); + if (m_title != newTitle) { + setTitle(newTitle); changed = true; } std::optional newPrice; diff --git a/src/items/DiagramScene.cpp b/src/items/DiagramScene.cpp index c862c5a..a8b8d9e 100644 --- a/src/items/DiagramScene.cpp +++ b/src/items/DiagramScene.cpp @@ -298,6 +298,14 @@ bool DiagramScene::tryStartArrowDrag(const QPointF& scenePos, Qt::KeyboardModifi 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); // попадание в интерфейсный круг @@ -357,6 +365,7 @@ bool DiagramScene::tryFinishArrowDrag(const QPointF& scenePos) { m_dragArrow->setFrom(stubEp); m_dragArrow->setTo(blockEp); } + markSubdiagramOriginTunnel(m_dragArrow); m_dragArrow->setLabelLocked(true); m_dragArrow->finalize(); } @@ -366,6 +375,7 @@ bool DiagramScene::tryFinishArrowDrag(const QPointF& scenePos) { to.port = port; to.localPos = localPos; m_dragArrow->setTo(to); + markSubdiagramOriginTunnel(m_dragArrow); m_dragArrow->finalize(); } @@ -398,6 +408,7 @@ bool DiagramScene::tryFinishArrowDrag(const QPointF& scenePos) { m_dragArrow->setFrom(stubEp); m_dragArrow->setTo(jun); } + markSubdiagramOriginTunnel(m_dragArrow); m_dragArrow->setLabelLocked(true); m_dragArrow->finalize(); } @@ -406,6 +417,7 @@ bool DiagramScene::tryFinishArrowDrag(const QPointF& scenePos) { to.junction = j; to.port = BlockItem::Port::Input; m_dragArrow->setTo(to); + markSubdiagramOriginTunnel(m_dragArrow); m_dragArrow->finalize(); } @@ -462,6 +474,7 @@ bool DiagramScene::tryFinishArrowDrag(const QPointF& scenePos) { m_dragArrow->setLabelHidden(hideInherited); m_dragArrow->setLabelInherited(true); m_dragArrow->setLabelSource(sourceRoot); + markSubdiagramOriginTunnel(m_dragArrow); m_dragArrow->finalize(); m_dragArrow = nullptr; @@ -485,6 +498,7 @@ bool DiagramScene::tryFinishArrowDrag(const QPointF& scenePos) { to.scenePos = edgePoint; to.port = BlockItem::Port::Input; m_dragArrow->setTo(to); + markSubdiagramOriginTunnel(m_dragArrow); m_dragArrow->finalize(); m_dragArrow = nullptr; m_dragFromBlock.clear(); @@ -558,6 +572,15 @@ bool DiagramScene::tryBranchAtArrow(const QPointF& scenePos) { 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 blockSet; QSet junctionSet; @@ -582,6 +605,7 @@ DiagramScene::Snapshot DiagramScene::captureSnapshot() const { out.scenePos = *ep.scenePos; out.port = ep.port; } + out.tunneled = ep.tunneled; return out; }; @@ -699,6 +723,7 @@ for (const auto& j : snap.junctions) { case Snapshot::Endpoint::Kind::None: break; } + out.tunneled = ep.tunneled; return out; }; @@ -733,7 +758,20 @@ for (const auto& j : snap.junctions) { 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()); @@ -833,6 +871,7 @@ bool DiagramScene::goDownIntoSelected() { 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; @@ -851,6 +890,87 @@ bool DiagramScene::goUp() { 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(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 tunneledEdgeKeys; + QSet 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(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; } @@ -870,6 +990,16 @@ bool DiagramScene::goDownIntoBlock(BlockItem* b) { 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 @@ -951,11 +1081,14 @@ bool DiagramScene::goDownIntoBlock(BlockItem* b) { Snapshot::Arrow ar; ar.from = makeSceneEndpoint(edgeScene, t.port); ar.to = ar.from; - ar.label = a->label(); + 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); @@ -968,11 +1101,14 @@ bool DiagramScene::goDownIntoBlock(BlockItem* b) { Snapshot::Arrow ar; ar.from = makeSceneEndpoint(edgeScene, f.port); ar.to = ar.from; - ar.label = a->label(); + 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); @@ -1223,7 +1359,23 @@ void DiagramScene::deleteSelection() { } for (ArrowItem* a : arrowsToDelete) { if (a->isInterface()) { - a->resetInterfaceStub(); + 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; } @@ -1357,6 +1509,7 @@ QJsonObject endpointToJson(const DiagramScene::Snapshot::Endpoint& ep) { 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()}; } @@ -1371,6 +1524,7 @@ DiagramScene::Snapshot::Endpoint endpointFromJson(const QJsonObject& o) { 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()); @@ -1430,6 +1584,7 @@ QVariantMap DiagramScene::exportToVariant() { arrows.append(o); } root["arrows"] = arrows; + if (snap.hasState) root["state"] = snap.state; return root.toVariantMap(); }; @@ -1454,10 +1609,27 @@ QVariantMap DiagramScene::exportToVariant() { } bool DiagramScene::importFromVariant(const QVariantMap& map) { - auto toSnap = [](const QVariantMap& vm, DiagramScene* self) -> std::optional { + const QVariantMap rootMeta = map.value("meta").toMap(); + auto toSnap = [&rootMeta](const QVariantMap& vm, DiagramScene* self) -> std::optional { 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(); diff --git a/src/items/DiagramScene.h b/src/items/DiagramScene.h index 4ddff9e..2a89bd7 100644 --- a/src/items/DiagramScene.h +++ b/src/items/DiagramScene.h @@ -51,6 +51,7 @@ private: BlockItem::Port port = BlockItem::Port::Input; std::optional localPos; std::optional scenePos; + bool tunneled = false; }; struct Block { int id; QString title; QPointF pos; bool hasDecomp = false; QString number; std::optional price; std::optional color; }; struct Junction { int id; QPointF pos; }; @@ -72,6 +73,8 @@ private: bool callMechanism = false; int callRefId = -1; }; + QString state; + bool hasState = false; QVector blocks; QVector junctions; QVector arrows; diff --git a/src/main.cpp b/src/main.cpp index aa7ee5a..19d2edf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -34,18 +34,51 @@ public: int main(int argc, char** argv) { IdefApplication app(argc, argv); + const QLocale systemLocale = QLocale::system(); + QLocale::setDefault(systemLocale); + QTranslator translator; - const QLocale locale; - const QString baseName = QStringLiteral("idef0_%1").arg(locale.name()); + QStringList candidates; + auto addCandidate = [&](const QString& c) { + if (c.isEmpty()) return; + const QString normalized = QString(c).replace('-', '_'); + if (normalized == QStringLiteral("C") || normalized == QStringLiteral("POSIX")) return; + candidates << normalized; + const QString shortName = normalized.section('_', 0, 0); + if (!shortName.isEmpty()) candidates << shortName; + }; + addCandidate(QLocale().name()); + addCandidate(systemLocale.name()); + for (const QString& lang : systemLocale.uiLanguages()) { + addCandidate(lang); + } + addCandidate(QString::fromLocal8Bit(qgetenv("LANG")).section('.', 0, 0)); + candidates.removeDuplicates(); + if (candidates.isEmpty()) candidates << QStringLiteral("en"); + const QStringList searchPaths = { QDir::currentPath() + "/translations", QCoreApplication::applicationDirPath() + "/../share/idef0/translations", ":/i18n" }; - for (const QString& path : searchPaths) { - if (translator.load(baseName, path)) { - app.installTranslator(&translator); - break; + bool loaded = false; + for (const QString& loc : candidates) { + const QString baseName = QStringLiteral("idef0_%1").arg(loc); + for (const QString& path : searchPaths) { + if (translator.load(baseName, path)) { + app.installTranslator(&translator); + loaded = true; + break; + } + } + if (loaded) break; + } + if (!loaded) { + for (const QString& path : searchPaths) { + if (translator.load(QStringLiteral("idef0_en"), path)) { + app.installTranslator(&translator); + break; + } } } @@ -62,7 +95,7 @@ int main(int argc, char** argv) { app.fileOpenHandler = [&w](const QString& path) { w.openDiagramPath(path); }; - w.resize(1200, 800); + w.resize(1150, 850); w.show(); return app.exec(); } diff --git a/src/plugins/color/ColorsPlugin.cpp b/src/plugins/color/ColorsPlugin.cpp index 3a7b85f..85917fb 100644 --- a/src/plugins/color/ColorsPlugin.cpp +++ b/src/plugins/color/ColorsPlugin.cpp @@ -19,7 +19,9 @@ static void ensureTranslator(Idef0Host* host) { QStringList candidates; auto addCandidate = [&](const QString& c) { - if (!c.isEmpty() && !c.contains("C")) candidates << c; + if (c.isEmpty()) return; + if (c == QStringLiteral("C") || c == QStringLiteral("POSIX")) return; + candidates << c; }; addCandidate(baseLocale); addCandidate(shortLocale);