Add a terminal name check when saving an element

Discussion #1157. IEC 61666 §4.1 requires each terminal to be
identified unambiguously within its object, so two terminals of one
element must not share a name.

On save, the element editor now:
- refuses to save when two terminals share a name, lists the names
  ("N ×3") and selects those terminals;
- warns, and still saves, when a terminal has no name. Folio reports,
  conductor definitions and thumbnails are skipped.

Names are compared after trimming surrounding spaces, case sensitive.

Settings > General > Editor has a new checkbox, on by default, that
turns both checks off (elementeditor/check-terminal-names).

--check-elements applies the same rule: repeated names are a FAIL,
missing names a WARN. It ignores the setting, since it is an explicit
check. On the shipped collection this reports 82 FAILs, the elements
that repeat a terminal name today.

The rule lives in the header-only editor/terminalnamecheck.h, shared
by both, and is unit tested by tst_terminalnamecheck.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-09-30 07:24:55 +13:00
parent f15736bff7
commit bd4d942d71
7 changed files with 242 additions and 1 deletions
+7
View File
@@ -111,6 +111,13 @@ add_test(NAME tst_alignment COMMAND tst_alignment)
target_include_directories(tst_alignment PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_alignment PRIVATE Qt::Test)
# terminalnamecheck.h is header-only: the terminal name rule applied by the
# element editor on save and by --check-elements.
add_executable(tst_terminalnamecheck tst_terminalnamecheck.cpp)
add_test(NAME tst_terminalnamecheck COMMAND tst_terminalnamecheck)
target_include_directories(tst_terminalnamecheck PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_terminalnamecheck PRIVATE Qt::Test)
# itemgroups.cpp keeps group membership and the selection rule apart from
# Diagram, so it is tested here on a plain scene of rectangles.
add_executable(tst_itemgroups tst_itemgroups.cpp ${QET_DIR}/sources/itemgroups.cpp)
+55
View File
@@ -0,0 +1,55 @@
#include <QtTest>
#include "editor/terminalnamecheck.h"
class tst_terminalnamecheck : public QObject
{
Q_OBJECT
private slots:
void uniqueNamesPass()
{
QVERIFY(TerminalNameCheck::repeatedNames({"A1", "A2", "13", "14"}).isEmpty());
}
// Shelly Pro 2PM in the shipped collection: three terminals named N.
void repeatedNamesAreCounted()
{
const auto repeated = TerminalNameCheck::repeatedNames(
{"L", "N", "O1", "N", "O2", "N", "L"});
QCOMPARE(repeated.size(), 2);
QCOMPARE(repeated.at(0), qMakePair(QString("L"), 2));
QCOMPARE(repeated.at(1), qMakePair(QString("N"), 3));
QCOMPARE(TerminalNameCheck::describe(repeated),
QString::fromUtf8("L ×2, N ×3"));
}
void surroundingSpacesAreIgnored()
{
QCOMPARE(TerminalNameCheck::repeatedNames({"PE", " PE "}).size(), 1);
}
void caseMatters()
{
QVERIFY(TerminalNameCheck::repeatedNames({"n", "N"}).isEmpty());
}
// Unnamed terminals are reported as unnamed, never as a repeat of "".
void unnamedAreNotRepeats()
{
const QStringList names{"", " ", "1", ""};
QVERIFY(TerminalNameCheck::repeatedNames(names).isEmpty());
QCOMPARE(TerminalNameCheck::unnamedCount(names), 3);
}
// describe() goes into a tr() message with %1: a name holding %2 stays literal.
void describeKeepsPercentLiteral()
{
QCOMPARE(TerminalNameCheck::describe({qMakePair(QString("%2"), 2)}),
QString::fromUtf8("%2 ×2"));
}
};
QTEST_GUILESS_MAIN(tst_terminalnamecheck)
#include "tst_terminalnamecheck.moc"