modified: CMakeLists.txt

modified:   src/MainWindow.cpp
	modified:   src/MainWindow.h
	modified:   src/items/ArrowItem.cpp
	modified:   src/items/ArrowItem.h
	modified:   src/items/BlockItem.cpp
	modified:   src/items/BlockItem.h
	modified:   src/items/DiagramScene.cpp
	modified:   src/items/DiagramScene.h
	new file:   src/items/HeaderFooterItem.cpp
	new file:   src/items/HeaderFooterItem.h
	modified:   src/items/JunctionItem.cpp
	modified:   src/items/JunctionItem.h
	modified:   src/main.cpp
This commit is contained in:
Gregory Bednov 2026-02-25 18:25:51 +03:00
commit f6f0598ff2
14 changed files with 2541 additions and 94 deletions

View file

@ -0,0 +1,320 @@
#include "HeaderFooterItem.h"
#include "items/DiagramScene.h"
#include <QPainter>
#include <QDialog>
#include <QFormLayout>
#include <QLineEdit>
#include <QDialogButtonBox>
#include <QCheckBox>
#include <QRadioButton>
#include <QButtonGroup>
#include <QVBoxLayout>
#include <QGraphicsSceneMouseEvent>
HeaderFooterItem::HeaderFooterItem(DiagramScene* scene)
: QGraphicsObject(nullptr)
, m_scene(scene)
{
setZValue(-4);
setAcceptedMouseButtons(Qt::LeftButton);
setFlag(QGraphicsItem::ItemIsSelectable, false);
}
void HeaderFooterItem::setMeta(const QVariantMap& meta) {
m_meta = meta;
update();
}
void HeaderFooterItem::setPageRect(const QRectF& rect) {
if (m_pageRect == rect) return;
prepareGeometryChange();
m_pageRect = rect;
update();
}
void HeaderFooterItem::setShowHeaderFooter(bool showHeader, bool showFooter) {
m_showHeader = showHeader;
m_showFooter = showFooter;
update();
}
QRectF HeaderFooterItem::boundingRect() const {
return m_pageRect;
}
QString HeaderFooterItem::metaText(const QString& key, const QString& fallback) const {
const auto val = m_meta.value(key);
if (val.isNull()) return fallback;
return val.toString();
}
bool HeaderFooterItem::metaBool(const QString& key, bool fallback) const {
if (!m_meta.contains(key)) return fallback;
return m_meta.value(key).toBool();
}
bool HeaderFooterItem::hitHeaderFooter(const QPointF& scenePos) const {
if (!m_pageRect.contains(scenePos)) return false;
const qreal headerH = m_pageRect.height() * 0.12;
const qreal footerH = m_pageRect.height() * 0.08;
const qreal top = m_pageRect.top();
const qreal bottom = m_pageRect.bottom();
if (m_showHeader && scenePos.y() <= top + headerH) return true;
if (m_showFooter && scenePos.y() >= bottom - footerH) return true;
return false;
}
void HeaderFooterItem::paint(QPainter* painter, const QStyleOptionGraphicsItem*, QWidget*) {
if (m_pageRect.isNull()) return;
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
const bool darkMode = metaBool("resolvedDarkMode", metaBool("darkMode", false));
const QColor fg = darkMode ? QColor(235, 235, 235) : QColor(20, 20, 20);
const QColor bg = darkMode ? QColor(26, 26, 26) : QColor(255, 255, 255);
const qreal w = m_pageRect.width();
const qreal h = m_pageRect.height();
const qreal headerH = h * 0.12;
const qreal footerH = h * 0.08;
QPen linePen(fg, 1.0);
painter->setPen(linePen);
painter->setBrush(Qt::NoBrush);
QFont labelFont = painter->font();
labelFont.setPointSizeF(labelFont.pointSizeF() - 1);
painter->setFont(labelFont);
if (m_showHeader) {
QRectF headerRect(m_pageRect.left(), m_pageRect.top(), w, headerH);
painter->drawRect(headerRect);
const qreal leftW = w * 0.62;
const qreal rightW = w - leftW;
const qreal xSplit = headerRect.left() + leftW;
painter->drawLine(QPointF(xSplit, headerRect.top()), QPointF(xSplit, headerRect.bottom()));
const qreal rowH = headerH / 4.0;
for (int i = 1; i < 4; ++i) {
const qreal y = headerRect.top() + i * rowH;
painter->drawLine(QPointF(headerRect.left(), y), QPointF(xSplit, y));
}
// Left area text
const QString organization = metaText("organization");
const QString usedAt = organization.isEmpty() ? metaText("usedAt") : organization;
const QString author = metaText("author");
const QString title = metaText("title");
const QString project = title.isEmpty() ? metaText("project") : title;
const QString notes = metaText("notes");
const QString date = metaText("date");
const QString rev = metaText("rev");
auto drawRow = [&](int row, const QString& label, const QString& value) {
QRectF r(headerRect.left() + 6, headerRect.top() + row * rowH + 2, leftW - 12, rowH - 4);
painter->drawText(r, Qt::AlignLeft | Qt::AlignVCenter, label + (value.isEmpty() ? "" : " " + value));
};
drawRow(0, organization.isEmpty() ? tr("USED AT:") : tr("ORGANIZATION:"), usedAt);
drawRow(1, tr("AUTHOR:"), author);
drawRow(2, title.isEmpty() ? tr("PROJECT:") : tr("TITLE:"), project);
drawRow(3, tr("NOTES:"), notes);
// Date/rev in left area top-right corner
QRectF dateRect(headerRect.left() + leftW * 0.55, headerRect.top() + 2, leftW * 0.45 - 6, rowH - 4);
painter->drawText(dateRect, Qt::AlignRight | Qt::AlignVCenter, tr("DATE: ") + date);
QRectF revRect(headerRect.left() + leftW * 0.55, headerRect.top() + rowH + 2, leftW * 0.45 - 6, rowH - 4);
painter->drawText(revRect, Qt::AlignRight | Qt::AlignVCenter, tr("REV: ") + rev);
// Right area split
QRectF rightRect(xSplit, headerRect.top(), rightW, headerH);
const qreal statusW = rightW * 0.55;
const qreal infoW = rightW - statusW;
const qreal statusX = rightRect.left();
const qreal infoX = rightRect.left() + statusW;
painter->drawLine(QPointF(infoX, headerRect.top()), QPointF(infoX, headerRect.bottom()));
const qreal statusRowH = headerH / 4.0;
for (int i = 1; i < 4; ++i) {
const qreal y = headerRect.top() + i * statusRowH;
painter->drawLine(QPointF(statusX, y), QPointF(infoX, y));
}
QString activeState = metaText("state");
if (activeState.isEmpty()) {
if (metaBool("publication", false)) activeState = "publication";
else if (metaBool("recommended", false)) activeState = "recommended";
else if (metaBool("draft", false)) activeState = "draft";
else if (metaBool("working", false)) activeState = "working";
}
auto drawStatus = [&](int row, const QString& label, const QString& key) {
QRectF r(statusX, headerRect.top() + row * statusRowH, statusW, statusRowH);
const qreal box = statusRowH;
const qreal bx = r.left();
const qreal by = r.top();
QRectF boxRect(bx, by, box, box);
painter->setBrush(activeState == key ? fg : bg);
painter->drawRect(boxRect);
painter->setBrush(Qt::NoBrush);
QRectF textRect = r.adjusted(box + 6, 0, -6, 0);
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, label);
};
drawStatus(0, tr("WORKING"), "working");
drawStatus(1, tr("DRAFT"), "draft");
drawStatus(2, tr("RECOMMENDED"), "recommended");
drawStatus(3, tr("PUBLICATION"), "publication");
// Info column
const qreal infoRowH = headerH / 3.0;
for (int i = 1; i < 3; ++i) {
const qreal y = headerRect.top() + i * infoRowH;
painter->drawLine(QPointF(infoX, y), QPointF(rightRect.right(), y));
}
QRectF readerRect(infoX + 6, headerRect.top() + 2, infoW - 12, infoRowH - 4);
painter->drawText(readerRect, Qt::AlignLeft | Qt::AlignVCenter, tr("READER: ") + metaText("reader"));
QRectF readerDateRect(infoX + 6, headerRect.top() + infoRowH + 2, infoW - 12, infoRowH - 4);
painter->drawText(readerDateRect, Qt::AlignLeft | Qt::AlignVCenter, tr("DATE: ") + metaText("readerDate"));
QRectF contextRect(infoX + 6, headerRect.top() + 2 * infoRowH + 2, infoW - 12, infoRowH - 4);
painter->drawText(contextRect, Qt::AlignLeft | Qt::AlignVCenter, tr("CONTEXT: ") + metaText("context"));
}
if (m_showFooter) {
QRectF footerRect(m_pageRect.left(), m_pageRect.bottom() - footerH, w, footerH);
painter->drawRect(footerRect);
const qreal leftW = w * 0.2;
const qreal rightW = w * 0.2;
const qreal middleW = w - leftW - rightW;
const qreal x1 = footerRect.left() + leftW;
const qreal x2 = x1 + middleW;
painter->drawLine(QPointF(x1, footerRect.top()), QPointF(x1, footerRect.bottom()));
painter->drawLine(QPointF(x2, footerRect.top()), QPointF(x2, footerRect.bottom()));
QString nodeValue = metaText("footerNodeOverride");
if (nodeValue.isEmpty() && m_scene) nodeValue = m_scene->currentNodeLabel();
if (nodeValue.isEmpty()) nodeValue = metaText("node", "A0");
QRectF nodeRect(footerRect.left() + 6, footerRect.top() + 2, leftW - 12, footerH - 4);
painter->drawText(nodeRect, Qt::AlignLeft | Qt::AlignVCenter, tr("NODE: ") + nodeValue);
QString titleValue = metaText("footerTitleOverride");
if (titleValue.isEmpty() && m_scene) titleValue = m_scene->currentDiagramTitle();
if (titleValue.isEmpty()) titleValue = metaText("title");
QRectF titleRect(x1 + 6, footerRect.top() + 2, middleW - 12, footerH - 4);
painter->drawText(titleRect, Qt::AlignHCenter | Qt::AlignVCenter, titleValue);
QString numberValue = metaText("footerNumberOverride");
if (numberValue.isEmpty()) numberValue = metaText("number");
QRectF numberRect(x2 + 6, footerRect.top() + 2, rightW - 12, footerH - 4);
painter->drawText(numberRect, Qt::AlignLeft | Qt::AlignVCenter, tr("NUMBER: ") + numberValue);
}
painter->restore();
}
void HeaderFooterItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* e) {
if (!hitHeaderFooter(e->scenePos())) {
QGraphicsObject::mouseDoubleClickEvent(e);
return;
}
auto* dlg = new QDialog();
dlg->setWindowTitle(tr("Edit header/footer"));
auto* layout = new QVBoxLayout(dlg);
auto* form = new QFormLayout();
auto* usedAt = new QLineEdit(metaText("usedAt"), dlg);
auto* author = new QLineEdit(metaText("author"), dlg);
auto* project = new QLineEdit(metaText("project"), dlg);
auto* notes = new QLineEdit(metaText("notes"), dlg);
auto* date = new QLineEdit(metaText("date"), dlg);
auto* rev = new QLineEdit(metaText("rev"), dlg);
auto* reader = new QLineEdit(metaText("reader"), dlg);
auto* readerDate = new QLineEdit(metaText("readerDate"), dlg);
auto* context = new QLineEdit(metaText("context"), dlg);
auto* node = new QLineEdit(metaText("node", "A0"), dlg);
auto* title = new QLineEdit(metaText("title"), dlg);
auto* number = new QLineEdit(metaText("number"), dlg);
const QString state = metaText("state");
auto* working = new QRadioButton(tr("Working"), dlg);
auto* draft = new QRadioButton(tr("Draft"), dlg);
auto* recommended = new QRadioButton(tr("Recommended"), dlg);
auto* publication = new QRadioButton(tr("Publication"), dlg);
auto* group = new QButtonGroup(dlg);
group->addButton(working);
group->addButton(draft);
group->addButton(recommended);
group->addButton(publication);
if (state == "draft") {
draft->setChecked(true);
} else if (state == "recommended") {
recommended->setChecked(true);
} else if (state == "publication") {
publication->setChecked(true);
} else if (state == "working") {
working->setChecked(true);
} else {
// Backward-compatible: map old flags to a single selection.
if (metaBool("publication", false)) publication->setChecked(true);
else if (metaBool("recommended", false)) recommended->setChecked(true);
else if (metaBool("draft", false)) draft->setChecked(true);
else if (metaBool("working", false)) working->setChecked(true);
}
form->addRow(tr("Used at:"), usedAt);
form->addRow(tr("Author:"), author);
form->addRow(tr("Project:"), project);
form->addRow(tr("Notes:"), notes);
form->addRow(tr("Date:"), date);
form->addRow(tr("Rev:"), rev);
form->addRow(tr("Reader:"), reader);
form->addRow(tr("Reader date:"), readerDate);
form->addRow(tr("Context:"), context);
form->addRow(tr("Node:"), node);
form->addRow(tr("Title:"), title);
form->addRow(tr("Number:"), number);
form->addRow(tr("State:"), working);
form->addRow(QString(), draft);
form->addRow(QString(), recommended);
form->addRow(QString(), publication);
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) {
QVariantMap patch;
patch["usedAt"] = usedAt->text();
patch["author"] = author->text();
patch["project"] = project->text();
patch["notes"] = notes->text();
patch["date"] = date->text();
patch["rev"] = rev->text();
patch["reader"] = reader->text();
patch["readerDate"] = readerDate->text();
patch["context"] = context->text();
patch["node"] = node->text();
patch["title"] = title->text();
patch["number"] = number->text();
QString nextState;
if (working->isChecked()) nextState = "working";
else if (draft->isChecked()) nextState = "draft";
else if (recommended->isChecked()) nextState = "recommended";
else if (publication->isChecked()) nextState = "publication";
patch["state"] = nextState;
patch["working"] = (nextState == "working");
patch["draft"] = (nextState == "draft");
patch["recommended"] = (nextState == "recommended");
patch["publication"] = (nextState == "publication");
if (m_scene) m_scene->updateMeta(patch);
}
dlg->deleteLater();
QGraphicsObject::mouseDoubleClickEvent(e);
}