minor patches
This commit is contained in:
parent
630c952382
commit
891067ac38
9 changed files with 506 additions and 55 deletions
|
|
@ -7,6 +7,7 @@
|
|||
#include <QStatusBar>
|
||||
#include <QDialog>
|
||||
#include <QTabWidget>
|
||||
#include <QTreeWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QFormLayout>
|
||||
#include <QLineEdit>
|
||||
|
|
@ -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<void(QTreeWidgetItem*, const QVariantMap&, const QString&)> addSubtree;
|
||||
addSubtree = [&](QTreeWidgetItem* parent, const QVariantMap& diagram, const QString& prefix) {
|
||||
const QVariantList blockList = diagram.value("blocks").toList();
|
||||
QVector<QVariant> 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<BlockItem*>(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<BlockItem*>(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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<QPointF> 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<QPointF>& 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<QPointF>& 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<QPointF> pts = computePolyline();
|
||||
m_lastPolyline = pts;
|
||||
const QVector<QPointF>& 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<QPointF> 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<DiagramScene*>(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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ public:
|
|||
BlockItem::Port port = BlockItem::Port::Input;
|
||||
std::optional<QPointF> localPos; // точка на стороне блока (в локальных координатах)
|
||||
std::optional<QPointF> 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<QPointF> 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<QPointF> computePolyline() const;
|
||||
static QPointF portDirection(const Endpoint& ep, bool isFrom, const QPointF& fallbackDir, const QGraphicsScene* scene);
|
||||
static QVector<QPointF> simplifyPolyline(const QVector<QPointF>& pts);
|
||||
|
|
@ -111,6 +117,8 @@ private:
|
|||
void updateLabelItem(const QVector<QPointF>& 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;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include <QDoubleSpinBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QCheckBox>
|
||||
#include <QVBoxLayout>
|
||||
#include <QLocale>
|
||||
|
|
@ -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<qreal> newPrice;
|
||||
|
|
|
|||
|
|
@ -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<BlockItem*> blockSet;
|
||||
QSet<JunctionItem*> 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<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;
|
||||
}
|
||||
|
||||
|
|
@ -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<Snapshot> {
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ private:
|
|||
BlockItem::Port port = BlockItem::Port::Input;
|
||||
std::optional<QPointF> localPos;
|
||||
std::optional<QPointF> scenePos;
|
||||
bool tunneled = false;
|
||||
};
|
||||
struct Block { int id; QString title; QPointF pos; bool hasDecomp = false; QString number; std::optional<qreal> price; std::optional<QString> color; };
|
||||
struct Junction { int id; QPointF pos; };
|
||||
|
|
@ -72,6 +73,8 @@ private:
|
|||
bool callMechanism = false;
|
||||
int callRefId = -1;
|
||||
};
|
||||
QString state;
|
||||
bool hasState = false;
|
||||
QVector<Block> blocks;
|
||||
QVector<Junction> junctions;
|
||||
QVector<Arrow> arrows;
|
||||
|
|
|
|||
47
src/main.cpp
47
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue