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:
parent
abdbae5a11
commit
f6f0598ff2
14 changed files with 2541 additions and 94 deletions
|
|
@ -15,8 +15,39 @@ qt_add_executable(idef0_editor
|
|||
src/items/BlockItem.h src/items/BlockItem.cpp
|
||||
src/items/ArrowItem.h src/items/ArrowItem.cpp
|
||||
src/items/JunctionItem.h src/items/JunctionItem.cpp
|
||||
src/items/HeaderFooterItem.h src/items/HeaderFooterItem.cpp
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
set(MACOS_BUNDLE_ID "com.gregorybednov.idef0-editor")
|
||||
set(MACOS_BUNDLE_NAME "IDEF0 Editor")
|
||||
configure_file(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/cmake/Info.plist.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/Info.plist
|
||||
@ONLY
|
||||
)
|
||||
set_target_properties(idef0_editor PROPERTIES
|
||||
MACOSX_BUNDLE TRUE
|
||||
MACOSX_BUNDLE_BUNDLE_NAME "${MACOS_BUNDLE_NAME}"
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER "${MACOS_BUNDLE_ID}"
|
||||
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/Info.plist"
|
||||
)
|
||||
endif()
|
||||
|
||||
target_compile_definitions(idef0_editor PRIVATE $<$<NOT:$<CONFIG:Debug>>:QT_NO_DEBUG_OUTPUT>)
|
||||
|
||||
target_include_directories(idef0_editor PRIVATE src)
|
||||
|
||||
target_link_libraries(idef0_editor PRIVATE Qt6::Widgets)
|
||||
|
||||
install(TARGETS idef0_editor
|
||||
BUNDLE DESTINATION .
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
|
||||
if(UNIX AND NOT APPLE)
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/packaging/linux/idef0-editor.desktop
|
||||
DESTINATION share/applications)
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/packaging/linux/idef0.xml
|
||||
DESTINATION share/mime/packages)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -5,35 +5,825 @@
|
|||
#include <QToolBar>
|
||||
#include <QAction>
|
||||
#include <QStatusBar>
|
||||
#include <QDialog>
|
||||
#include <QTabWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QFormLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QComboBox>
|
||||
#include <QCheckBox>
|
||||
#include <QLabel>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLocale>
|
||||
#include <QTimer>
|
||||
#include <QMenuBar>
|
||||
#include <QFileDialog>
|
||||
#include <QFile>
|
||||
#include <QMessageBox>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QFileInfo>
|
||||
#include <QPointF>
|
||||
#include <QVariantMap>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QSignalBlocker>
|
||||
#include <QCloseEvent>
|
||||
#include <QInputDialog>
|
||||
#include <QShortcut>
|
||||
#include <QColorDialog>
|
||||
#include <QApplication>
|
||||
#include <QPalette>
|
||||
#include <QStyleHints>
|
||||
#include <cmath>
|
||||
#include <QGestureEvent>
|
||||
#include <QPinchGesture>
|
||||
#include <QNativeGestureEvent>
|
||||
#include <QPdfWriter>
|
||||
#include <QPageSize>
|
||||
#include <QPageLayout>
|
||||
#include <QPainter>
|
||||
#include "items/BlockItem.h"
|
||||
|
||||
MainWindow::MainWindow(QWidget* parent)
|
||||
static const char* kDiagramFileFilter = "IDEF0 Diagram (*.idef0);;JSON Diagram (*.json)";
|
||||
static const char* kPdfFileFilter = "PDF (*.pdf)";
|
||||
|
||||
static QPageSize::PageSizeId pageSizeFromMeta(const QVariantMap& meta) {
|
||||
const QString size = meta.value("pageSize", "A4").toString();
|
||||
if (size == "A1") return QPageSize::A1;
|
||||
if (size == "A2") return QPageSize::A2;
|
||||
if (size == "A3") return QPageSize::A3;
|
||||
return QPageSize::A4;
|
||||
}
|
||||
|
||||
static QPageLayout::Orientation pageOrientationFromMeta(const QVariantMap& meta) {
|
||||
const QString orient = meta.value("pageOrientation").toString();
|
||||
if (orient == QObject::tr("Portrait") || orient == "Portrait") {
|
||||
return QPageLayout::Portrait;
|
||||
}
|
||||
return QPageLayout::Landscape;
|
||||
}
|
||||
|
||||
MainWindow::MainWindow(const QString& startupPath, QWidget* parent)
|
||||
: QMainWindow(parent)
|
||||
{
|
||||
setupUi();
|
||||
setupActions();
|
||||
statusBar()->showMessage("RMB: add block. LMB on port: drag arrow. Double click block: rename.");
|
||||
statusBar()->showMessage(tr("RMB: add block. LMB on port: drag arrow. Double click block: rename."));
|
||||
if (!startupPath.isEmpty()) {
|
||||
if (!loadDiagramFromPath(startupPath)) {
|
||||
QTimer::singleShot(0, this, &QWidget::close);
|
||||
return;
|
||||
}
|
||||
} else if (!promptStartup()) {
|
||||
QTimer::singleShot(0, this, &QWidget::close);
|
||||
return;
|
||||
}
|
||||
markDirty(false);
|
||||
}
|
||||
|
||||
void MainWindow::setupUi() {
|
||||
m_scene = new DiagramScene(this);
|
||||
if (!m_scene) {
|
||||
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;
|
||||
markDirty(true);
|
||||
});
|
||||
m_scene->applyMeta(m_welcomeState);
|
||||
}
|
||||
m_view = new QGraphicsView(m_scene, this);
|
||||
m_view->setRenderHint(QPainter::Antialiasing, true);
|
||||
m_view->setDragMode(QGraphicsView::RubberBandDrag);
|
||||
// Полное обновление избавляет от графических артефактов во время drag временной стрелки.
|
||||
m_view->setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
|
||||
m_view->viewport()->grabGesture(Qt::PinchGesture);
|
||||
m_view->viewport()->installEventFilter(this);
|
||||
|
||||
setCentralWidget(m_view);
|
||||
|
||||
// стартовые блоки
|
||||
m_scene->createBlockAt(QPointF(-200, -50))->setPos(-300, -150);
|
||||
m_scene->createBlockAt(QPointF(200, -50))->setPos(200, -150);
|
||||
}
|
||||
|
||||
void MainWindow::setupActions() {
|
||||
auto* tb = addToolBar("Tools");
|
||||
auto* actFit = new QAction("Fit", this);
|
||||
auto* fileMenu = menuBar()->addMenu(tr("&File"));
|
||||
auto* actNew = new QAction(tr("New"), this);
|
||||
auto* actOpen = new QAction(tr("Open…"), this);
|
||||
auto* actSave = new QAction(tr("Save"), this);
|
||||
auto* actSaveAs = new QAction(tr("Save As…"), this);
|
||||
auto* actExportPdf = new QAction(tr("Export to PDF…"), this);
|
||||
actNew->setShortcut(QKeySequence::New);
|
||||
actOpen->setShortcut(QKeySequence::Open);
|
||||
actSave->setShortcut(QKeySequence::Save);
|
||||
actSaveAs->setShortcut(QKeySequence::SaveAs);
|
||||
fileMenu->addAction(actNew);
|
||||
fileMenu->addAction(actOpen);
|
||||
fileMenu->addSeparator();
|
||||
fileMenu->addAction(actSave);
|
||||
fileMenu->addAction(actSaveAs);
|
||||
fileMenu->addSeparator();
|
||||
fileMenu->addAction(actExportPdf);
|
||||
|
||||
auto* editMenu = menuBar()->addMenu(tr("&Edit"));
|
||||
auto* actSwapNums = new QAction(tr("Swap numbers…"), this);
|
||||
editMenu->addAction(actSwapNums);
|
||||
|
||||
auto* viewMenu = menuBar()->addMenu(tr("&View"));
|
||||
auto* actFit = new QAction(tr("Fit"), this);
|
||||
actFit->setShortcut(Qt::Key_F);
|
||||
connect(actFit, &QAction::triggered, this, [this]{
|
||||
m_view->fitInView(m_scene->itemsBoundingRect().adjusted(-50,-50,50,50), Qt::KeepAspectRatio);
|
||||
});
|
||||
tb->addAction(actFit);
|
||||
viewMenu->addAction(actFit);
|
||||
|
||||
auto* actZoomIn = new QAction(tr("Scale +"), this);
|
||||
actZoomIn->setShortcut(QKeySequence::ZoomIn);
|
||||
connect(actZoomIn, &QAction::triggered, this, [this]{
|
||||
m_view->scale(1.1, 1.1);
|
||||
});
|
||||
viewMenu->addAction(actZoomIn);
|
||||
|
||||
auto* actZoomOut = new QAction(tr("Scale -"), this);
|
||||
actZoomOut->setShortcut(QKeySequence::ZoomOut);
|
||||
connect(actZoomOut, &QAction::triggered, this, [this]{
|
||||
m_view->scale(1/1.1, 1/1.1);
|
||||
});
|
||||
viewMenu->addAction(actZoomOut);
|
||||
|
||||
m_actColorfulMode = new QAction(tr("Colorful mode"), this);
|
||||
m_actColorfulMode->setCheckable(true);
|
||||
m_actColorfulMode->setChecked(m_welcomeState.value("colorfulMode", false).toBool());
|
||||
viewMenu->addSeparator();
|
||||
viewMenu->addAction(m_actColorfulMode);
|
||||
|
||||
m_actDarkMode = new QAction(tr("Dark mode"), this);
|
||||
m_actDarkMode->setCheckable(true);
|
||||
m_actDarkMode->setChecked(m_welcomeState.value("darkMode", false).toBool());
|
||||
viewMenu->addAction(m_actDarkMode);
|
||||
|
||||
m_actFollowSystemTheme = new QAction(tr("Follow system theme"), this);
|
||||
m_actFollowSystemTheme->setCheckable(true);
|
||||
m_actFollowSystemTheme->setChecked(m_welcomeState.value("followSystemTheme", false).toBool());
|
||||
viewMenu->addAction(m_actFollowSystemTheme);
|
||||
|
||||
m_actArrowColor = new QAction(tr("Arrow color..."), this);
|
||||
m_actBorderColor = new QAction(tr("Block border color..."), this);
|
||||
m_actFontColor = new QAction(tr("Block font color..."), this);
|
||||
viewMenu->addAction(m_actArrowColor);
|
||||
viewMenu->addAction(m_actBorderColor);
|
||||
viewMenu->addAction(m_actFontColor);
|
||||
|
||||
auto ensureDefaultColor = [this](const char* key, const QColor& fallback) {
|
||||
QColor c(m_welcomeState.value(key).toString());
|
||||
if (!c.isValid()) m_welcomeState[key] = fallback.name();
|
||||
};
|
||||
ensureDefaultColor("arrowColor", QColor("#2b6ee6"));
|
||||
ensureDefaultColor("blockBorderColor", QColor("#0f766e"));
|
||||
ensureDefaultColor("blockFontColor", QColor("#991b1b"));
|
||||
|
||||
auto applyModes = [this](bool mark) {
|
||||
const bool colorful = m_actColorfulMode->isChecked();
|
||||
const bool followSystem = m_actFollowSystemTheme->isChecked();
|
||||
const bool dark = m_actDarkMode->isChecked();
|
||||
m_welcomeState["colorfulMode"] = colorful;
|
||||
m_welcomeState["followSystemTheme"] = followSystem;
|
||||
m_welcomeState["darkMode"] = dark;
|
||||
m_welcomeState["resolvedDarkMode"] = effectiveDarkMode();
|
||||
|
||||
m_actArrowColor->setEnabled(colorful);
|
||||
m_actBorderColor->setEnabled(colorful);
|
||||
m_actFontColor->setEnabled(colorful);
|
||||
m_actDarkMode->setEnabled(!followSystem);
|
||||
|
||||
applyAppPalette(m_welcomeState.value("resolvedDarkMode").toBool());
|
||||
m_scene->applyMeta(m_welcomeState);
|
||||
if (mark) markDirty(true);
|
||||
};
|
||||
|
||||
connect(m_actColorfulMode, &QAction::toggled, this, [applyModes]{ applyModes(true); });
|
||||
connect(m_actDarkMode, &QAction::toggled, this, [applyModes]{ applyModes(true); });
|
||||
connect(m_actFollowSystemTheme, &QAction::toggled, this, [applyModes]{ applyModes(true); });
|
||||
connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged, this, [applyModes](Qt::ColorScheme){
|
||||
applyModes(false);
|
||||
});
|
||||
connect(m_actArrowColor, &QAction::triggered, this, [this, applyModes]{
|
||||
const QColor cur(m_welcomeState.value("arrowColor").toString());
|
||||
const QColor chosen = QColorDialog::getColor(cur.isValid() ? cur : QColor("#2b6ee6"), this, tr("Arrow color"));
|
||||
if (!chosen.isValid()) return;
|
||||
m_welcomeState["arrowColor"] = chosen.name();
|
||||
applyModes(true);
|
||||
});
|
||||
connect(m_actBorderColor, &QAction::triggered, this, [this, applyModes]{
|
||||
const QColor cur(m_welcomeState.value("blockBorderColor").toString());
|
||||
const QColor chosen = QColorDialog::getColor(cur.isValid() ? cur : QColor("#0f766e"), this, tr("Block border color"));
|
||||
if (!chosen.isValid()) return;
|
||||
m_welcomeState["blockBorderColor"] = chosen.name();
|
||||
applyModes(true);
|
||||
});
|
||||
connect(m_actFontColor, &QAction::triggered, this, [this, applyModes]{
|
||||
const QColor cur(m_welcomeState.value("blockFontColor").toString());
|
||||
const QColor chosen = QColorDialog::getColor(cur.isValid() ? cur : QColor("#991b1b"), this, tr("Block font color"));
|
||||
if (!chosen.isValid()) return;
|
||||
m_welcomeState["blockFontColor"] = chosen.name();
|
||||
applyModes(true);
|
||||
});
|
||||
applyModes(false);
|
||||
|
||||
connect(actSwapNums, &QAction::triggered, this, [this]{
|
||||
const auto sel = m_scene->selectedItems();
|
||||
if (sel.size() != 1) {
|
||||
QMessageBox::information(this, tr("Swap numbers"), tr("Select a single block to swap its number."));
|
||||
return;
|
||||
}
|
||||
auto* blk = qgraphicsitem_cast<BlockItem*>(sel.first());
|
||||
if (!blk) {
|
||||
QMessageBox::information(this, tr("Swap numbers"), tr("Select a block."));
|
||||
return;
|
||||
}
|
||||
QStringList choices;
|
||||
QHash<QString, BlockItem*> lookup;
|
||||
for (QGraphicsItem* it : m_scene->items()) {
|
||||
auto* other = qgraphicsitem_cast<BlockItem*>(it);
|
||||
if (!other || other == blk) continue;
|
||||
choices << other->number();
|
||||
lookup.insert(other->number(), other);
|
||||
}
|
||||
choices.removeAll(QString());
|
||||
if (choices.isEmpty()) {
|
||||
QMessageBox::information(this, tr("Swap numbers"), tr("No other blocks to swap with."));
|
||||
return;
|
||||
}
|
||||
bool ok = false;
|
||||
const QString choice = QInputDialog::getItem(this, tr("Swap numbers"), tr("Swap with:"), choices, 0, false, &ok);
|
||||
if (!ok || choice.isEmpty()) return;
|
||||
BlockItem* other = lookup.value(choice, nullptr);
|
||||
if (!other) return;
|
||||
const QString numA = blk->number();
|
||||
const QString numB = other->number();
|
||||
blk->setNumber(numB);
|
||||
other->setNumber(numA);
|
||||
m_scene->requestSnapshot();
|
||||
markDirty(true);
|
||||
});
|
||||
|
||||
connect(actNew, &QAction::triggered, this, [this]{ newDiagram(); });
|
||||
connect(actOpen, &QAction::triggered, this, [this]{
|
||||
const QString path = QFileDialog::getOpenFileName(this, tr("Open diagram"), QString(), tr(kDiagramFileFilter));
|
||||
if (!path.isEmpty()) {
|
||||
loadDiagramFromPath(path);
|
||||
}
|
||||
});
|
||||
|
||||
auto saveTo = [this](const QString& path) {
|
||||
QString outPath = path;
|
||||
if (QFileInfo(outPath).suffix().isEmpty()) {
|
||||
outPath += ".idef0";
|
||||
}
|
||||
const QVariantMap exported = m_scene->exportToVariant();
|
||||
QVariantMap root;
|
||||
root["diagram"] = exported.value("diagram");
|
||||
root["children"] = exported.value("children");
|
||||
root["meta"] = m_welcomeState;
|
||||
QJsonDocument doc = QJsonDocument::fromVariant(root);
|
||||
QFile f(outPath);
|
||||
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||
QMessageBox::warning(this, tr("Save failed"), tr("Could not open file for writing."));
|
||||
return false;
|
||||
}
|
||||
f.write(doc.toJson(QJsonDocument::Indented));
|
||||
f.close();
|
||||
m_currentFile = outPath;
|
||||
markDirty(false);
|
||||
return true;
|
||||
};
|
||||
|
||||
connect(actSave, &QAction::triggered, this, [this, saveTo]{
|
||||
if (m_currentFile.isEmpty()) {
|
||||
const QString path = QFileDialog::getSaveFileName(this, tr("Save diagram"), QString(), tr(kDiagramFileFilter));
|
||||
if (!path.isEmpty()) saveTo(path);
|
||||
} else {
|
||||
saveTo(m_currentFile);
|
||||
}
|
||||
});
|
||||
connect(actSaveAs, &QAction::triggered, this, [this, saveTo]{
|
||||
const QString path = QFileDialog::getSaveFileName(this, tr("Save diagram as"), QString(), tr(kDiagramFileFilter));
|
||||
if (!path.isEmpty()) saveTo(path);
|
||||
});
|
||||
connect(actExportPdf, &QAction::triggered, this, [this]{
|
||||
QDialog dlg(this);
|
||||
dlg.setWindowTitle(tr("Export to PDF"));
|
||||
auto* layout = new QVBoxLayout(&dlg);
|
||||
auto* form = new QFormLayout();
|
||||
auto* scope = new QComboBox(&dlg);
|
||||
scope->addItems({tr("Current diagram"), tr("All diagrams")});
|
||||
auto* pageNumbers = new QCheckBox(tr("Number pages in footer (NUMBER field)"), &dlg);
|
||||
pageNumbers->setChecked(false);
|
||||
form->addRow(tr("Export scope:"), scope);
|
||||
form->addRow(QString(), pageNumbers);
|
||||
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());
|
||||
});
|
||||
}
|
||||
|
||||
static QString currencySymbolForLocale(const QLocale& loc) {
|
||||
const QString sym = loc.currencySymbol(QLocale::CurrencySymbol);
|
||||
if (!sym.isEmpty()) return sym;
|
||||
const auto terr = loc.territory();
|
||||
if (terr == QLocale::Russia || terr == QLocale::RussianFederation) return QString::fromUtf8("\u20BD");
|
||||
return "$";
|
||||
}
|
||||
|
||||
static QString symbolPlacementDefault(const QLocale& loc, const QString& sym) {
|
||||
const QString sample = loc.toCurrencyString(123.0);
|
||||
if (sample.startsWith(sym)) {
|
||||
const QString rest = sample.mid(sym.size());
|
||||
return rest.startsWith(' ') ? "? 1" : "?1";
|
||||
}
|
||||
if (sample.endsWith(sym)) {
|
||||
const QString rest = sample.left(sample.size() - sym.size());
|
||||
return rest.endsWith(' ') ? "1 ?" : "1?";
|
||||
}
|
||||
return "?1";
|
||||
}
|
||||
|
||||
bool MainWindow::showWelcome() {
|
||||
auto* dlg = new QDialog(this);
|
||||
dlg->setWindowTitle(tr("Welcome"));
|
||||
auto* tabs = new QTabWidget(dlg);
|
||||
|
||||
// General tab
|
||||
auto* general = new QWidget(dlg);
|
||||
auto* genForm = new QFormLayout(general);
|
||||
auto* author = new QLineEdit(general);
|
||||
author->setPlaceholderText(tr("John Doe"));
|
||||
author->setText(m_welcomeState.value("author").toString());
|
||||
auto* title = new QLineEdit(general);
|
||||
title->setPlaceholderText(tr("Main process"));
|
||||
title->setText(m_welcomeState.value("title").toString());
|
||||
auto* organization = new QLineEdit(general);
|
||||
organization->setPlaceholderText(tr("Example.Org Inc."));
|
||||
organization->setText(m_welcomeState.value("organization").toString());
|
||||
auto* initials = new QLineEdit(general);
|
||||
initials->setPlaceholderText(tr("JD"));
|
||||
initials->setText(m_welcomeState.value("initials").toString());
|
||||
autoInitialsFromAuthor(author, initials);
|
||||
connect(author, &QLineEdit::textChanged, this, [this, author, initials]{
|
||||
autoInitialsFromAuthor(author, initials);
|
||||
});
|
||||
genForm->addRow(tr("Title:"), title);
|
||||
genForm->addRow(tr("Organization:"), organization);
|
||||
genForm->addRow(tr("Author:"), author);
|
||||
genForm->addRow(tr("Author's initials:"), initials);
|
||||
general->setLayout(genForm);
|
||||
tabs->addTab(general, tr("General"));
|
||||
|
||||
// Numbering tab (placeholder)
|
||||
auto* numbering = new QWidget(dlg);
|
||||
auto* numLayout = new QVBoxLayout(numbering);
|
||||
numLayout->addStretch();
|
||||
numbering->setLayout(numLayout);
|
||||
tabs->addTab(numbering, tr("Numbering"));
|
||||
|
||||
// Display tab (placeholder)
|
||||
auto* display = new QWidget(dlg);
|
||||
auto* disLayout = new QVBoxLayout(display);
|
||||
disLayout->addStretch();
|
||||
display->setLayout(disLayout);
|
||||
tabs->addTab(display, tr("Display"));
|
||||
|
||||
// ABC Units tab
|
||||
auto* units = new QWidget(dlg);
|
||||
auto* unitsForm = new QFormLayout(units);
|
||||
const QLocale loc;
|
||||
const auto terr = loc.territory();
|
||||
const QString curSym = (terr == QLocale::Russia || terr == QLocale::RussianFederation)
|
||||
? QString::fromUtf8("\u20BD")
|
||||
: currencySymbolForLocale(loc);
|
||||
auto* currency = new QComboBox(units);
|
||||
currency->setEditable(false);
|
||||
currency->addItem(curSym);
|
||||
if (curSym != "$") currency->addItem("$");
|
||||
const QString existingCur = m_welcomeState.value("currency").toString();
|
||||
if (!existingCur.isEmpty() && currency->findText(existingCur) >= 0) {
|
||||
currency->setCurrentText(existingCur);
|
||||
} else {
|
||||
currency->setCurrentIndex(0);
|
||||
}
|
||||
|
||||
auto* placement = new QComboBox(units);
|
||||
placement->addItems({"1?", "?1", "1 ?", "? 1"});
|
||||
const QString placementVal = m_welcomeState.value("currencyPlacement").toString();
|
||||
if (!placementVal.isEmpty() && placement->findText(placementVal) >= 0) {
|
||||
placement->setCurrentText(placementVal);
|
||||
} else {
|
||||
placement->setCurrentText(symbolPlacementDefault(loc, currency->currentText()));
|
||||
}
|
||||
|
||||
auto* timeUnit = new QComboBox(units);
|
||||
const QStringList unitsList = {tr("Seconds"), tr("Minutes"), tr("Hours"), tr("Days"), tr("Weeks"), tr("Months"), tr("Years")};
|
||||
timeUnit->addItems(unitsList);
|
||||
const QString timeVal = m_welcomeState.value("timeUnit").toString();
|
||||
timeUnit->setCurrentText(timeVal.isEmpty() ? tr("Days") : timeVal);
|
||||
|
||||
unitsForm->addRow(tr("Currency:"), currency);
|
||||
unitsForm->addRow(tr("Symbol placement:"), placement);
|
||||
unitsForm->addRow(tr("Time Unit:"), timeUnit);
|
||||
units->setLayout(unitsForm);
|
||||
tabs->addTab(units, tr("ABC Units"));
|
||||
|
||||
// Page Setup tab
|
||||
auto* page = new QWidget(dlg);
|
||||
auto* pageForm = new QFormLayout(page);
|
||||
auto* sizeCombo = new QComboBox(page);
|
||||
sizeCombo->addItems({"A4","A3","A2","A1"});
|
||||
const QString sizeVal = m_welcomeState.value("pageSize").toString();
|
||||
sizeCombo->setCurrentText(sizeVal.isEmpty() ? "A4" : sizeVal);
|
||||
|
||||
auto* orientCombo = new QComboBox(page);
|
||||
orientCombo->addItems({tr("Landscape"),tr("Portrait")});
|
||||
const QString orientVal = m_welcomeState.value("pageOrientation").toString();
|
||||
orientCombo->setCurrentText(orientVal.isEmpty() ? tr("Landscape") : orientVal);
|
||||
|
||||
auto* headerChk = new QCheckBox(tr("With header"), page);
|
||||
headerChk->setChecked(m_welcomeState.value("withHeader", true).toBool());
|
||||
auto* footerChk = new QCheckBox(tr("With footer"), page);
|
||||
footerChk->setChecked(m_welcomeState.value("withFooter", true).toBool());
|
||||
|
||||
pageForm->addRow(tr("Size:"), sizeCombo);
|
||||
pageForm->addRow(tr("Type:"), orientCombo);
|
||||
pageForm->addRow(headerChk);
|
||||
pageForm->addRow(footerChk);
|
||||
page->setLayout(pageForm);
|
||||
tabs->addTab(page, tr("Page Setup"));
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dlg);
|
||||
connect(buttons, &QDialogButtonBox::accepted, dlg, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, dlg, &QDialog::reject);
|
||||
|
||||
auto* layout = new QVBoxLayout(dlg);
|
||||
layout->addWidget(tabs);
|
||||
layout->addWidget(buttons);
|
||||
dlg->setLayout(layout);
|
||||
|
||||
if (dlg->exec() != QDialog::Accepted) {
|
||||
dlg->deleteLater();
|
||||
return false;
|
||||
}
|
||||
|
||||
m_welcomeDialog = dlg;
|
||||
m_welcomeState["author"] = author->text();
|
||||
m_welcomeState["title"] = title->text();
|
||||
m_welcomeState["organization"] = organization->text();
|
||||
m_welcomeState["initials"] = initials->text();
|
||||
m_welcomeState["currency"] = currency->currentText();
|
||||
m_welcomeState["currencyPlacement"] = placement->currentText();
|
||||
m_welcomeState["timeUnit"] = timeUnit->currentText();
|
||||
m_welcomeState["pageSize"] = sizeCombo->currentText();
|
||||
m_welcomeState["pageOrientation"] = orientCombo->currentText();
|
||||
m_welcomeState["withHeader"] = headerChk->isChecked();
|
||||
m_welcomeState["withFooter"] = footerChk->isChecked();
|
||||
|
||||
dlg->deleteLater();
|
||||
return true;
|
||||
}
|
||||
|
||||
void MainWindow::resetScene() {
|
||||
auto* old = m_scene;
|
||||
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;
|
||||
markDirty(true);
|
||||
});
|
||||
m_scene->applyMeta(m_welcomeState);
|
||||
if (m_view) m_view->setScene(m_scene);
|
||||
if (old) old->deleteLater();
|
||||
}
|
||||
|
||||
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);
|
||||
m_currentFile.clear();
|
||||
markDirty(false);
|
||||
}
|
||||
|
||||
bool MainWindow::promptStartup() {
|
||||
QMessageBox box(this);
|
||||
box.setWindowTitle(tr("Start"));
|
||||
box.setText(tr("Start a new diagram or open an existing one?"));
|
||||
QPushButton* newBtn = box.addButton(tr("New"), QMessageBox::AcceptRole);
|
||||
QPushButton* openBtn = box.addButton(tr("Open…"), QMessageBox::ActionRole);
|
||||
QPushButton* cancelBtn = box.addButton(QMessageBox::Cancel);
|
||||
box.exec();
|
||||
if (box.clickedButton() == cancelBtn) return false;
|
||||
if (box.clickedButton() == openBtn) {
|
||||
const QString path = QFileDialog::getOpenFileName(this, tr("Open diagram"), QString(), tr(kDiagramFileFilter));
|
||||
if (path.isEmpty()) return false;
|
||||
return loadDiagramFromPath(path);
|
||||
}
|
||||
if (!showWelcome()) return false;
|
||||
resetScene();
|
||||
m_scene->createBlockAt(QPointF(-100, -50));
|
||||
m_currentFile.clear();
|
||||
markDirty(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MainWindow::loadDiagramFromPath(const QString& path) {
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
QMessageBox::warning(this, tr("Open failed"), tr("Could not open file for reading."));
|
||||
return false;
|
||||
}
|
||||
const auto doc = QJsonDocument::fromJson(f.readAll());
|
||||
f.close();
|
||||
if (!doc.isObject()) {
|
||||
QMessageBox::warning(this, tr("Open failed"), tr("File format is invalid."));
|
||||
return false;
|
||||
}
|
||||
|
||||
const QVariantMap root = doc.object().toVariantMap();
|
||||
resetScene();
|
||||
if (!m_scene->importFromVariant(root)) {
|
||||
QMessageBox::warning(this, tr("Open failed"), tr("Diagram data missing or corrupted."));
|
||||
return false;
|
||||
}
|
||||
if (root.contains("meta")) {
|
||||
m_welcomeState = root.value("meta").toMap();
|
||||
}
|
||||
if (m_actColorfulMode && m_actDarkMode && m_actFollowSystemTheme) {
|
||||
const QSignalBlocker b1(m_actColorfulMode);
|
||||
const QSignalBlocker b2(m_actDarkMode);
|
||||
const QSignalBlocker b3(m_actFollowSystemTheme);
|
||||
m_actColorfulMode->setChecked(m_welcomeState.value("colorfulMode", false).toBool());
|
||||
m_actDarkMode->setChecked(m_welcomeState.value("darkMode", false).toBool());
|
||||
m_actFollowSystemTheme->setChecked(m_welcomeState.value("followSystemTheme", false).toBool());
|
||||
m_actDarkMode->setEnabled(!m_actFollowSystemTheme->isChecked());
|
||||
}
|
||||
if (m_actArrowColor && m_actBorderColor && m_actFontColor) {
|
||||
const bool colorful = m_welcomeState.value("colorfulMode", false).toBool();
|
||||
m_actArrowColor->setEnabled(colorful);
|
||||
m_actBorderColor->setEnabled(colorful);
|
||||
m_actFontColor->setEnabled(colorful);
|
||||
}
|
||||
m_welcomeState["resolvedDarkMode"] = effectiveDarkMode();
|
||||
applyAppPalette(m_welcomeState.value("resolvedDarkMode").toBool());
|
||||
m_scene->applyMeta(m_welcomeState);
|
||||
m_currentFile = path;
|
||||
markDirty(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MainWindow::exportPdf(bool allDiagrams, bool numberPages) {
|
||||
QString path = QFileDialog::getSaveFileName(this, tr("Export to PDF"), QString(), tr(kPdfFileFilter));
|
||||
if (path.isEmpty()) return false;
|
||||
if (QFileInfo(path).suffix().isEmpty()) path += ".pdf";
|
||||
|
||||
QPdfWriter writer(path);
|
||||
writer.setPageSize(QPageSize(pageSizeFromMeta(m_welcomeState)));
|
||||
writer.setPageOrientation(pageOrientationFromMeta(m_welcomeState));
|
||||
writer.setResolution(96);
|
||||
writer.setTitle(tr("IDEF0 Diagram Export"));
|
||||
|
||||
QPainter painter(&writer);
|
||||
if (!painter.isActive()) {
|
||||
QMessageBox::warning(this, tr("Export failed"), tr("Could not initialize PDF painter."));
|
||||
return false;
|
||||
}
|
||||
const QVariantMap originalMeta = m_scene->meta();
|
||||
|
||||
int pageNo = 0;
|
||||
auto renderScene = [&](QGraphicsScene* scene, bool newPage) {
|
||||
if (newPage) writer.newPage();
|
||||
++pageNo;
|
||||
if (auto* ds = qobject_cast<DiagramScene*>(scene)) {
|
||||
QVariantMap meta = ds->meta();
|
||||
if (numberPages) {
|
||||
meta["footerNumberOverride"] = QString::number(pageNo);
|
||||
} else {
|
||||
meta.remove("footerNumberOverride");
|
||||
}
|
||||
ds->applyMeta(meta);
|
||||
}
|
||||
const QRect target = writer.pageLayout().paintRectPixels(writer.resolution());
|
||||
scene->render(&painter, QRectF(target), scene->sceneRect(), Qt::KeepAspectRatio);
|
||||
};
|
||||
|
||||
if (!allDiagrams) {
|
||||
renderScene(m_scene, false);
|
||||
m_scene->applyMeta(originalMeta);
|
||||
painter.end();
|
||||
return true;
|
||||
}
|
||||
|
||||
const QVariantMap exported = m_scene->exportToVariant();
|
||||
const QVariantMap children = exported.value("children").toMap();
|
||||
QVector<QString> keys;
|
||||
QSet<QString> visited;
|
||||
std::function<void(const QString&, const QVariantMap&)> collectKeys;
|
||||
collectKeys = [&](const QString& prefix, const QVariantMap& diagramMap) {
|
||||
const QJsonObject root = QJsonObject::fromVariantMap(diagramMap);
|
||||
const QJsonArray blocks = root.value("blocks").toArray();
|
||||
for (const auto& vb : blocks) {
|
||||
const QJsonObject bo = vb.toObject();
|
||||
if (!bo.value("hasDecomp").toBool(false)) continue; // dog-eared: no subdiagram export
|
||||
const int id = bo.value("id").toInt(-1);
|
||||
if (id < 0) continue;
|
||||
const QString key = prefix.isEmpty() ? QString::number(id) : (prefix + "/" + QString::number(id));
|
||||
if (!children.contains(key) || visited.contains(key)) continue;
|
||||
visited.insert(key);
|
||||
keys.push_back(key);
|
||||
collectKeys(key, children.value(key).toMap());
|
||||
}
|
||||
};
|
||||
collectKeys(QString(), exported.value("diagram").toMap());
|
||||
|
||||
DiagramScene tempScene;
|
||||
tempScene.applyMeta(m_welcomeState);
|
||||
|
||||
QVariantMap mapRoot;
|
||||
mapRoot["diagram"] = exported.value("diagram");
|
||||
mapRoot["children"] = children;
|
||||
bool first = true;
|
||||
if (tempScene.importFromVariant(mapRoot)) {
|
||||
tempScene.applyMeta(m_welcomeState);
|
||||
renderScene(&tempScene, false);
|
||||
first = false;
|
||||
}
|
||||
|
||||
for (const QString& key : keys) {
|
||||
QVariantMap mapOne;
|
||||
mapOne["diagram"] = children.value(key).toMap();
|
||||
|
||||
QVariantMap relChildren;
|
||||
const QString prefix = key + "/";
|
||||
for (auto it = children.cbegin(); it != children.cend(); ++it) {
|
||||
if (it.key().startsWith(prefix)) {
|
||||
relChildren.insert(it.key().mid(prefix.size()), it.value());
|
||||
}
|
||||
}
|
||||
mapOne["children"] = relChildren;
|
||||
|
||||
if (!tempScene.importFromVariant(mapOne)) continue;
|
||||
tempScene.applyMeta(m_welcomeState);
|
||||
renderScene(&tempScene, !first);
|
||||
first = false;
|
||||
}
|
||||
|
||||
m_scene->applyMeta(originalMeta);
|
||||
painter.end();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MainWindow::effectiveDarkMode() const {
|
||||
const bool followSystem = m_welcomeState.value("followSystemTheme", false).toBool();
|
||||
if (!followSystem) {
|
||||
return m_welcomeState.value("darkMode", false).toBool();
|
||||
}
|
||||
const Qt::ColorScheme scheme = qApp->styleHints()->colorScheme();
|
||||
if (scheme == Qt::ColorScheme::Dark) return true;
|
||||
if (scheme == Qt::ColorScheme::Light) return false;
|
||||
return m_welcomeState.value("darkMode", false).toBool();
|
||||
}
|
||||
|
||||
void MainWindow::applyAppPalette(bool darkMode) {
|
||||
static const QPalette defaultPalette = qApp->palette();
|
||||
if (!darkMode) {
|
||||
qApp->setPalette(defaultPalette);
|
||||
return;
|
||||
}
|
||||
QPalette p;
|
||||
p.setColor(QPalette::Window, QColor(36, 36, 36));
|
||||
p.setColor(QPalette::WindowText, QColor(230, 230, 230));
|
||||
p.setColor(QPalette::Base, QColor(26, 26, 26));
|
||||
p.setColor(QPalette::AlternateBase, QColor(42, 42, 42));
|
||||
p.setColor(QPalette::ToolTipBase, QColor(46, 46, 46));
|
||||
p.setColor(QPalette::ToolTipText, QColor(230, 230, 230));
|
||||
p.setColor(QPalette::Text, QColor(230, 230, 230));
|
||||
p.setColor(QPalette::Button, QColor(46, 46, 46));
|
||||
p.setColor(QPalette::ButtonText, QColor(230, 230, 230));
|
||||
p.setColor(QPalette::BrightText, QColor(255, 90, 90));
|
||||
p.setColor(QPalette::Highlight, QColor(75, 110, 180));
|
||||
p.setColor(QPalette::HighlightedText, QColor(255, 255, 255));
|
||||
qApp->setPalette(p);
|
||||
}
|
||||
|
||||
bool MainWindow::openDiagramPath(const QString& path) {
|
||||
return loadDiagramFromPath(path);
|
||||
}
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent* e) {
|
||||
if (!m_dirty) {
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
QMessageBox box(this);
|
||||
box.setWindowTitle(tr("Unsaved changes"));
|
||||
box.setText(tr("You have unsaved changes. Quit anyway?"));
|
||||
QPushButton* yes = box.addButton(tr("Yes"), QMessageBox::AcceptRole);
|
||||
QPushButton* no = box.addButton(tr("No"), QMessageBox::RejectRole);
|
||||
box.setDefaultButton(no);
|
||||
box.exec();
|
||||
if (box.clickedButton() == yes) {
|
||||
e->accept();
|
||||
} else {
|
||||
e->ignore();
|
||||
}
|
||||
}
|
||||
|
||||
bool MainWindow::eventFilter(QObject* obj, QEvent* event) {
|
||||
if (obj == m_view->viewport()) {
|
||||
if (event->type() == QEvent::Gesture) {
|
||||
auto* ge = static_cast<QGestureEvent*>(event);
|
||||
if (QGesture* g = ge->gesture(Qt::PinchGesture)) {
|
||||
auto* pinch = static_cast<QPinchGesture*>(g);
|
||||
if (pinch->state() == Qt::GestureStarted) {
|
||||
m_pinchHandled = false;
|
||||
}
|
||||
if (!m_pinchHandled && pinch->state() == Qt::GestureFinished) {
|
||||
const qreal scale = pinch->totalScaleFactor();
|
||||
const QPointF viewPt = pinch->centerPoint();
|
||||
const QPointF scenePt = m_view->mapToScene(viewPt.toPoint());
|
||||
if (scale > 1.15) {
|
||||
const auto itemsUnder = m_scene->items(scenePt);
|
||||
for (QGraphicsItem* it : itemsUnder) {
|
||||
if (auto* b = qgraphicsitem_cast<BlockItem*>(it)) {
|
||||
if (m_scene->goDownIntoBlock(b)) {
|
||||
m_pinchHandled = true;
|
||||
event->accept();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (scale < 0.85) {
|
||||
if (m_scene->goUp()) {
|
||||
m_pinchHandled = true;
|
||||
event->accept();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (event->type() == QEvent::NativeGesture) {
|
||||
auto* ng = static_cast<QNativeGestureEvent*>(event);
|
||||
if (ng->gestureType() == Qt::ZoomNativeGesture && !m_pinchHandled) {
|
||||
const qreal delta = ng->value();
|
||||
if (std::abs(delta) > 0.25) {
|
||||
const QPointF scenePt = m_view->mapToScene(ng->position().toPoint());
|
||||
if (delta > 0) {
|
||||
const auto itemsUnder = m_scene->items(scenePt);
|
||||
for (QGraphicsItem* it : itemsUnder) {
|
||||
if (auto* b = qgraphicsitem_cast<BlockItem*>(it)) {
|
||||
if (m_scene->goDownIntoBlock(b)) {
|
||||
m_pinchHandled = true;
|
||||
event->accept();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (delta < 0) {
|
||||
if (m_scene->goUp()) {
|
||||
m_pinchHandled = true;
|
||||
event->accept();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return QMainWindow::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
void MainWindow::autoInitialsFromAuthor(QLineEdit* author, QLineEdit* initials) {
|
||||
if (!author || !initials) return;
|
||||
const QString currentInit = initials->text();
|
||||
if (!initials->isModified() || currentInit.isEmpty()) {
|
||||
const auto parts = author->text().split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
|
||||
QString gen;
|
||||
for (const auto& p : parts) {
|
||||
gen.append(p.left(1).toUpper());
|
||||
}
|
||||
if (!gen.isEmpty()) {
|
||||
QSignalBlocker b(initials);
|
||||
initials->setText(gen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::markDirty(bool dirty) {
|
||||
m_dirty = dirty;
|
||||
updateWindowTitle();
|
||||
}
|
||||
|
||||
void MainWindow::updateWindowTitle() {
|
||||
QString name = m_currentFile.isEmpty() ? tr("Untitled") : QFileInfo(m_currentFile).fileName();
|
||||
if (m_dirty) name += " *";
|
||||
setWindowTitle(name + tr(" — IDEF0 editor"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,50 @@
|
|||
#pragma once
|
||||
#include <QMainWindow>
|
||||
#include <QVariant>
|
||||
class QLineEdit;
|
||||
class QCloseEvent;
|
||||
|
||||
class QGraphicsView;
|
||||
class DiagramScene;
|
||||
class QDialog;
|
||||
class QAction;
|
||||
|
||||
class MainWindow final : public QMainWindow {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MainWindow(QWidget* parent = nullptr);
|
||||
explicit MainWindow(const QString& startupPath = QString(), QWidget* parent = nullptr);
|
||||
bool openDiagramPath(const QString& path);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||
|
||||
private:
|
||||
QGraphicsView* m_view = nullptr;
|
||||
DiagramScene* m_scene = nullptr;
|
||||
QDialog* m_welcomeDialog = nullptr;
|
||||
QVariantMap m_welcomeState;
|
||||
QString m_currentFile;
|
||||
bool m_dirty = false;
|
||||
bool m_pinchHandled = false;
|
||||
QAction* m_actColorfulMode = nullptr;
|
||||
QAction* m_actFollowSystemTheme = nullptr;
|
||||
QAction* m_actDarkMode = nullptr;
|
||||
QAction* m_actArrowColor = nullptr;
|
||||
QAction* m_actBorderColor = nullptr;
|
||||
QAction* m_actFontColor = nullptr;
|
||||
|
||||
void setupUi();
|
||||
void setupActions();
|
||||
bool showWelcome();
|
||||
void closeEvent(QCloseEvent* e) override;
|
||||
void resetScene();
|
||||
void newDiagram();
|
||||
bool promptStartup();
|
||||
bool loadDiagramFromPath(const QString& path);
|
||||
bool exportPdf(bool allDiagrams, bool numberPages);
|
||||
bool effectiveDarkMode() const;
|
||||
void applyAppPalette(bool darkMode);
|
||||
void autoInitialsFromAuthor(QLineEdit* author, QLineEdit* initials);
|
||||
void markDirty(bool dirty);
|
||||
void updateWindowTitle();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,16 +10,32 @@
|
|||
#include <QCursor>
|
||||
#include <QInputDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QDebug>
|
||||
#include <QObject>
|
||||
#include "DiagramScene.h"
|
||||
#include <algorithm>
|
||||
#include <QSet>
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
static bool almostEqual(qreal a, qreal b, qreal eps = 1e-6);
|
||||
static bool almostEqual(const QPointF& a, const QPointF& b, qreal eps = 1e-6);
|
||||
static QPointF scenePortPos(const ArrowItem::Endpoint& ep);
|
||||
|
||||
static QRectF labelDragHitRect(const QGraphicsSimpleTextItem* item) {
|
||||
if (!item || !item->isVisible()) return QRectF();
|
||||
// Generous hit area makes label dragging much less fiddly.
|
||||
return item->sceneBoundingRect().adjusted(-16, -10, 16, 10);
|
||||
}
|
||||
QColor ArrowItem::s_lineColor = QColor(10, 10, 10);
|
||||
QColor ArrowItem::s_textColor = QColor(10, 10, 10);
|
||||
|
||||
ArrowItem::ArrowItem(QGraphicsItem* parent)
|
||||
: QGraphicsPathItem(parent)
|
||||
{
|
||||
QPen pen(QColor(10,10,10));
|
||||
QPen pen(s_lineColor);
|
||||
pen.setWidthF(1.4);
|
||||
pen.setCapStyle(Qt::RoundCap);
|
||||
pen.setJoinStyle(Qt::RoundJoin);
|
||||
|
|
@ -28,15 +44,21 @@ ArrowItem::ArrowItem(QGraphicsItem* parent)
|
|||
|
||||
setAcceptHoverEvents(true);
|
||||
setAcceptedMouseButtons(Qt::LeftButton);
|
||||
setFlag(QGraphicsItem::ItemIsSelectable, true);
|
||||
|
||||
m_labelItem = new QGraphicsSimpleTextItem(this);
|
||||
m_labelItem->setBrush(QColor(10, 10, 10));
|
||||
m_labelItem->setBrush(s_textColor);
|
||||
m_labelItem->setZValue(2);
|
||||
m_labelItem->setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
|
||||
m_labelItem->setVisible(false);
|
||||
m_labelOffset = QPointF(0, 0);
|
||||
}
|
||||
|
||||
void ArrowItem::setVisualTheme(const QColor& lineColor, const QColor& textColor) {
|
||||
s_lineColor = lineColor;
|
||||
s_textColor = textColor;
|
||||
}
|
||||
|
||||
ArrowItem::~ArrowItem() {
|
||||
if (m_from.block) m_from.block->removeArrow(this);
|
||||
if (m_to.block) m_to.block->removeArrow(this);
|
||||
|
|
@ -49,7 +71,7 @@ void ArrowItem::setLabel(const QString& text) {
|
|||
m_label = text;
|
||||
if (m_labelItem) {
|
||||
m_labelItem->setText(m_label);
|
||||
m_labelItem->setVisible(!m_label.isEmpty());
|
||||
m_labelItem->setVisible(!m_label.isEmpty() && !m_labelHidden);
|
||||
}
|
||||
updateLabelItem(computePolyline());
|
||||
}
|
||||
|
|
@ -66,6 +88,65 @@ void ArrowItem::setLabelOffset(const QPointF& off) {
|
|||
updateLabelItem(m_lastPolyline.isEmpty() ? computePolyline() : m_lastPolyline);
|
||||
}
|
||||
|
||||
void ArrowItem::setLabelHidden(bool hidden) {
|
||||
m_labelHidden = hidden;
|
||||
updateLabelItem(m_lastPolyline.isEmpty() ? computePolyline() : m_lastPolyline);
|
||||
}
|
||||
|
||||
void ArrowItem::setLabelInherited(bool inherited) {
|
||||
m_labelInherited = inherited;
|
||||
}
|
||||
|
||||
void ArrowItem::setLabelSource(ArrowItem* src) {
|
||||
m_labelSource = src;
|
||||
}
|
||||
|
||||
ArrowItem* ArrowItem::labelSourceRoot() const {
|
||||
const ArrowItem* cur = this;
|
||||
QSet<const ArrowItem*> visited;
|
||||
while (cur) {
|
||||
if (visited.contains(cur)) break;
|
||||
visited.insert(cur);
|
||||
if (!cur->m_labelSource) return const_cast<ArrowItem*>(cur);
|
||||
cur = cur->m_labelSource;
|
||||
}
|
||||
return const_cast<ArrowItem*>(cur);
|
||||
}
|
||||
|
||||
void ArrowItem::setInterfaceStub(const Endpoint& edge, const QString& label) {
|
||||
m_isInterface = true;
|
||||
m_interfaceStubOnly = true;
|
||||
m_labelLocked = true;
|
||||
m_labelHidden = false;
|
||||
m_interfaceEdge = edge;
|
||||
setLabel(label);
|
||||
|
||||
const QPointF posScene = scenePortPos(edge);
|
||||
Endpoint stub = edge;
|
||||
stub.scenePos = posScene;
|
||||
stub.block = nullptr;
|
||||
stub.junction = nullptr;
|
||||
stub.localPos.reset();
|
||||
|
||||
setFrom(stub);
|
||||
setTo(stub);
|
||||
finalize();
|
||||
}
|
||||
|
||||
void ArrowItem::resetInterfaceStub() {
|
||||
if (!m_isInterface || !m_interfaceEdge) return;
|
||||
setInterfaceStub(*m_interfaceEdge, m_label);
|
||||
}
|
||||
|
||||
void ArrowItem::setInterfaceIsStub(bool stub) {
|
||||
m_interfaceStubOnly = stub;
|
||||
updatePath();
|
||||
}
|
||||
|
||||
void ArrowItem::setLabelLocked(bool locked) {
|
||||
m_labelLocked = locked;
|
||||
}
|
||||
|
||||
bool ArrowItem::adjustEndpoint(Endpoint& ep, const QPointF& delta) {
|
||||
if (ep.block) {
|
||||
auto seg = ep.block->portSegment(ep.port);
|
||||
|
|
@ -84,7 +165,11 @@ bool ArrowItem::adjustEndpoint(Endpoint& ep, const QPointF& delta) {
|
|||
if (ep.scenePos) {
|
||||
QPointF p = *ep.scenePos + delta;
|
||||
if (scene()) {
|
||||
const QRectF r = scene()->sceneRect();
|
||||
QRectF r = scene()->sceneRect();
|
||||
if (auto* ds = qobject_cast<DiagramScene*>(scene())) {
|
||||
const QRectF cr = ds->contentRect();
|
||||
if (!cr.isNull()) r = cr;
|
||||
}
|
||||
// snap to nearest edge it was originally on
|
||||
const qreal tol = 4.0;
|
||||
if (std::abs(ep.scenePos->x() - r.left()) < tol) {
|
||||
|
|
@ -165,11 +250,11 @@ static QPointF normalizedOr(const QPointF& v, const QPointF& fallback) {
|
|||
return v / len;
|
||||
}
|
||||
|
||||
static bool almostEqual(qreal a, qreal b, qreal eps = 1e-6) {
|
||||
static bool almostEqual(qreal a, qreal b, qreal eps) {
|
||||
return std::abs(a - b) < eps;
|
||||
}
|
||||
|
||||
static bool almostEqual(const QPointF& a, const QPointF& b, qreal eps = 1e-6) {
|
||||
static bool almostEqual(const QPointF& a, const QPointF& b, qreal eps) {
|
||||
return almostEqual(a.x(), b.x(), eps) && almostEqual(a.y(), b.y(), eps);
|
||||
}
|
||||
|
||||
|
|
@ -423,6 +508,11 @@ QVector<QPointF> ArrowItem::simplifyPolyline(const QVector<QPointF>& ptsIn) {
|
|||
}
|
||||
|
||||
QVector<QPointF> ArrowItem::computePolyline() const {
|
||||
if (m_isInterface && m_interfaceStubOnly && m_interfaceEdge) {
|
||||
const QPointF p = scenePortPos(*m_interfaceEdge);
|
||||
return {p, p};
|
||||
}
|
||||
|
||||
const qreal gap = 24.0;
|
||||
QPointF a = scenePortPos(m_from);
|
||||
QPointF b = m_hasTempEnd ? m_tempEndScene : scenePortPos(m_to);
|
||||
|
|
@ -532,7 +622,19 @@ std::optional<QPointF> ArrowItem::hitTest(const QPointF& scenePos, qreal radius)
|
|||
|
||||
void ArrowItem::updateLabelItem(const QVector<QPointF>& pts) {
|
||||
if (!m_labelItem) return;
|
||||
if (m_label.isEmpty() || pts.size() < 2) {
|
||||
const bool stubOnly = m_isInterface && m_interfaceStubOnly && m_interfaceEdge;
|
||||
if (stubOnly) {
|
||||
if (m_label.isEmpty() || m_labelHidden) {
|
||||
m_labelItem->setVisible(false);
|
||||
return;
|
||||
}
|
||||
const QPointF p = scenePortPos(*m_interfaceEdge);
|
||||
const QRectF br = m_labelItem->boundingRect();
|
||||
m_labelItem->setPos(p + m_labelOffset + QPointF(10, -br.height() * 0.5));
|
||||
m_labelItem->setVisible(true);
|
||||
return;
|
||||
}
|
||||
if (m_labelHidden || m_label.isEmpty() || pts.size() < 2) {
|
||||
m_labelItem->setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
|
@ -571,21 +673,37 @@ void ArrowItem::updateLabelItem(const QVector<QPointF>& pts) {
|
|||
|
||||
// Обновляем маршрут ортогонально, добавляя/схлопывая сегменты при необходимости.
|
||||
void ArrowItem::updatePath() {
|
||||
QPen themedPen = pen();
|
||||
themedPen.setColor(s_lineColor);
|
||||
setPen(themedPen);
|
||||
if (m_labelItem) {
|
||||
m_labelItem->setBrush(s_textColor);
|
||||
}
|
||||
|
||||
const QRectF oldSceneRect = mapRectToScene(boundingRect());
|
||||
|
||||
QVector<QPointF> pts = computePolyline();
|
||||
m_lastPolyline = pts;
|
||||
|
||||
QPainterPath p;
|
||||
if (!pts.isEmpty()) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
// interface stub circle at edge
|
||||
if (m_isInterface && m_interfaceEdge && m_interfaceStubOnly) {
|
||||
const QPointF stubPos = scenePortPos(*m_interfaceEdge);
|
||||
const qreal r = 6.0;
|
||||
p.addEllipse(stubPos, r, r);
|
||||
}
|
||||
|
||||
// наконечник стрелки
|
||||
if (pts.size() >= 2) {
|
||||
// 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];
|
||||
drawArrowHead(p, tip, from);
|
||||
|
|
@ -605,10 +723,19 @@ void ArrowItem::updatePath() {
|
|||
}
|
||||
|
||||
void ArrowItem::hoverMoveEvent(QGraphicsSceneHoverEvent* e) {
|
||||
// Keep label geometry in sync before hit-testing drag area.
|
||||
updateLabelItem(m_lastPolyline.isEmpty() ? computePolyline() : m_lastPolyline);
|
||||
|
||||
const qreal tol = 6.0;
|
||||
const QVector<QPointF> pts = computePolyline();
|
||||
QPointF pos = e->scenePos();
|
||||
|
||||
if (!m_labelLocked && m_labelItem && labelDragHitRect(m_labelItem).contains(pos)) {
|
||||
setCursor(Qt::SizeAllCursor);
|
||||
QGraphicsPathItem::hoverMoveEvent(e);
|
||||
return;
|
||||
}
|
||||
|
||||
Qt::CursorShape cursor = Qt::ArrowCursor;
|
||||
for (int i = 0; i + 1 < pts.size(); ++i) {
|
||||
const QPointF a = pts[i];
|
||||
|
|
@ -629,8 +756,10 @@ void ArrowItem::mousePressEvent(QGraphicsSceneMouseEvent* e) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (m_labelItem && m_labelItem->isVisible() &&
|
||||
m_labelItem->sceneBoundingRect().adjusted(-6, -4, 6, 4).contains(e->scenePos())) {
|
||||
// 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())) {
|
||||
m_labelDragging = true;
|
||||
m_labelDragLastScene = e->scenePos();
|
||||
setCursor(Qt::SizeAllCursor);
|
||||
|
|
@ -642,15 +771,16 @@ void ArrowItem::mousePressEvent(QGraphicsSceneMouseEvent* e) {
|
|||
const QVector<QPointF> pts = computePolyline();
|
||||
const QPointF pos = e->scenePos();
|
||||
|
||||
// Проверим, не жмем ли по началу/концу для сдвига точки соединения.
|
||||
// Shift+drag endpoint: slide connection along the same side.
|
||||
const bool shift = e->modifiers().testFlag(Qt::ShiftModifier);
|
||||
if (!pts.isEmpty()) {
|
||||
if (QLineF(pos, pts.first()).length() <= tol) {
|
||||
if (shift && QLineF(pos, pts.first()).length() <= tol) {
|
||||
m_dragPart = DragPart::FromEnd;
|
||||
m_lastDragScenePos = pos;
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
if (QLineF(pos, pts.last()).length() <= tol) {
|
||||
if (shift && QLineF(pos, pts.last()).length() <= tol) {
|
||||
m_dragPart = DragPart::ToEnd;
|
||||
m_lastDragScenePos = pos;
|
||||
e->accept();
|
||||
|
|
@ -738,6 +868,8 @@ void ArrowItem::mouseMoveEvent(QGraphicsSceneMouseEvent* e) {
|
|||
case DragPart::BottomHorizontal:
|
||||
m_bottomOffset += delta.y();
|
||||
break;
|
||||
case DragPart::FromEnd:
|
||||
case DragPart::ToEnd:
|
||||
case DragPart::None:
|
||||
break;
|
||||
}
|
||||
|
|
@ -778,12 +910,18 @@ void ArrowItem::mouseReleaseEvent(QGraphicsSceneMouseEvent* e) {
|
|||
}
|
||||
|
||||
void ArrowItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* e) {
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
if (e->button() == Qt::LeftButton && !m_labelLocked) {
|
||||
bool ok = false;
|
||||
const QString text = QInputDialog::getText(nullptr, "Arrow label", "Label:", QLineEdit::Normal, m_label, &ok);
|
||||
const QString text = QInputDialog::getText(nullptr, QObject::tr("Arrow label"), QObject::tr("Label:"), QLineEdit::Normal, m_label, &ok);
|
||||
if (ok) {
|
||||
setLabel(text);
|
||||
setLabelHidden(false);
|
||||
setLabelInherited(false);
|
||||
setLabelSource(nullptr);
|
||||
if (text.isEmpty()) m_labelOffset = QPointF();
|
||||
if (auto* ds = qobject_cast<DiagramScene*>(scene())) {
|
||||
ds->propagateLabelFrom(this);
|
||||
}
|
||||
}
|
||||
e->accept();
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
#pragma once
|
||||
#include <QGraphicsPathItem>
|
||||
#include <QPointer>
|
||||
#include <QSet>
|
||||
#include <optional>
|
||||
#include <QString>
|
||||
#include <QColor>
|
||||
|
||||
#include "BlockItem.h"
|
||||
class JunctionItem;
|
||||
|
|
@ -36,10 +38,26 @@ public:
|
|||
QPointF labelOffset() const { return m_labelOffset; }
|
||||
void setOffsets(qreal bend, qreal top, qreal bottom);
|
||||
void setLabelOffset(const QPointF& off);
|
||||
void setLabelHidden(bool hidden);
|
||||
bool isLabelHidden() const { return m_labelHidden; }
|
||||
void setLabelInherited(bool inherited);
|
||||
bool isLabelInherited() const { return m_labelInherited; }
|
||||
void setLabelSource(ArrowItem* src);
|
||||
ArrowItem* labelSource() const { return m_labelSource; }
|
||||
ArrowItem* labelSourceRoot() const;
|
||||
qreal bendOffset() const { return m_bendOffset; }
|
||||
qreal topOffset() const { return m_topOffset; }
|
||||
qreal bottomOffset() const { return m_bottomOffset; }
|
||||
int type() const override { return Type; }
|
||||
bool isInterface() const { return m_isInterface; }
|
||||
bool isInterfaceStub() const { return m_isInterface && m_interfaceStubOnly; }
|
||||
bool isLabelLocked() const { return m_labelLocked; }
|
||||
std::optional<Endpoint> interfaceEdge() const { return m_interfaceEdge; }
|
||||
void setInterfaceStub(const Endpoint& edge, const QString& label);
|
||||
void setInterfaceIsStub(bool stub);
|
||||
void resetInterfaceStub();
|
||||
void setLabelLocked(bool locked);
|
||||
static void setVisualTheme(const QColor& lineColor, const QColor& textColor);
|
||||
|
||||
void updatePath();
|
||||
std::optional<QPointF> hitTest(const QPointF& scenePos, qreal radius) const;
|
||||
|
|
@ -56,10 +74,19 @@ private:
|
|||
qreal m_bottomOffset = 0.0;
|
||||
QString m_label;
|
||||
QGraphicsSimpleTextItem* m_labelItem = nullptr;
|
||||
bool m_labelHidden = false;
|
||||
bool m_labelInherited = false;
|
||||
QPointF m_labelOffset; // in scene-space relative to anchor point
|
||||
bool m_labelDragging = false;
|
||||
QPointF m_labelDragLastScene;
|
||||
QVector<QPointF> m_lastPolyline;
|
||||
bool m_isInterface = false;
|
||||
bool m_interfaceStubOnly = false;
|
||||
bool m_labelLocked = false;
|
||||
std::optional<Endpoint> m_interfaceEdge;
|
||||
ArrowItem* m_labelSource = nullptr;
|
||||
static QColor s_lineColor;
|
||||
static QColor s_textColor;
|
||||
|
||||
DragPart m_dragPart = DragPart::None;
|
||||
QPointF m_lastDragScenePos;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,30 @@
|
|||
#include "BlockItem.h"
|
||||
#include "ArrowItem.h"
|
||||
#include "items/DiagramScene.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <cmath>
|
||||
#include <QPainter>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QInputDialog>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QCheckBox>
|
||||
#include <QVBoxLayout>
|
||||
#include <QLocale>
|
||||
|
||||
#include "DiagramScene.h"
|
||||
|
||||
QString BlockItem::s_currencySymbol = QStringLiteral("$");
|
||||
QString BlockItem::s_currencyPlacement = QStringLiteral("1?");
|
||||
QColor BlockItem::s_foregroundColor = QColor(20, 20, 20);
|
||||
QColor BlockItem::s_backgroundColor = QColor(255, 255, 255);
|
||||
QColor BlockItem::s_borderColor = QColor(30, 30, 30);
|
||||
QColor BlockItem::s_fontColor = QColor(20, 20, 20);
|
||||
QColor BlockItem::s_selectedBackgroundColor = QColor(240, 240, 255);
|
||||
|
||||
BlockItem::BlockItem(QString title, QGraphicsItem* parent, int id)
|
||||
: QGraphicsObject(parent),
|
||||
|
|
@ -27,19 +45,44 @@ void BlockItem::paint(QPainter* p, const QStyleOptionGraphicsItem*, QWidget*) {
|
|||
|
||||
// фон
|
||||
p->setPen(Qt::NoPen);
|
||||
p->setBrush(isSelected() ? QColor(240,240,255) : QColor(250,250,250));
|
||||
p->setBrush(isSelected() ? s_selectedBackgroundColor : s_backgroundColor);
|
||||
p->drawRoundedRect(m_rect, 6, 6);
|
||||
|
||||
// рамка
|
||||
QPen pen(QColor(30,30,30));
|
||||
QPen pen(s_borderColor);
|
||||
pen.setWidthF(1.5);
|
||||
p->setPen(pen);
|
||||
p->setBrush(Qt::NoBrush);
|
||||
p->drawRoundedRect(m_rect, 6, 6);
|
||||
|
||||
// dog-ear indicator when no decomposition
|
||||
if (!m_hasDecomposition) {
|
||||
const qreal ear = 12.0;
|
||||
QPainterPath fold;
|
||||
fold.moveTo(m_rect.topLeft());
|
||||
fold.lineTo(m_rect.topLeft() + QPointF(ear, 0));
|
||||
fold.lineTo(m_rect.topLeft() + QPointF(0, ear));
|
||||
fold.closeSubpath();
|
||||
p->setBrush(s_backgroundColor.darker(108));
|
||||
p->drawPath(fold);
|
||||
p->setPen(QPen(s_borderColor.lighter(130), 1.0));
|
||||
p->drawLine(m_rect.topLeft() + QPointF(ear, 0), m_rect.topLeft() + QPointF(0, ear));
|
||||
}
|
||||
|
||||
// заголовок
|
||||
p->setPen(QColor(20,20,20));
|
||||
p->setPen(s_fontColor);
|
||||
p->drawText(m_rect.adjusted(10, 8, -10, -8), Qt::AlignLeft | Qt::AlignTop, m_title);
|
||||
if (!m_number.isEmpty()) {
|
||||
QFont f = p->font();
|
||||
f.setBold(true);
|
||||
p->setFont(f);
|
||||
p->setPen(s_foregroundColor);
|
||||
p->drawText(m_rect.adjusted(8, 4, -8, -8), Qt::AlignRight | Qt::AlignBottom, m_number);
|
||||
}
|
||||
if (m_price.has_value()) {
|
||||
p->setPen(s_foregroundColor);
|
||||
p->drawText(m_rect.adjusted(8, 4, -8, -8), Qt::AlignLeft | Qt::AlignBottom, formattedPrice());
|
||||
}
|
||||
}
|
||||
|
||||
void BlockItem::setTitle(const QString& t) {
|
||||
|
|
@ -48,6 +91,48 @@ void BlockItem::setTitle(const QString& t) {
|
|||
update();
|
||||
}
|
||||
|
||||
void BlockItem::setNumber(const QString& n) {
|
||||
if (n == m_number) return;
|
||||
m_number = n;
|
||||
update();
|
||||
}
|
||||
|
||||
void BlockItem::setPrice(std::optional<qreal> price) {
|
||||
if (m_price == price) return;
|
||||
m_price = price;
|
||||
update();
|
||||
}
|
||||
|
||||
void BlockItem::setCurrencyFormat(const QString& symbol, const QString& placement) {
|
||||
QString effectiveSymbol = symbol;
|
||||
if (effectiveSymbol.isEmpty()) {
|
||||
const QLocale loc;
|
||||
effectiveSymbol = loc.currencySymbol(QLocale::CurrencySymbol);
|
||||
if (effectiveSymbol.isEmpty()) {
|
||||
const auto terr = loc.territory();
|
||||
if (terr == QLocale::Russia || terr == QLocale::RussianFederation) {
|
||||
effectiveSymbol = QString::fromUtf8("\u20BD");
|
||||
} else {
|
||||
effectiveSymbol = QStringLiteral("$");
|
||||
}
|
||||
}
|
||||
}
|
||||
s_currencySymbol = effectiveSymbol;
|
||||
s_currencyPlacement = placement.isEmpty() ? QStringLiteral("1?") : placement;
|
||||
}
|
||||
|
||||
void BlockItem::setVisualTheme(const QColor& foreground,
|
||||
const QColor& background,
|
||||
const QColor& border,
|
||||
const QColor& font,
|
||||
const QColor& selectedBackground) {
|
||||
s_foregroundColor = foreground;
|
||||
s_backgroundColor = background;
|
||||
s_borderColor = border;
|
||||
s_fontColor = font;
|
||||
s_selectedBackgroundColor = selectedBackground;
|
||||
}
|
||||
|
||||
QPointF BlockItem::portLocalPos(Port p) const {
|
||||
const qreal x0 = m_rect.left();
|
||||
const qreal x1 = m_rect.right();
|
||||
|
|
@ -133,6 +218,20 @@ void BlockItem::removeArrow(ArrowItem* a) {
|
|||
}
|
||||
|
||||
QVariant BlockItem::itemChange(GraphicsItemChange change, const QVariant& value) {
|
||||
if (change == ItemPositionChange) {
|
||||
const QPointF desired = value.toPointF();
|
||||
if (auto* ds = qobject_cast<DiagramScene*>(scene())) {
|
||||
QRectF bounds = ds->contentRect();
|
||||
if (bounds.isNull()) bounds = ds->sceneRect();
|
||||
QRectF rect = boundingRect().translated(desired);
|
||||
QPointF clamped = desired;
|
||||
if (rect.left() < bounds.left()) clamped.setX(desired.x() + (bounds.left() - rect.left()));
|
||||
if (rect.right() > bounds.right()) clamped.setX(clamped.x() - (rect.right() - bounds.right()));
|
||||
if (rect.top() < bounds.top()) clamped.setY(desired.y() + (bounds.top() - rect.top()));
|
||||
if (rect.bottom() > bounds.bottom()) clamped.setY(clamped.y() - (rect.bottom() - bounds.bottom()));
|
||||
return clamped;
|
||||
}
|
||||
}
|
||||
if (change == ItemPositionHasChanged || change == ItemTransformHasChanged) {
|
||||
// уведомим стрелки, что нужно пересчитать геометрию
|
||||
for (auto& a : m_arrows) {
|
||||
|
|
@ -143,9 +242,71 @@ QVariant BlockItem::itemChange(GraphicsItemChange change, const QVariant& value)
|
|||
return QGraphicsObject::itemChange(change, value);
|
||||
}
|
||||
|
||||
QPair<QString, QString> BlockItem::currencyAffixes() {
|
||||
const bool prefix = s_currencyPlacement.startsWith("?");
|
||||
const bool spaced = s_currencyPlacement.contains(' ');
|
||||
if (prefix) {
|
||||
return {s_currencySymbol + (spaced ? " " : ""), QString()};
|
||||
}
|
||||
return {QString(), (spaced ? " " : "") + s_currencySymbol};
|
||||
}
|
||||
|
||||
QString BlockItem::formattedPrice() const {
|
||||
if (!m_price.has_value()) return QString();
|
||||
const auto affixes = currencyAffixes();
|
||||
const QString value = QLocale().toString(*m_price, 'f', 2);
|
||||
return affixes.first + value + affixes.second;
|
||||
}
|
||||
|
||||
void BlockItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* e) {
|
||||
bool ok = false;
|
||||
const QString t = QInputDialog::getText(nullptr, "Rename function", "Title:", QLineEdit::Normal, m_title, &ok);
|
||||
if (ok) setTitle(t);
|
||||
auto* dlg = new QDialog();
|
||||
dlg->setWindowTitle(tr("Edit block"));
|
||||
|
||||
auto* layout = new QVBoxLayout(dlg);
|
||||
auto* form = new QFormLayout();
|
||||
auto* titleEdit = new QLineEdit(m_title, dlg);
|
||||
form->addRow(tr("Title:"), titleEdit);
|
||||
|
||||
auto affixes = currencyAffixes();
|
||||
auto* priceSpin = new QDoubleSpinBox(dlg);
|
||||
priceSpin->setRange(0.0, 1e9);
|
||||
priceSpin->setDecimals(2);
|
||||
priceSpin->setPrefix(affixes.first);
|
||||
priceSpin->setSuffix(affixes.second);
|
||||
priceSpin->setValue(m_price.value_or(0.0));
|
||||
|
||||
auto* priceToggle = new QCheckBox(tr("Set price"), dlg);
|
||||
priceToggle->setChecked(m_price.has_value());
|
||||
priceSpin->setEnabled(m_price.has_value());
|
||||
connect(priceToggle, &QCheckBox::toggled, priceSpin, &QWidget::setEnabled);
|
||||
|
||||
form->addRow(tr("Price:"), priceSpin);
|
||||
form->addRow(QString(), priceToggle);
|
||||
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);
|
||||
|
||||
bool changed = false;
|
||||
if (dlg->exec() == QDialog::Accepted) {
|
||||
if (m_title != titleEdit->text()) {
|
||||
setTitle(titleEdit->text());
|
||||
changed = true;
|
||||
}
|
||||
std::optional<qreal> newPrice;
|
||||
if (priceToggle->isChecked()) newPrice = priceSpin->value();
|
||||
if (m_price != newPrice) {
|
||||
setPrice(newPrice);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
if (auto* ds = qobject_cast<DiagramScene*>(scene())) {
|
||||
ds->requestSnapshot();
|
||||
}
|
||||
}
|
||||
}
|
||||
dlg->deleteLater();
|
||||
QGraphicsObject::mouseDoubleClickEvent(e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
#pragma once
|
||||
#include <QGraphicsObject>
|
||||
#include <QSet>
|
||||
#include <optional>
|
||||
#include <QColor>
|
||||
|
||||
class ArrowItem;
|
||||
|
||||
|
|
@ -18,6 +20,10 @@ public:
|
|||
|
||||
QString title() const { return m_title; }
|
||||
void setTitle(const QString& t);
|
||||
QString number() const { return m_number; }
|
||||
void setNumber(const QString& n);
|
||||
bool hasDecomposition() const { return m_hasDecomposition; }
|
||||
void setHasDecomposition(bool v) { m_hasDecomposition = v; update(); }
|
||||
int id() const { return m_id; }
|
||||
void setId(int id) { m_id = id; }
|
||||
|
||||
|
|
@ -29,6 +35,15 @@ public:
|
|||
void addArrow(ArrowItem* a);
|
||||
void removeArrow(ArrowItem* a);
|
||||
|
||||
std::optional<qreal> price() const { return m_price; }
|
||||
void setPrice(std::optional<qreal> price);
|
||||
static void setCurrencyFormat(const QString& symbol, const QString& placement);
|
||||
static void setVisualTheme(const QColor& foreground,
|
||||
const QColor& background,
|
||||
const QColor& border,
|
||||
const QColor& font,
|
||||
const QColor& selectedBackground);
|
||||
|
||||
signals:
|
||||
void geometryChanged();
|
||||
|
||||
|
|
@ -37,8 +52,22 @@ protected:
|
|||
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent* e) override;
|
||||
|
||||
private:
|
||||
QString formattedPrice() const;
|
||||
static QPair<QString, QString> currencyAffixes();
|
||||
|
||||
QString m_title;
|
||||
QString m_number;
|
||||
QRectF m_rect; // local rect
|
||||
int m_id;
|
||||
bool m_hasDecomposition = false;
|
||||
QSet<ArrowItem*> m_arrows;
|
||||
std::optional<qreal> m_price;
|
||||
|
||||
static QString s_currencySymbol;
|
||||
static QString s_currencyPlacement;
|
||||
static QColor s_foregroundColor;
|
||||
static QColor s_backgroundColor;
|
||||
static QColor s_borderColor;
|
||||
static QColor s_fontColor;
|
||||
static QColor s_selectedBackgroundColor;
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +1,13 @@
|
|||
#pragma once
|
||||
#include <QGraphicsScene>
|
||||
#include <QPointer>
|
||||
#include <optional>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include "BlockItem.h"
|
||||
class ArrowItem;
|
||||
class JunctionItem;
|
||||
class HeaderFooterItem;
|
||||
|
||||
class DiagramScene final : public QGraphicsScene {
|
||||
Q_OBJECT
|
||||
|
|
@ -14,9 +17,21 @@ public:
|
|||
BlockItem* createBlockAt(const QPointF& scenePos);
|
||||
BlockItem* createBlockWithId(const QPointF& scenePos, int id, const QString& title);
|
||||
void requestSnapshot();
|
||||
void applyMeta(const QVariantMap& meta);
|
||||
void updateMeta(const QVariantMap& patch);
|
||||
QVariantMap meta() const { return m_meta; }
|
||||
QRectF contentRect() const { return m_contentRect; }
|
||||
QString currentNodeLabel() const;
|
||||
QString currentDiagramTitle() const;
|
||||
bool goDownIntoSelected();
|
||||
bool goDownIntoBlock(BlockItem* b);
|
||||
bool goUp();
|
||||
void propagateLabelFrom(ArrowItem* root);
|
||||
QVariantMap exportToVariant();
|
||||
bool importFromVariant(const QVariantMap& map);
|
||||
signals:
|
||||
void changed();
|
||||
void metaChanged(const QVariantMap& meta);
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QGraphicsSceneMouseEvent* e) override;
|
||||
|
|
@ -30,10 +45,10 @@ private:
|
|||
enum class Kind { None, Block, Junction, Scene } kind = Kind::None;
|
||||
int id = -1;
|
||||
BlockItem::Port port = BlockItem::Port::Input;
|
||||
QPointF localPos;
|
||||
QPointF scenePos;
|
||||
std::optional<QPointF> localPos;
|
||||
std::optional<QPointF> scenePos;
|
||||
};
|
||||
struct Block { int id; QString title; QPointF pos; };
|
||||
struct Block { int id; QString title; QPointF pos; bool hasDecomp = false; QString number; std::optional<qreal> price; };
|
||||
struct Junction { int id; QPointF pos; };
|
||||
struct Arrow {
|
||||
Endpoint from;
|
||||
|
|
@ -43,13 +58,19 @@ private:
|
|||
qreal bottom = 0.0;
|
||||
QString label;
|
||||
QPointF labelOffset;
|
||||
bool labelHidden = false;
|
||||
bool labelInherited = false;
|
||||
bool isInterface = false;
|
||||
bool isInterfaceStub = false;
|
||||
bool labelLocked = false;
|
||||
Endpoint interfaceEdge;
|
||||
};
|
||||
QVector<Block> blocks;
|
||||
QVector<Junction> junctions;
|
||||
QVector<Arrow> arrows;
|
||||
};
|
||||
|
||||
struct HierNode { int blockId; Snapshot snapshot; };
|
||||
struct HierNode { int blockId; Snapshot snapshot; QString prefix; };
|
||||
|
||||
ArrowItem* m_dragArrow = nullptr;
|
||||
QPointer<BlockItem> m_dragFromBlock;
|
||||
|
|
@ -57,15 +78,20 @@ private:
|
|||
BlockItem::Port m_dragFromPort = BlockItem::Port::Output;
|
||||
int m_nextBlockId = 1;
|
||||
int m_nextJunctionId = 1;
|
||||
QString m_currentPrefix = QStringLiteral("A");
|
||||
bool m_snapshotScheduled = false;
|
||||
bool m_restoringSnapshot = false;
|
||||
bool m_itemDragActive = false;
|
||||
QHash<QGraphicsItem*, QPointF> m_pressPositions;
|
||||
int m_currentBlockId = -1;
|
||||
QVector<HierNode> m_hierarchy;
|
||||
QHash<int, Snapshot> m_children; // blockId -> saved child diagram
|
||||
QHash<QString, Snapshot> m_children; // hierarchical key ("1/2/3") -> saved child diagram
|
||||
ArrowItem* m_dragInterfaceStub = nullptr;
|
||||
QVariantMap m_meta;
|
||||
HeaderFooterItem* m_headerFooter = nullptr;
|
||||
QRectF m_contentRect;
|
||||
|
||||
bool tryStartArrowDrag(const QPointF& scenePos);
|
||||
bool tryStartArrowDrag(const QPointF& scenePos, Qt::KeyboardModifiers mods);
|
||||
bool tryFinishArrowDrag(const QPointF& scenePos);
|
||||
bool tryBranchAtArrow(const QPointF& scenePos);
|
||||
void cancelCurrentDrag();
|
||||
|
|
@ -75,14 +101,22 @@ private:
|
|||
int m_historyIndex = -1;
|
||||
|
||||
Snapshot captureSnapshot() const;
|
||||
void restoreSnapshot(const Snapshot& snap);
|
||||
void restoreSnapshot(const Snapshot& snap, bool resetHistoryState = true);
|
||||
void pushSnapshot();
|
||||
void scheduleSnapshot();
|
||||
void undo();
|
||||
void maybeSnapshotMovedItems();
|
||||
void resetHistory(const Snapshot& base);
|
||||
void ensureFrame();
|
||||
void ensureHeaderFooter();
|
||||
bool childHasContent(int blockId) const;
|
||||
QString currentPathKey() const;
|
||||
QString childKeyFor(int blockId) const;
|
||||
QString assignNumber(BlockItem* b);
|
||||
|
||||
enum class Edge { None, Left, Right, Top, Bottom };
|
||||
Edge hitTestEdge(const QPointF& scenePos, QPointF* outScenePoint = nullptr) const;
|
||||
|
||||
friend QJsonObject endpointToJson(const Snapshot::Endpoint& ep);
|
||||
friend Snapshot::Endpoint endpointFromJson(const QJsonObject& o);
|
||||
};
|
||||
|
|
|
|||
320
src/items/HeaderFooterItem.cpp
Normal file
320
src/items/HeaderFooterItem.cpp
Normal 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);
|
||||
}
|
||||
32
src/items/HeaderFooterItem.h
Normal file
32
src/items/HeaderFooterItem.h
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
#include <QGraphicsObject>
|
||||
#include <QVariantMap>
|
||||
|
||||
class DiagramScene;
|
||||
|
||||
class HeaderFooterItem final : public QGraphicsObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit HeaderFooterItem(DiagramScene* scene);
|
||||
|
||||
void setMeta(const QVariantMap& meta);
|
||||
void setPageRect(const QRectF& rect);
|
||||
void setShowHeaderFooter(bool showHeader, bool showFooter);
|
||||
|
||||
QRectF boundingRect() const override;
|
||||
void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override;
|
||||
|
||||
protected:
|
||||
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent* e) override;
|
||||
|
||||
private:
|
||||
DiagramScene* m_scene = nullptr;
|
||||
QVariantMap m_meta;
|
||||
QRectF m_pageRect;
|
||||
bool m_showHeader = true;
|
||||
bool m_showFooter = true;
|
||||
|
||||
QString metaText(const QString& key, const QString& fallback = QString()) const;
|
||||
bool metaBool(const QString& key, bool fallback = false) const;
|
||||
bool hitHeaderFooter(const QPointF& scenePos) const;
|
||||
};
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
#include "JunctionItem.h"
|
||||
#include "ArrowItem.h"
|
||||
#include "items/DiagramScene.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QStyleOptionGraphicsItem>
|
||||
#include <cmath>
|
||||
|
||||
QColor JunctionItem::s_normalColor = QColor(30, 30, 30);
|
||||
QColor JunctionItem::s_selectedColor = QColor(40, 100, 255);
|
||||
|
||||
JunctionItem::JunctionItem(QGraphicsItem* parent, int id)
|
||||
: QGraphicsObject(parent),
|
||||
m_id(id)
|
||||
|
|
@ -20,10 +24,15 @@ QRectF JunctionItem::boundingRect() const {
|
|||
void JunctionItem::paint(QPainter* p, const QStyleOptionGraphicsItem*, QWidget*) {
|
||||
p->setRenderHint(QPainter::Antialiasing, true);
|
||||
p->setPen(Qt::NoPen);
|
||||
p->setBrush(isSelected() ? QColor(40, 100, 255) : QColor(30, 30, 30));
|
||||
p->setBrush(isSelected() ? s_selectedColor : s_normalColor);
|
||||
p->drawEllipse(QPointF(0, 0), m_radius, m_radius);
|
||||
}
|
||||
|
||||
void JunctionItem::setVisualTheme(const QColor& normalColor, const QColor& selectedColor) {
|
||||
s_normalColor = normalColor;
|
||||
s_selectedColor = selectedColor;
|
||||
}
|
||||
|
||||
void JunctionItem::addArrow(ArrowItem* a) {
|
||||
m_arrows.insert(a);
|
||||
}
|
||||
|
|
@ -38,6 +47,20 @@ bool JunctionItem::hitTest(const QPointF& scenePos, qreal radius) const {
|
|||
}
|
||||
|
||||
QVariant JunctionItem::itemChange(GraphicsItemChange change, const QVariant& value) {
|
||||
if (change == ItemPositionChange) {
|
||||
const QPointF desired = value.toPointF();
|
||||
if (auto* ds = qobject_cast<DiagramScene*>(scene())) {
|
||||
QRectF bounds = ds->contentRect();
|
||||
if (bounds.isNull()) bounds = ds->sceneRect();
|
||||
QRectF rect = boundingRect().translated(desired);
|
||||
QPointF clamped = desired;
|
||||
if (rect.left() < bounds.left()) clamped.setX(desired.x() + (bounds.left() - rect.left()));
|
||||
if (rect.right() > bounds.right()) clamped.setX(clamped.x() - (rect.right() - bounds.right()));
|
||||
if (rect.top() < bounds.top()) clamped.setY(desired.y() + (bounds.top() - rect.top()));
|
||||
if (rect.bottom() > bounds.bottom()) clamped.setY(clamped.y() - (rect.bottom() - bounds.bottom()));
|
||||
return clamped;
|
||||
}
|
||||
}
|
||||
if (change == ItemPositionHasChanged || change == ItemTransformHasChanged) {
|
||||
for (ArrowItem* a : m_arrows) {
|
||||
if (a) a->updatePath();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
#include <QGraphicsObject>
|
||||
#include <QSet>
|
||||
#include <QColor>
|
||||
|
||||
class ArrowItem;
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ public:
|
|||
bool hitTest(const QPointF& scenePos, qreal radius) const;
|
||||
int id() const { return m_id; }
|
||||
void setId(int id) { m_id = id; }
|
||||
static void setVisualTheme(const QColor& normalColor, const QColor& selectedColor);
|
||||
|
||||
protected:
|
||||
QVariant itemChange(GraphicsItemChange change, const QVariant& value) override;
|
||||
|
|
@ -29,4 +31,6 @@ private:
|
|||
qreal m_radius = 5.0;
|
||||
int m_id;
|
||||
QSet<ArrowItem*> m_arrows;
|
||||
static QColor s_normalColor;
|
||||
static QColor s_selectedColor;
|
||||
};
|
||||
|
|
|
|||
62
src/main.cpp
62
src/main.cpp
|
|
@ -1,9 +1,67 @@
|
|||
#include <QApplication>
|
||||
#include <QTranslator>
|
||||
#include <QLocale>
|
||||
#include <QDir>
|
||||
#include <QCoreApplication>
|
||||
#include <QEvent>
|
||||
#include <QFileOpenEvent>
|
||||
#include <functional>
|
||||
#include "MainWindow.h"
|
||||
|
||||
class IdefApplication final : public QApplication {
|
||||
public:
|
||||
using QApplication::QApplication;
|
||||
|
||||
QString pendingFileOpen;
|
||||
std::function<void(const QString&)> fileOpenHandler;
|
||||
|
||||
bool event(QEvent* ev) override {
|
||||
if (ev->type() == QEvent::FileOpen) {
|
||||
auto* foe = static_cast<QFileOpenEvent*>(ev);
|
||||
if (foe && !foe->file().isEmpty()) {
|
||||
if (fileOpenHandler) {
|
||||
fileOpenHandler(foe->file());
|
||||
} else {
|
||||
pendingFileOpen = foe->file();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return QApplication::event(ev);
|
||||
}
|
||||
};
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
QApplication app(argc, argv);
|
||||
MainWindow w;
|
||||
IdefApplication app(argc, argv);
|
||||
|
||||
QTranslator translator;
|
||||
const QLocale locale;
|
||||
const QString baseName = QStringLiteral("idef0_%1").arg(locale.name());
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
QString startupPath;
|
||||
const QStringList args = QCoreApplication::arguments();
|
||||
if (args.size() > 1) {
|
||||
startupPath = args.at(1);
|
||||
}
|
||||
if (!app.pendingFileOpen.isEmpty()) {
|
||||
startupPath = app.pendingFileOpen;
|
||||
}
|
||||
|
||||
MainWindow w(startupPath);
|
||||
app.fileOpenHandler = [&w](const QString& path) {
|
||||
w.openDiagramPath(path);
|
||||
};
|
||||
w.resize(1200, 800);
|
||||
w.show();
|
||||
return app.exec();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue