modified: CMakeLists.txt

modified:   default.nix
	deleted:    packaging/linux/idef0-editor.desktop
	modified:   src/MainWindow.cpp
	modified:   src/MainWindow.h
	modified:   src/items/DiagramScene.cpp
	modified:   src/items/DiagramScene.h
	modified:   src/main.cpp
	modified:   translations/idef0_en.ts
	modified:   translations/idef0_fr.ts
	modified:   translations/idef0_ru.ts
This commit is contained in:
Gregory Bednov 2026-03-04 13:31:20 +03:00
commit 086644ae82
11 changed files with 948 additions and 57 deletions

View file

@ -1,10 +1,10 @@
cmake_minimum_required(VERSION 3.21)
project(idef0_editor LANGUAGES CXX)
project(erlu_idef0_editor LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Widgets)
find_package(Qt6 REQUIRED COMPONENTS Widgets Svg)
qt_standard_project_setup()
@ -21,19 +21,25 @@ qt_add_library(idef0_core SHARED
option(BUILD_COLORS_PLUGIN "Build the Colors plugin" ON)
qt_add_executable(idef0_editor
qt_add_executable(erlu_idef0_editor
src/main.cpp
)
qt_add_resources(erlu_idef0_editor "app_icons"
PREFIX "/icons"
BASE assets/icons
FILES assets/icons/erlu.svg
)
if(APPLE)
set(MACOS_BUNDLE_ID "com.gregorybednov.idef0-editor")
set(MACOS_BUNDLE_NAME "IDEF0 Editor")
set(MACOS_BUNDLE_ID "com.gregorybednov.erlu-idef0-editor")
set(MACOS_BUNDLE_NAME "erlu 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
set_target_properties(erlu_idef0_editor PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_BUNDLE_NAME "${MACOS_BUNDLE_NAME}"
MACOSX_BUNDLE_GUI_IDENTIFIER "${MACOS_BUNDLE_ID}"
@ -41,13 +47,13 @@ if(APPLE)
)
endif()
target_compile_definitions(idef0_editor PRIVATE $<$<NOT:$<CONFIG:Debug>>:QT_NO_DEBUG_OUTPUT>)
target_compile_definitions(erlu_idef0_editor PRIVATE $<$<NOT:$<CONFIG:Debug>>:QT_NO_DEBUG_OUTPUT>)
target_include_directories(idef0_core PRIVATE src)
target_include_directories(idef0_editor PRIVATE src)
target_include_directories(erlu_idef0_editor PRIVATE src)
target_link_libraries(idef0_core PRIVATE Qt6::Widgets)
target_link_libraries(idef0_editor PRIVATE Qt6::Widgets idef0_core)
target_link_libraries(erlu_idef0_editor PRIVATE Qt6::Widgets Qt6::Svg idef0_core)
set_target_properties(idef0_core PROPERTIES
WINDOWS_EXPORT_ALL_SYMBOLS ON
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
@ -69,11 +75,11 @@ if(BUILD_COLORS_PLUGIN)
)
endif()
set_target_properties(idef0_editor PROPERTIES
set_target_properties(erlu_idef0_editor PROPERTIES
INSTALL_RPATH "\$ORIGIN/../lib"
)
install(TARGETS idef0_editor
install(TARGETS erlu_idef0_editor
BUNDLE DESTINATION .
RUNTIME DESTINATION bin
)
@ -90,8 +96,10 @@ if(BUILD_COLORS_PLUGIN)
endif()
if(UNIX AND NOT APPLE)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/packaging/linux/idef0-editor.desktop
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/packaging/linux/erlu-idef0-editor.desktop
DESTINATION share/applications)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/packaging/linux/idef0.xml
DESTINATION share/mime/packages)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/erlu.svg
DESTINATION share/icons/hicolor/scalable/apps)
endif()

View file

@ -7,7 +7,7 @@ let
qt = pkgs.qt6;
in
stdenv.mkDerivation rec {
pname = "idef0_editor";
pname = "erlu_idef0_editor";
version = "0.1.0";
src = lib.cleanSourceWith {
@ -68,7 +68,7 @@ stdenv.mkDerivation rec {
meta = with lib; {
description = "IDEF0 diagram editor built with Qt 6 Widgets";
license = licenses.lgpl3Plus;
mainProgram = "idef0_editor";
mainProgram = "erlu_idef0_editor";
platforms = platforms.linux;
};
}

View file

@ -1,9 +0,0 @@
[Desktop Entry]
Type=Application
Name=IDEF0 Editor
Comment=IDEF0 diagram editor
Exec=idef0_editor %f
Terminal=false
Categories=Office;Graphics;
MimeType=application/x-idef0+json;
StartupNotify=true

View file

@ -8,6 +8,7 @@
#include <QDialog>
#include <QTabWidget>
#include <QTreeWidget>
#include <QTextEdit>
#include <QVBoxLayout>
#include <QFormLayout>
#include <QLineEdit>
@ -51,6 +52,7 @@
static const char* kDiagramFileFilter = QT_TR_NOOP("IDEF0 Diagram (*.idef0);;JSON Diagram (*.json)");
static const char* kPdfFileFilter = QT_TR_NOOP("PDF (*.pdf)");
static const char* kMarkdownFileFilter = QT_TR_NOOP("Markdown (*.md)");
static QPageSize::PageSizeId pageSizeFromMeta(const QVariantMap& meta) {
const QString size = meta.value("pageSize", "A4").toString();
@ -78,6 +80,13 @@ static QVariantMap stripPerDiagramState(QVariantMap meta) {
return meta;
}
static QString normalizeCurrencyName(const QString& currency) {
if (currency == QCoreApplication::translate("MainWindow", "man-hours") || currency == QStringLiteral("man-hours")) {
return QCoreApplication::translate("MainWindow", "person-hours");
}
return currency;
}
static QVariantMap mergeWithDiagramState(QVariantMap globalMeta, const QVariantMap& diagramMeta) {
for (const char* key : {"state", "working", "draft", "recommended", "publication"}) {
const QString k = QString::fromLatin1(key);
@ -132,6 +141,7 @@ void MainWindow::setupActions() {
auto* actSave = new QAction(tr("Save"), this);
auto* actSaveAs = new QAction(tr("Save As…"), this);
auto* actExportPdf = new QAction(tr("Export to PDF…"), this);
auto* actExportMarkdown = new QAction(tr("Export to Markdown…"), this);
actNew->setShortcut(QKeySequence::New);
actOpen->setShortcut(QKeySequence::Open);
actSave->setShortcut(QKeySequence::Save);
@ -143,12 +153,18 @@ void MainWindow::setupActions() {
fileMenu->addAction(actSaveAs);
fileMenu->addSeparator();
fileMenu->addAction(actExportPdf);
fileMenu->addAction(actExportMarkdown);
auto* editMenu = menuBar()->addMenu(tr("&Edit"));
auto* toolsMenu = menuBar()->addMenu(tr("&Tools"));
auto* actSwapNums = new QAction(tr("Swap numbers…"), this);
auto* actCallMech = new QAction(tr("Call Mechanism add"), this);
auto* actValidation = new QAction(tr("Validation"), this);
auto* actCalcPrices = new QAction(tr("Calculate Prices"), this);
editMenu->addAction(actSwapNums);
editMenu->addAction(actCallMech);
toolsMenu->addAction(actValidation);
toolsMenu->addAction(actCalcPrices);
auto* viewMenu = menuBar()->addMenu(tr("&View"));
auto* actFit = new QAction(tr("Fit"), this);
@ -177,7 +193,7 @@ void MainWindow::setupActions() {
connect(actNodeTree, &QAction::triggered, this, [this]{
showNodeTreeDialog();
});
viewMenu->addAction(actNodeTree);
toolsMenu->addAction(actNodeTree);
m_actDarkMode = new QAction(tr("Dark mode"), this);
m_actDarkMode->setCheckable(true);
@ -309,6 +325,9 @@ void MainWindow::setupActions() {
if (dlg.exec() != QDialog::Accepted) return;
exportPdf(scope->currentIndex() == 1, pageNumbers->isChecked(), forceLight->isChecked());
});
connect(actExportMarkdown, &QAction::triggered, this, [this]{
exportMarkdownExplanation();
});
connect(actCallMech, &QAction::triggered, this, [this]{
if (!m_scene) return;
@ -360,6 +379,13 @@ void MainWindow::setupActions() {
statusBar()->showMessage(tr("Click bottom frame to place call mechanism arrow. Esc to cancel."), 5000);
});
connect(actValidation, &QAction::triggered, this, [this]{
runValidation();
});
connect(actCalcPrices, &QAction::triggered, this, [this]{
calculatePrices();
});
m_pluginManager = new PluginManager(this, pluginsMenu, this);
m_pluginManager->loadPlugins();
}
@ -445,7 +471,8 @@ bool MainWindow::showWelcome() {
currency->setEditable(true);
currency->addItem(curSym);
if (curSym != "$") currency->addItem("$");
const QString existingCur = m_welcomeState.value("currency").toString();
currency->addItem(tr("person-hours"));
const QString existingCur = normalizeCurrencyName(m_welcomeState.value("currency").toString());
if (!existingCur.isEmpty()) {
currency->setCurrentText(existingCur);
} else {
@ -454,12 +481,18 @@ bool MainWindow::showWelcome() {
auto* placement = new QComboBox(units);
placement->addItems({"1?", "?1", "1 ?", "? 1"});
const QString personHours = tr("person-hours");
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()));
placement->setCurrentText(currency->currentText() == personHours
? QStringLiteral("1 ?")
: symbolPlacementDefault(loc, currency->currentText()));
}
connect(currency, &QComboBox::currentTextChanged, this, [placement, personHours](const QString& cur){
if (cur == personHours) placement->setCurrentText(QStringLiteral("1 ?"));
});
auto* timeUnit = new QComboBox(units);
const QStringList unitsList = {tr("Seconds"), tr("Minutes"), tr("Hours"), tr("Days"), tr("Weeks"), tr("Months"), tr("Years")};
@ -517,7 +550,7 @@ bool MainWindow::showWelcome() {
m_welcomeState["title"] = title->text();
m_welcomeState["organization"] = organization->text();
m_welcomeState["initials"] = initials->text();
m_welcomeState["currency"] = currency->currentText();
m_welcomeState["currency"] = normalizeCurrencyName(currency->currentText());
m_welcomeState["currencyPlacement"] = placement->currentText();
m_welcomeState["timeUnit"] = timeUnit->currentText();
m_welcomeState["pageSize"] = sizeCombo->currentText();
@ -549,6 +582,13 @@ void MainWindow::newDiagram() {
m_scene->createBlockAt(bounds.center());
m_currentFile.clear();
markDirty(false);
// Keep main window visible and focused after closing modal creation dialogs.
QTimer::singleShot(0, this, [this]{
setWindowState(windowState() & ~Qt::WindowMinimized);
showNormal();
raise();
activateWindow();
});
}
bool MainWindow::promptStartup() {
@ -571,6 +611,12 @@ bool MainWindow::promptStartup() {
m_scene->createBlockAt(bounds.center());
m_currentFile.clear();
markDirty(false);
QTimer::singleShot(0, this, [this]{
setWindowState(windowState() & ~Qt::WindowMinimized);
showNormal();
raise();
activateWindow();
});
return true;
}
@ -595,6 +641,7 @@ bool MainWindow::loadDiagramFromPath(const QString& path) {
}
if (root.contains("meta")) {
m_welcomeState = stripPerDiagramState(root.value("meta").toMap());
m_welcomeState["currency"] = normalizeCurrencyName(m_welcomeState.value("currency").toString());
}
if (m_actDarkMode && m_actFollowSystemTheme) {
const QSignalBlocker b2(m_actDarkMode);
@ -716,6 +763,137 @@ bool MainWindow::exportPdf(bool allDiagrams, bool numberPages, bool forceLightTh
return true;
}
bool MainWindow::exportMarkdownExplanation() {
if (!m_scene) return false;
QString path = QFileDialog::getSaveFileName(this, tr("Export to Markdown"), QString(), tr(kMarkdownFileFilter));
if (path.isEmpty()) return false;
if (QFileInfo(path).suffix().isEmpty()) path += ".md";
const QVariantMap exported = m_scene->exportToVariant();
const QVariantMap rootDiagram = exported.value("diagram").toMap();
const QVariantMap children = exported.value("children").toMap();
auto sortedBlocks = [](const QVariantMap& diagram) {
QVector<QVariantMap> out;
const QVariantList blocks = diagram.value("blocks").toList();
out.reserve(blocks.size());
for (const QVariant& vb : blocks) out.push_back(vb.toMap());
std::sort(out.begin(), out.end(), [](const QVariantMap& a, const QVariantMap& b) {
const QString na = a.value("number").toString();
const QString nb = b.value("number").toString();
if (na == nb) return a.value("id", -1).toInt() < b.value("id", -1).toInt();
return na.localeAwareCompare(nb) < 0;
});
return out;
};
auto addUniqueSorted = [](QStringList& list, const QString& value) {
const QString v = value.trimmed();
if (v.isEmpty() || list.contains(v)) return;
list.push_back(v);
};
auto joinOrDash = [this](QStringList list) {
if (list.isEmpty()) return tr("none");
std::sort(list.begin(), list.end(), [](const QString& a, const QString& b) {
return a.localeAwareCompare(b) < 0;
});
return list.join(", ");
};
struct Ports {
QStringList inputs;
QStringList outputs;
QStringList mechanisms;
QStringList controls;
};
auto collectPorts = [&](const QVariantMap& diagram) {
Ports p;
const QVariantList arrows = diagram.value("arrows").toList();
for (const QVariant& va : arrows) {
const QVariantMap a = va.toMap();
const QString label = a.value("label").toString().trimmed();
if (label.isEmpty()) continue;
const QVariantMap from = a.value("from").toMap();
const QVariantMap to = a.value("to").toMap();
const int fk = from.value("kind").toInt(0);
const int fp = from.value("port").toInt(0);
const int tk = to.value("kind").toInt(0);
const int tp = to.value("port").toInt(0);
if (fk == 3) {
if (fp == 0) addUniqueSorted(p.inputs, label);
if (fp == 1) addUniqueSorted(p.controls, label);
if (fp == 2) addUniqueSorted(p.outputs, label);
if (fp == 3) addUniqueSorted(p.mechanisms, label);
}
if (tk == 3) {
if (tp == 0) addUniqueSorted(p.inputs, label);
if (tp == 1) addUniqueSorted(p.controls, label);
if (tp == 2) addUniqueSorted(p.outputs, label);
if (tp == 3) addUniqueSorted(p.mechanisms, label);
}
}
return p;
};
auto stars = [](int count) { return QString(count, '*'); };
auto safeName = [this](const QString& s) {
const QString trimmed = s.trimmed();
return trimmed.isEmpty() ? tr("Untitled") : trimmed;
};
QString md;
std::function<void(const QVariantMap&, const QString&, const QString&, int, const QString&)> emitNode;
emitNode = [&](const QVariantMap& diagram, const QString& key, const QString& number, int depth, const QString& title) {
const Ports p = collectPorts(diagram);
md += QStringLiteral("%1 %2: %3\n").arg(stars(depth), number, safeName(title));
md += QStringLiteral("%1 Inputs: %2\n").arg(stars(depth + 1), joinOrDash(p.inputs));
md += QStringLiteral("%1 Outputs: %2\n").arg(stars(depth + 1), joinOrDash(p.outputs));
md += QStringLiteral("%1 Mechanisms: %2\n").arg(stars(depth + 1), joinOrDash(p.mechanisms));
md += QStringLiteral("%1 Controls: %2\n").arg(stars(depth + 1), joinOrDash(p.controls));
md += QStringLiteral("%1 Subprocesses:\n").arg(stars(depth + 1));
const QVector<QVariantMap> blocks = sortedBlocks(diagram);
if (blocks.isEmpty()) {
md += QStringLiteral("%1 %2\n").arg(stars(depth + 2), tr("none"));
return;
}
for (const QVariantMap& b : blocks) {
const int id = b.value("id", -1).toInt();
const QString childKey = key.isEmpty() ? QString::number(id) : (key + "/" + QString::number(id));
const QString childNumber = b.value("number").toString().trimmed().isEmpty()
? QStringLiteral("A?")
: b.value("number").toString().trimmed();
const QString childTitle = safeName(b.value("title").toString());
if (id >= 0 && children.contains(childKey)) {
emitNode(children.value(childKey).toMap(), childKey, childNumber, depth + 2, childTitle);
} else {
md += QStringLiteral("%1 %2: %3\n").arg(stars(depth + 2), childNumber, childTitle);
}
}
};
QString rootTitle = m_welcomeState.value("title").toString();
for (const QVariantMap& b : sortedBlocks(rootDiagram)) {
const QString n = b.value("number").toString();
if (n.compare(QStringLiteral("A0"), Qt::CaseInsensitive) == 0) {
rootTitle = b.value("title").toString();
break;
}
}
emitNode(rootDiagram, QString(), QStringLiteral("A0"), 1, rootTitle);
QFile f(path);
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
QMessageBox::warning(this, tr("Export failed"), tr("Could not open file for writing."));
return false;
}
f.write(md.toUtf8());
f.close();
return true;
}
bool MainWindow::effectiveDarkMode() const {
const bool followSystem = m_welcomeState.value("followSystemTheme", false).toBool();
if (!followSystem) {
@ -866,6 +1044,316 @@ void MainWindow::showNodeTreeDialog() {
dlg->deleteLater();
}
void MainWindow::runValidation() {
if (!m_scene) return;
const QVariantMap exported = m_scene->exportToVariant();
const QVariantMap rootDiagram = exported.value("diagram").toMap();
const QVariantMap children = exported.value("children").toMap();
struct DiagCtx {
QString key; // "" for root, "1/2/3" for descendants
QString node; // A0, A1, A1.2, ...
QVariantMap diagram; // variant map with blocks/arrows
};
QHash<QString, DiagCtx> diags;
std::function<void(const QString&, const QString&, const QVariantMap&)> collect;
collect = [&](const QString& key, const QString& node, const QVariantMap& diagram) {
diags.insert(key, DiagCtx{key, node, diagram});
const QVariantList blocks = diagram.value("blocks").toList();
for (const QVariant& vb : blocks) {
const QVariantMap b = vb.toMap();
const int id = b.value("id", -1).toInt();
if (id < 0) continue;
const QString childKey = key.isEmpty() ? QString::number(id) : key + "/" + QString::number(id);
if (!children.contains(childKey)) continue;
const QString childNode = b.value("number").toString().isEmpty() ? node : b.value("number").toString();
collect(childKey, childNode, children.value(childKey).toMap());
}
};
collect(QString(), QStringLiteral("A0"), rootDiagram);
auto epKind = [](const QVariantMap& ep) { return ep.value("kind").toInt(0); }; // 1=Block, 2=Junction, 3=Scene
auto epId = [](const QVariantMap& ep) { return ep.value("id", -1).toInt(); };
auto epPort = [](const QVariantMap& ep) { return ep.value("port", 0).toInt(); }; // 0=I,1=C,2=O,3=M
auto epTun = [](const QVariantMap& ep) { return ep.value("tunneled").toBool(); };
auto blockLabel = [](const QVariantMap& b) {
const QString num = b.value("number").toString();
const QString title = b.value("title").toString();
if (num.isEmpty()) return title;
return title.isEmpty() ? num : QStringLiteral("%1 %2").arg(num, title);
};
QStringList errors;
QStringList warnings;
// rule 5
if (rootDiagram.value("blocks").toList().size() > 1) {
errors << tr("There are several blocks in the context (root) diagram (there should be only one).");
}
for (const DiagCtx& d : diags) {
const bool isRoot = d.key.isEmpty();
const QVariantList blocks = d.diagram.value("blocks").toList();
const QVariantList arrows = d.diagram.value("arrows").toList();
QHash<int, QVariantMap> blockById;
for (const QVariant& vb : blocks) {
const QVariantMap b = vb.toMap();
blockById.insert(b.value("id", -1).toInt(), b);
}
// rules 6,7 for non-root
if (!isRoot) {
if (blocks.size() < 3) {
errors << tr("The decomposition diagram (%1) contains fewer than 3 blocks (there should be 38).").arg(d.node);
} else if (blocks.size() > 8) {
errors << tr("The decomposition diagram (%1) contains more than 8 blocks (there should be 38).").arg(d.node);
}
}
// rule 8 hanging interface circles
if (!isRoot) {
bool hasHanging = false;
for (const QVariant& va : arrows) {
const QVariantMap a = va.toMap();
if (a.value("isInterface").toBool() &&
a.value("isInterfaceStub").toBool() &&
!a.value("interfaceEdge").toMap().value("tunneled").toBool()) {
hasHanging = true;
break;
}
}
if (hasHanging) {
errors << tr("The decomposition of block (%1) is incomplete; there are hanging white connector circles.").arg(d.node);
}
}
QHash<int, int> inIC;
QHash<int, int> outO;
QHash<int, QStringList> inByBlockLabel;
QHash<int, QStringList> outByBlockLabel;
QHash<QString, int> sameLabelEntries; // blockId:port:label
QHash<int, bool> hasOtherBlockLink;
QHash<int, bool> hasIncident;
QHash<int, bool> allIncidentTunneled;
for (auto it = blockById.cbegin(); it != blockById.cend(); ++it) {
hasOtherBlockLink[it.key()] = false;
hasIncident[it.key()] = false;
allIncidentTunneled[it.key()] = true;
}
for (const QVariant& va : arrows) {
const QVariantMap a = va.toMap();
const QVariantMap f = a.value("from").toMap();
const QVariantMap t = a.value("to").toMap();
const QString lbl = a.value("label").toString().trimmed();
const int fk = epKind(f), tk = epKind(t);
const int fid = epId(f), tid = epId(t);
const int fp = epPort(f), tp = epPort(t);
if (fk == 1 && blockById.contains(fid)) {
hasIncident[fid] = true;
allIncidentTunneled[fid] = allIncidentTunneled[fid] && epTun(f);
}
if (tk == 1 && blockById.contains(tid)) {
hasIncident[tid] = true;
allIncidentTunneled[tid] = allIncidentTunneled[tid] && epTun(t);
}
if (tk == 1 && blockById.contains(tid) && (tp == 0 || tp == 1)) {
inIC[tid] += 1;
}
if (fk == 1 && blockById.contains(fid) && fp == 2) {
outO[fid] += 1;
}
if (tk == 1 && blockById.contains(tid) && tp == 0 && !lbl.isEmpty()) {
inByBlockLabel[tid].push_back(lbl);
}
if (fk == 1 && blockById.contains(fid) && fp == 2 && !lbl.isEmpty()) {
outByBlockLabel[fid].push_back(lbl);
}
if (tk == 1 && blockById.contains(tid) && (tp == 0 || tp == 1 || tp == 2) && !lbl.isEmpty()) {
const QString key = QStringLiteral("%1:%2:%3").arg(tid).arg(tp).arg(lbl);
sameLabelEntries[key] += 1;
}
if (fk == 1 && tk == 1 && blockById.contains(fid) && blockById.contains(tid) && fid != tid) {
hasOtherBlockLink[fid] = true;
hasOtherBlockLink[tid] = true;
}
// warnings 1..4
if (!isRoot && fk == 3 && tk == 1) {
if (fp == 3 && tp == 0) warnings << tr("The mechanism of diagram %1 acts as an input for block %2.").arg(d.node, blockById.value(tid).value("number").toString());
if (fp == 3 && tp == 1) warnings << tr("The mechanism of diagram %1 acts as a control for block %2.").arg(d.node, blockById.value(tid).value("number").toString());
if (fp == 1 && tp == 3) warnings << tr("The control of diagram %1 acts as a mechanism for block %2.").arg(d.node, blockById.value(tid).value("number").toString());
if (fp == 0 && tp == 3) warnings << tr("The input of diagram %1 acts as a mechanism for block %2.").arg(d.node, blockById.value(tid).value("number").toString());
}
// warning 5
if (fk == 1 && tk == 1 && fp == 2 && tp == 3 && blockById.contains(fid) && blockById.contains(tid)) {
warnings << tr("The output of block %1 acts as a mechanism for block %2.")
.arg(blockById.value(fid).value("number").toString(),
blockById.value(tid).value("number").toString());
}
}
for (auto it = blockById.cbegin(); it != blockById.cend(); ++it) {
const int id = it.key();
const QVariantMap b = it.value();
const QString title = b.value("title").toString();
const QString titleTrim = title.trimmed();
// rules 1,2
if (title == tr("Function") || title == QStringLiteral("Function")) {
errors << tr("The block name should reflect what it does. Do not leave block %1 with a default name.").arg(blockLabel(b));
}
if (titleTrim.isEmpty()) {
errors << tr("The block name should reflect what it does. Do not leave block %1 with an empty name.").arg(blockLabel(b));
}
// rules 3,4
if (inIC.value(id, 0) == 0) {
errors << tr("Each block must have at least one input or control: %1.").arg(blockLabel(b));
}
if (outO.value(id, 0) == 0) {
errors << tr("Each block must have at least one output: %1.").arg(blockLabel(b));
}
// rule 11
QSet<QString> inNames, outNames;
for (const QString& s : inByBlockLabel.value(id)) inNames.insert(s.trimmed());
for (const QString& s : outByBlockLabel.value(id)) outNames.insert(s.trimmed());
for (const QString& s : inNames) {
if (!s.isEmpty() && outNames.contains(s)) {
errors << tr("Input and output names are identical (%1) in process %2.").arg(s, b.value("number").toString());
}
}
// warning 6
if (hasIncident.value(id, false) && allIncidentTunneled.value(id, false)) {
warnings << tr("Block %1 is a black box (all connected endpoints are tunneled).").arg(b.value("number").toString());
}
// warning 8
if (!isRoot && blocks.size() > 1 && !hasOtherBlockLink.value(id, false)) {
warnings << tr("Block %1 is not related to other blocks in decomposition %2.").arg(b.value("number").toString(), d.node);
}
}
// rule 10
for (auto it = sameLabelEntries.cbegin(); it != sameLabelEntries.cend(); ++it) {
if (it.value() > 1) {
errors << tr("The same arrow enters the same block several times in diagram %1 (%2).").arg(d.node, it.key());
}
}
// warning 7
for (const QVariant& vb : blocks) {
const QVariantMap b = vb.toMap();
if (!b.contains("price") || b.value("price").isNull()) continue;
const int id = b.value("id", -1).toInt();
if (id < 0) continue;
const QString childKey = d.key.isEmpty() ? QString::number(id) : d.key + "/" + QString::number(id);
if (!children.contains(childKey)) continue;
const QVariantList childBlocks = children.value(childKey).toMap().value("blocks").toList();
qreal sum = 0.0;
for (const QVariant& vcb : childBlocks) {
const QVariantMap cb = vcb.toMap();
if (cb.contains("price") && !cb.value("price").isNull()) sum += cb.value("price").toDouble();
}
const qreal parentPrice = b.value("price").toDouble();
if (sum > parentPrice + 1e-6) {
warnings << tr("The total cost of process %1 is lower than the total cost of its subprocesses. Use Tools >> Calculate Prices.")
.arg(b.value("number").toString());
}
}
}
QString report;
report += tr("Errors:") + "\n";
if (errors.isEmpty()) report += tr("None.") + "\n";
else for (int i = 0; i < errors.size(); ++i) report += QString::number(i + 1) + ". " + errors[i] + "\n";
report += "\n" + tr("Warnings:") + "\n";
if (warnings.isEmpty()) report += tr("None.") + "\n";
else for (int i = 0; i < warnings.size(); ++i) report += QString::number(i + 1) + ". " + warnings[i] + "\n";
auto* dlg = new QDialog(this);
dlg->setWindowTitle(tr("Validation"));
auto* layout = new QVBoxLayout(dlg);
auto* txt = new QTextEdit(dlg);
txt->setReadOnly(true);
txt->setPlainText(report);
layout->addWidget(txt);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, dlg);
connect(buttons, &QDialogButtonBox::rejected, dlg, &QDialog::reject);
connect(buttons, &QDialogButtonBox::accepted, dlg, &QDialog::accept);
layout->addWidget(buttons);
dlg->resize(860, 640);
dlg->exec();
dlg->deleteLater();
}
void MainWindow::calculatePrices() {
if (!m_scene) return;
QVariantMap exported = m_scene->exportToVariant();
QVariantMap rootDiagram = exported.value("diagram").toMap();
QVariantMap children = exported.value("children").toMap();
int updatedBlocks = 0;
std::function<void(const QString&, QVariantMap&)> recalc;
recalc = [&](const QString& key, QVariantMap& diagram) {
QVariantList blocks = diagram.value("blocks").toList();
for (int i = 0; i < blocks.size(); ++i) {
QVariantMap b = blocks[i].toMap();
const int id = b.value("id", -1).toInt();
if (id < 0) continue;
const QString childKey = key.isEmpty() ? QString::number(id) : key + "/" + QString::number(id);
if (!children.contains(childKey)) continue;
QVariantMap child = children.value(childKey).toMap();
recalc(childKey, child);
children[childKey] = child;
const QVariantList childBlocks = child.value("blocks").toList();
qreal sum = 0.0;
bool hasAny = false;
for (const QVariant& vcb : childBlocks) {
const QVariantMap cb = vcb.toMap();
if (cb.contains("price") && !cb.value("price").isNull()) {
sum += cb.value("price").toDouble();
hasAny = true;
}
}
if (!hasAny) continue;
const qreal oldPrice = b.contains("price") && !b.value("price").isNull() ? b.value("price").toDouble() : std::numeric_limits<qreal>::quiet_NaN();
if (std::isnan(oldPrice) || std::abs(oldPrice - sum) > 1e-6) {
b["price"] = sum;
blocks[i] = b;
updatedBlocks += 1;
}
}
diagram["blocks"] = blocks;
};
recalc(QString(), rootDiagram);
exported["diagram"] = rootDiagram;
exported["children"] = children;
exported["meta"] = stripPerDiagramState(m_welcomeState);
if (!m_scene->importFromVariant(exported)) {
QMessageBox::warning(this, tr("Calculate Prices"), tr("Failed to apply calculated prices."));
return;
}
m_scene->applyMeta(mergeWithDiagramState(m_welcomeState, m_scene->meta()));
markDirty(true);
QMessageBox::information(this, tr("Calculate Prices"), tr("Updated prices for %1 block(s).").arg(updatedBlocks));
}
void MainWindow::closeEvent(QCloseEvent* e) {
if (!m_dirty) {
e->accept();
@ -973,5 +1461,5 @@ void MainWindow::markDirty(bool dirty) {
void MainWindow::updateWindowTitle() {
QString name = m_currentFile.isEmpty() ? tr("Untitled") : QFileInfo(m_currentFile).fileName();
if (m_dirty) name += " *";
setWindowTitle(name + tr(" IDEF0 editor"));
setWindowTitle(name + tr(" erlu IDEF0 editor"));
}

View file

@ -41,6 +41,9 @@ private:
bool promptStartup();
bool loadDiagramFromPath(const QString& path);
bool exportPdf(bool allDiagrams, bool numberPages, bool forceLightTheme);
bool exportMarkdownExplanation();
void runValidation();
void calculatePrices();
bool effectiveDarkMode() const;
void applyAppPalette(bool darkMode);
void autoInitialsFromAuthor(QLineEdit* author, QLineEdit* initials);

View file

@ -26,6 +26,11 @@ DiagramScene::DiagramScene(QObject* parent)
: QGraphicsScene(parent)
{
m_currentPrefix.clear();
m_meta["state"] = QStringLiteral("working");
m_meta["working"] = true;
m_meta["draft"] = false;
m_meta["recommended"] = false;
m_meta["publication"] = false;
// Ограничиваем рабочую область A4 (210x297 мм) в пикселях при 96 dpi (~793x1122).
// По умолчанию используем альбомную ориентацию (ширина больше высоты).
const qreal w = 1122;
@ -284,6 +289,10 @@ bool DiagramScene::tryStartArrowDrag(const QPointF& scenePos, Qt::KeyboardModifi
ArrowItem::Endpoint from;
from.scenePos = edgePoint;
from.port = BlockItem::Port::Input; // ориентация не важна для свободной точки
if (m_currentBlockId >= 0) {
// Non-root subdiagram frame endpoints without interface stub are tunneled at source.
from.tunneled = true;
}
a->setFrom(from);
a->setTempEndPoint(scenePos);
m_dragArrow = a;
@ -298,14 +307,6 @@ bool DiagramScene::tryStartArrowDrag(const QPointF& scenePos, Qt::KeyboardModifi
bool DiagramScene::tryFinishArrowDrag(const QPointF& scenePos) {
if (!m_dragArrow) return false;
auto markSubdiagramOriginTunnel = [this](ArrowItem* arrow) {
if (!arrow || m_currentBlockId < 0) return;
auto from = arrow->from();
if (!from.tunneled) {
from.tunneled = true;
arrow->setFrom(from);
}
};
const auto itemsUnder = items(scenePos);
// попадание в интерфейсный круг
@ -345,6 +346,7 @@ bool DiagramScene::tryFinishArrowDrag(const QPointF& scenePos) {
BlockItem::Port port{};
QPointF localPos;
if (!b->hitTestPort(scenePos, &port, &localPos)) continue;
if (port == BlockItem::Port::Output) continue; // end of arrow cannot connect to Output port
// запретим соединять в тот же самый порт того же блока (упрощение)
if (b == m_dragFromBlock && port == m_dragFromPort) continue;
@ -365,7 +367,6 @@ bool DiagramScene::tryFinishArrowDrag(const QPointF& scenePos) {
m_dragArrow->setFrom(stubEp);
m_dragArrow->setTo(blockEp);
}
markSubdiagramOriginTunnel(m_dragArrow);
m_dragArrow->setLabelLocked(true);
m_dragArrow->finalize();
}
@ -375,7 +376,6 @@ bool DiagramScene::tryFinishArrowDrag(const QPointF& scenePos) {
to.port = port;
to.localPos = localPos;
m_dragArrow->setTo(to);
markSubdiagramOriginTunnel(m_dragArrow);
m_dragArrow->finalize();
}
@ -408,7 +408,6 @@ bool DiagramScene::tryFinishArrowDrag(const QPointF& scenePos) {
m_dragArrow->setFrom(stubEp);
m_dragArrow->setTo(jun);
}
markSubdiagramOriginTunnel(m_dragArrow);
m_dragArrow->setLabelLocked(true);
m_dragArrow->finalize();
}
@ -417,7 +416,6 @@ bool DiagramScene::tryFinishArrowDrag(const QPointF& scenePos) {
to.junction = j;
to.port = BlockItem::Port::Input;
m_dragArrow->setTo(to);
markSubdiagramOriginTunnel(m_dragArrow);
m_dragArrow->finalize();
}
@ -474,7 +472,6 @@ bool DiagramScene::tryFinishArrowDrag(const QPointF& scenePos) {
m_dragArrow->setLabelHidden(hideInherited);
m_dragArrow->setLabelInherited(true);
m_dragArrow->setLabelSource(sourceRoot);
markSubdiagramOriginTunnel(m_dragArrow);
m_dragArrow->finalize();
m_dragArrow = nullptr;
@ -497,8 +494,11 @@ bool DiagramScene::tryFinishArrowDrag(const QPointF& scenePos) {
ArrowItem::Endpoint to;
to.scenePos = edgePoint;
to.port = BlockItem::Port::Input;
if (m_currentBlockId >= 0 && edge == Edge::Right) {
// In subdiagrams, right-frame endings without interface stub are tunneled at target.
to.tunneled = true;
}
m_dragArrow->setTo(to);
markSubdiagramOriginTunnel(m_dragArrow);
m_dragArrow->finalize();
m_dragArrow = nullptr;
m_dragFromBlock.clear();
@ -833,8 +833,12 @@ void DiagramScene::updateCallMechanismLabels() {
void DiagramScene::purgeBrokenCallMechanisms() {
QSet<BlockItem*> blocks;
QHash<int, BlockItem*> blockById;
for (QGraphicsItem* it : items()) {
if (auto* b = qgraphicsitem_cast<BlockItem*>(it)) blocks.insert(b);
if (auto* b = qgraphicsitem_cast<BlockItem*>(it)) {
blocks.insert(b);
blockById.insert(b->id(), b);
}
}
QVector<ArrowItem*> toRemove;
for (QGraphicsItem* it : items()) {
@ -845,6 +849,7 @@ void DiagramScene::purgeBrokenCallMechanisms() {
bool ok = true;
if (from.block && !blocks.contains(from.block)) ok = false;
if (to.block && !blocks.contains(to.block)) ok = false;
if (!blockById.contains(a->callRefId())) ok = false; // referenced block was deleted
if (!ok) toRemove.push_back(a);
}
}
@ -860,6 +865,12 @@ void DiagramScene::undo() {
restoreSnapshot(m_history[m_historyIndex], false);
}
void DiagramScene::redo() {
if (m_historyIndex + 1 >= m_history.size()) return;
m_historyIndex += 1;
restoreSnapshot(m_history[m_historyIndex], false);
}
bool DiagramScene::goDownIntoSelected() {
const auto sel = selectedItems();
if (sel.size() != 1) return false;
@ -991,14 +1002,7 @@ bool DiagramScene::goDownIntoBlock(BlockItem* b) {
}
child.arrows = std::move(preserved);
} else {
QString state = m_meta.value("state").toString();
if (state.isEmpty()) {
if (m_meta.value("publication", false).toBool()) state = "publication";
else if (m_meta.value("recommended", false).toBool()) state = "recommended";
else if (m_meta.value("draft", false).toBool()) state = "draft";
else state = "working";
}
child.state = state;
child.state = QStringLiteral("working");
child.hasState = true;
}
@ -1138,12 +1142,28 @@ void DiagramScene::mousePressEvent(QGraphicsSceneMouseEvent* e) {
}
if (e->button() == Qt::LeftButton) {
const auto itemsUnder = items(e->scenePos());
if (e->modifiers().testFlag(Qt::ControlModifier)) {
if (tryBranchAtArrow(e->scenePos())) {
e->accept();
return;
}
}
if (e->modifiers().testFlag(Qt::ShiftModifier)) {
for (QGraphicsItem* it : itemsUnder) {
auto* j = qgraphicsitem_cast<JunctionItem*>(it);
if (!j || !j->hitTest(e->scenePos(), 8.0)) continue;
if (!j->isSelected()) {
clearSelection();
j->setSelected(true);
}
m_pressPositions.clear();
m_pressPositions.insert(j, j->pos());
m_itemDragActive = true;
QGraphicsScene::mousePressEvent(e);
return;
}
}
// если кликнули на порт — начинаем протягивание стрелки
if (tryStartArrowDrag(e->scenePos(), e->modifiers())) {
e->accept();
@ -1249,6 +1269,11 @@ void DiagramScene::keyPressEvent(QKeyEvent* e) {
e->accept();
return;
}
if (e->matches(QKeySequence::Redo)) {
redo();
e->accept();
return;
}
if ((e->key() == Qt::Key_Down || e->key() == Qt::Key_PageDown) && e->modifiers().testFlag(Qt::ControlModifier)) {
if (goDownIntoSelected()) {
e->accept();
@ -1348,6 +1373,44 @@ void DiagramScene::deleteSelection() {
}
}
// If deleting a pre-branch arrow (ending at junction), delete all downstream branches too.
QVector<ArrowItem*> selectedArrows;
for (QGraphicsItem* it : toDelete) {
if (auto* a = qgraphicsitem_cast<ArrowItem*>(it)) selectedArrows.push_back(a);
}
QSet<JunctionItem*> branchStarts;
for (ArrowItem* a : selectedArrows) {
if (!a) continue;
const auto t = a->to();
if (t.junction) branchStarts.insert(t.junction);
}
if (!branchStarts.isEmpty()) {
QSet<JunctionItem*> visitedJunctions = branchStarts;
QVector<JunctionItem*> queue(branchStarts.begin(), branchStarts.end());
int qi = 0;
while (qi < queue.size()) {
JunctionItem* j = queue[qi++];
if (!j) continue;
for (QGraphicsItem* it : items()) {
auto* a = qgraphicsitem_cast<ArrowItem*>(it);
if (!a) continue;
const auto f = a->from();
const auto t = a->to();
// downstream from branching junction
if (f.junction == j) {
toDelete.insert(a);
if (t.junction && !visitedJunctions.contains(t.junction)) {
visitedJunctions.insert(t.junction);
queue.push_back(t.junction);
}
}
}
}
for (JunctionItem* j : visitedJunctions) {
toDelete.insert(j);
}
}
QVector<ArrowItem*> arrowsToDelete;
QVector<QGraphicsItem*> othersToDelete;
for (QGraphicsItem* it : toDelete) {

View file

@ -115,6 +115,7 @@ private:
void pushSnapshot();
void scheduleSnapshot();
void undo();
void redo();
void maybeSnapshotMovedItems();
void resetHistory(const Snapshot& base);
void ensureFrame();

View file

@ -2,6 +2,8 @@
#include <QTranslator>
#include <QLocale>
#include <QDir>
#include <QIcon>
#include <QFile>
#include <QCoreApplication>
#include <QEvent>
#include <QFileOpenEvent>
@ -33,6 +35,19 @@ public:
int main(int argc, char** argv) {
IdefApplication app(argc, argv);
const QString resIcon = QStringLiteral(":/icons/erlu.svg");
if (QFile::exists(resIcon)) {
app.setWindowIcon(QIcon(resIcon));
} else {
const QString appDir = QCoreApplication::applicationDirPath();
const QString fallback1 = appDir + QStringLiteral("/../share/icons/hicolor/scalable/apps/erlu.svg");
const QString fallback2 = QDir::currentPath() + QStringLiteral("/assets/icons/erlu.svg");
if (QFile::exists(fallback1)) {
app.setWindowIcon(QIcon(fallback1));
} else if (QFile::exists(fallback2)) {
app.setWindowIcon(QIcon(fallback2));
}
}
const QLocale systemLocale = QLocale::system();
QLocale::setDefault(systemLocale);

View file

@ -560,8 +560,8 @@
<translation>Untitled</translation>
</message>
<message>
<source> IDEF0 editor</source>
<translation> IDEF0 editor</translation>
<source> erlu IDEF0 editor</source>
<translation> erlu IDEF0 editor</translation>
</message>
</context>
</TS>

View file

@ -255,6 +255,10 @@
<source>Export to PDF</source>
<translation>Exporter en PDF</translation>
</message>
<message>
<source>Export to Markdown</source>
<translation>Exporter en Markdown</translation>
</message>
<message>
<source>&amp;Edit</source>
<translation>&amp;Édition</translation>
@ -363,6 +367,14 @@
<source>Export to PDF</source>
<translation>Exporter en PDF</translation>
</message>
<message>
<source>Export to Markdown</source>
<translation>Exporter en Markdown</translation>
</message>
<message>
<source>Markdown (*.md)</source>
<translation>Markdown (*.md)</translation>
</message>
<message>
<source>Current diagram</source>
<translation>Diagramme actuel</translation>
@ -459,6 +471,10 @@
<source>Currency:</source>
<translation>Devise :</translation>
</message>
<message>
<source>person-hours</source>
<translation>heures-personne</translation>
</message>
<message>
<source>Symbol placement:</source>
<translation>Position du symbole :</translation>
@ -560,8 +576,155 @@
<translation>Sans titre</translation>
</message>
<message>
<source> IDEF0 editor</source>
<translation> éditeur IDEF0</translation>
<source> erlu IDEF0 editor</source>
<translation> erlu éditeur IDEF0</translation>
</message>
<message>
<source>&amp;Plugins</source>
<translation>&amp;Plugins</translation>
</message>
<message>
<source>&amp;Tools</source>
<translation>&amp;Outils</translation>
</message>
<message>
<source>Validation</source>
<translation>Validation</translation>
</message>
<message>
<source>Calculate Prices</source>
<translation>Calculer les coûts</translation>
</message>
<message>
<source>Use a light theme regardless of the document theme</source>
<translation>Utiliser un thème clair indépendamment du thème du document</translation>
</message>
<message>
<source>Node tree</source>
<translation>Arborescence</translation>
</message>
<message>
<source>Node tree</source>
<translation>Arborescence</translation>
</message>
<message>
<source>Model</source>
<translation>Modèle</translation>
</message>
<message>
<source>There are several blocks in the context (root) diagram (there should be only one).</source>
<translation>Le diagramme de contexte (racine) contient plusieurs blocs (il ne doit y en avoir qu&apos;un).</translation>
</message>
<message>
<source>The decomposition diagram (%1) contains fewer than 3 blocks (there should be 38).</source>
<translation>Le diagramme de décomposition (%1) contient moins de 3 blocs (il doit y en avoir 38).</translation>
</message>
<message>
<source>The decomposition diagram (%1) contains more than 8 blocks (there should be 38).</source>
<translation>Le diagramme de décomposition (%1) contient plus de 8 blocs (il doit y en avoir 38).</translation>
</message>
<message>
<source>The decomposition of block (%1) is incomplete; there are hanging white connector circles.</source>
<translation>La décomposition du bloc (%1) est incomplète ; il existe des connecteurs blancs en suspens.</translation>
</message>
<message>
<source>The mechanism of diagram %1 acts as an input for block %2.</source>
<translation>Le mécanisme du diagramme %1 agit comme une entrée pour le bloc %2.</translation>
</message>
<message>
<source>The mechanism of diagram %1 acts as a control for block %2.</source>
<translation>Le mécanisme du diagramme %1 agit comme un contrôle pour le bloc %2.</translation>
</message>
<message>
<source>The control of diagram %1 acts as a mechanism for block %2.</source>
<translation>Le contrôle du diagramme %1 agit comme un mécanisme pour le bloc %2.</translation>
</message>
<message>
<source>The input of diagram %1 acts as a mechanism for block %2.</source>
<translation>L&apos;entrée du diagramme %1 agit comme un mécanisme pour le bloc %2.</translation>
</message>
<message>
<source>The output of block %1 acts as a mechanism for block %2.</source>
<translation>La sortie du bloc %1 agit comme un mécanisme pour le bloc %2.</translation>
</message>
<message>
<source>The block name should reflect what it does. Do not leave block %1 with a default name.</source>
<translation>Le nom du bloc doit refléter ce qu&apos;il fait. Ne laissez pas le bloc %1 avec un nom par défaut.</translation>
</message>
<message>
<source>The block name should reflect what it does. Do not leave block %1 with an empty name.</source>
<translation>Le nom du bloc doit refléter ce qu&apos;il fait. Ne laissez pas le bloc %1 avec un nom vide.</translation>
</message>
<message>
<source>Each block must have at least one input or control: %1.</source>
<translation>Chaque bloc doit avoir au moins une entrée ou un contrôle : %1.</translation>
</message>
<message>
<source>Each block must have at least one output: %1.</source>
<translation>Chaque bloc doit avoir au moins une sortie : %1.</translation>
</message>
<message>
<source>Input and output names are identical (%1) in process %2.</source>
<translation>Les noms d&apos;entrée et de sortie sont identiques (%1) dans le processus %2.</translation>
</message>
<message>
<source>Block %1 is a black box (all connected endpoints are tunneled).</source>
<translation>Le bloc %1 est une « boîte noire » (toutes les extrémités connectées sont tunnelisées).</translation>
</message>
<message>
<source>Block %1 is not related to other blocks in decomposition %2.</source>
<translation>Le bloc %1 n&apos;est pas lié aux autres blocs de la décomposition %2.</translation>
</message>
<message>
<source>The same arrow enters the same block several times in diagram %1 (%2).</source>
<translation>La même flèche entre plusieurs fois dans le même bloc sur le diagramme %1 (%2).</translation>
</message>
<message>
<source>The total cost of process %1 is lower than the total cost of its subprocesses. Use Tools &gt;&gt; Calculate Prices.</source>
<translation>Le coût total du processus %1 est inférieur au coût total de ses sous-processus. Utilisez Outils &gt;&gt; Calculer les coûts.</translation>
</message>
<message>
<source>Errors:</source>
<translation>Erreurs :</translation>
</message>
<message>
<source>None.</source>
<translation>Aucun.</translation>
</message>
<message>
<source>none</source>
<translation>aucun</translation>
</message>
<message>
<source>Warnings:</source>
<translation>Avertissements :</translation>
</message>
<message>
<source>Failed to apply calculated prices.</source>
<translation>Impossible d&apos;appliquer les coûts calculés.</translation>
</message>
<message>
<source>Updated prices for %1 block(s).</source>
<translation>Coûts mis à jour pour %1 bloc(s).</translation>
</message>
<message>
<source>Call mechanism already exists for this block or selection is invalid.</source>
<translation>Un Call Mechanism existe déjà pour ce bloc, ou la sélection est invalide.</translation>
</message>
</context>
<context>
<name>ColorsPlugin</name>
<message>
<source>Select item color</source>
<translation>Sélectionner la couleur de l&apos;élément</translation>
</message>
<message>
<source>Set item color</source>
<translation>Définir la couleur de l&apos;élément</translation>
</message>
<message>
<source>Clear item colors</source>
<translation>Effacer les couleurs des éléments</translation>
</message>
</context>
</TS>

View file

@ -255,6 +255,10 @@
<source>Export to PDF</source>
<translation>Экспорт в PDF</translation>
</message>
<message>
<source>Export to Markdown</source>
<translation>Экспорт в Markdown</translation>
</message>
<message>
<source>&amp;Edit</source>
<translation>&amp;Правка</translation>
@ -367,6 +371,14 @@
<source>Export to PDF</source>
<translation>Экспорт в PDF</translation>
</message>
<message>
<source>Export to Markdown</source>
<translation>Экспорт в Markdown</translation>
</message>
<message>
<source>Markdown (*.md)</source>
<translation>Markdown (*.md)</translation>
</message>
<message>
<source>Current diagram</source>
<translation>Текущая диаграмма</translation>
@ -463,6 +475,10 @@
<source>Currency:</source>
<translation>Валюта:</translation>
</message>
<message>
<source>person-hours</source>
<translation>человеко-часы</translation>
</message>
<message>
<source>Symbol placement:</source>
<translation>Расположение символа:</translation>
@ -564,8 +580,151 @@
<translation>Без имени</translation>
</message>
<message>
<source> IDEF0 editor</source>
<translation> редактор IDEF0</translation>
<source> erlu IDEF0 editor</source>
<translation> erlu редактор IDEF0</translation>
</message>
<message>
<source>&amp;Tools</source>
<translation>&amp;Инструменты</translation>
</message>
<message>
<source>Validation</source>
<translation>Проверка</translation>
</message>
<message>
<source>Calculate Prices</source>
<translation>Рассчитать стоимости</translation>
</message>
<message>
<source>Use a light theme regardless of the document theme</source>
<translation>Использовать светлую тему независимо от темы документа</translation>
</message>
<message>
<source>Node tree</source>
<translation>Дерево узлов</translation>
</message>
<message>
<source>Node tree</source>
<translation>Дерево узлов</translation>
</message>
<message>
<source>Model</source>
<translation>Модель</translation>
</message>
<message>
<source>There are several blocks in the context (root) diagram (there should be only one).</source>
<translation>В контекстной (корневой) диаграмме несколько блоков (должен быть только один).</translation>
</message>
<message>
<source>The decomposition diagram (%1) contains fewer than 3 blocks (there should be 38).</source>
<translation>Диаграмма декомпозиции (%1) содержит менее 3 блоков (должно быть 38).</translation>
</message>
<message>
<source>The decomposition diagram (%1) contains more than 8 blocks (there should be 38).</source>
<translation>Диаграмма декомпозиции (%1) содержит более 8 блоков (должно быть 38).</translation>
</message>
<message>
<source>The decomposition of block (%1) is incomplete; there are hanging white connector circles.</source>
<translation>Декомпозиция блока (%1) неполная: есть висящие белые кружки-коннекторы.</translation>
</message>
<message>
<source>The mechanism of diagram %1 acts as an input for block %2.</source>
<translation>Механизм диаграммы %1 выступает входом для блока %2.</translation>
</message>
<message>
<source>The mechanism of diagram %1 acts as a control for block %2.</source>
<translation>Механизм диаграммы %1 выступает управлением для блока %2.</translation>
</message>
<message>
<source>The control of diagram %1 acts as a mechanism for block %2.</source>
<translation>Управление диаграммы %1 выступает механизмом для блока %2.</translation>
</message>
<message>
<source>The input of diagram %1 acts as a mechanism for block %2.</source>
<translation>Вход диаграммы %1 выступает механизмом для блока %2.</translation>
</message>
<message>
<source>The output of block %1 acts as a mechanism for block %2.</source>
<translation>Выход блока %1 выступает механизмом для блока %2.</translation>
</message>
<message>
<source>The block name should reflect what it does. Do not leave block %1 with a default name.</source>
<translation>Имя блока должно отражать его действие. Не оставляйте блоку %1 имя по умолчанию.</translation>
</message>
<message>
<source>The block name should reflect what it does. Do not leave block %1 with an empty name.</source>
<translation>Имя блока должно отражать его действие. Не оставляйте блок %1 без имени.</translation>
</message>
<message>
<source>Each block must have at least one input or control: %1.</source>
<translation>У каждого блока должен быть хотя бы один вход или контроль: %1.</translation>
</message>
<message>
<source>Each block must have at least one output: %1.</source>
<translation>У каждого блока должен быть хотя бы один выход: %1.</translation>
</message>
<message>
<source>Input and output names are identical (%1) in process %2.</source>
<translation>Имена входа и выхода совпадают (%1) в процессе %2.</translation>
</message>
<message>
<source>Block %1 is a black box (all connected endpoints are tunneled).</source>
<translation>Блок %1 является «черным ящиком» (все подключенные концы затуннелированы).</translation>
</message>
<message>
<source>Block %1 is not related to other blocks in decomposition %2.</source>
<translation>Блок %1 не связан с другими блоками в декомпозиции %2.</translation>
</message>
<message>
<source>The same arrow enters the same block several times in diagram %1 (%2).</source>
<translation>Одна и та же стрелка входит в один и тот же блок несколько раз на диаграмме %1 (%2).</translation>
</message>
<message>
<source>The total cost of process %1 is lower than the total cost of its subprocesses. Use Tools &gt;&gt; Calculate Prices.</source>
<translation>Общая стоимость процесса %1 ниже суммарной стоимости его подпроцессов. Используйте Инструменты &gt;&gt; Рассчитать стоимости.</translation>
</message>
<message>
<source>Errors:</source>
<translation>Ошибки:</translation>
</message>
<message>
<source>None.</source>
<translation>Нет.</translation>
</message>
<message>
<source>none</source>
<translation>нет</translation>
</message>
<message>
<source>Warnings:</source>
<translation>Предупреждения:</translation>
</message>
<message>
<source>Failed to apply calculated prices.</source>
<translation>Не удалось применить рассчитанные стоимости.</translation>
</message>
<message>
<source>Updated prices for %1 block(s).</source>
<translation>Обновлены стоимости для %1 блока(ов).</translation>
</message>
<message>
<source>Call mechanism already exists for this block or selection is invalid.</source>
<translation>Call Mechanism уже существует для этого блока, либо выбор недопустим.</translation>
</message>
</context>
<context>
<name>ColorsPlugin</name>
<message>
<source>Select item color</source>
<translation>Выбрать цвет элемента</translation>
</message>
<message>
<source>Set item color</source>
<translation>Установить цвет элемента</translation>
</message>
<message>
<source>Clear item colors</source>
<translation>Очистить цвета элементов</translation>
</message>
</context>
</TS>