saveConfig() serialises Qt::AlignHCenter as the string "AlignHCenter"
via QMetaEnum::valueToKey(), but loadConfig() matched against
Qt::AlignCenter (0x0084 = AlignHCenter|AlignVCenter) instead of
Qt::AlignHCenter (0x0004). The two values differ, so the switch fell
through to the default (Right) every time center was saved and reloaded.
Add Qt::AlignHCenter as the primary case and keep Qt::AlignCenter as a
fallthrough for any config files that were hand-edited by users following
the workaround documented in issue #283.
Verified with a standalone Qt test: QMetaEnum::keyToValue("AlignHCenter")
returns 4 (AlignHCenter), which now correctly resolves to combobox index 1
(Center) instead of 2 (Right).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The default user-collection path ends in "elements" and the default
company-collection path ends in "elements-company". Both
FileElementCollectionItem::isCustomCollection() and
ElementsLocation::isCustomCollection() used startsWith(customDir),
so "…/elements-company/…" matched "…/elements" and returned true.
This caused ElementsCollectionModel::addLocation() to insert a
newly-saved user-collection element as a child of the company-
collection branch in the tree, making it appear in the wrong panel.
Fix: require the path to equal the directory root exactly, or to
start with the directory root followed by '/'.
path == dir || path.startsWith(dir + QLatin1Char('/'))
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous logic resolved duplicate connectionDesignation values by
appending sanitised description text or numeric suffixes, which was
fragile and produced names an electrician could not easily map back to
the physical wiring.
New scheme:
• terminalNr present → "XDI.2", "XRO1.3", "XPOW.1" …
• terminalNr absent → designation as-is ("L1/U1", "UDC+", "PE") —
these are busbar / power connections and are already globally unique
• collision (malformed data) → numeric suffix as safety net
The description (connectiondescription) remains as the human-readable
label beside the terminal symbol, exactly as suggested by plc-user.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EdzPin gained a third field (group) but the fallback initializer at
line 70 still listed only two fields, triggering -Wmissing-field-initializers.
Added the explicit QString() for group.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EPLAN 2022-style part.xml files (e.g. IFM AL1122) use a numeric
functiondefgroup attribute and do not carry the text functiondefinition
block name that the previous grouping logic relied on. Those parts
fell back to grouping by pin designation, producing symbols with all
pin "1"s stacked together, then all "2"s, etc. — the bug reported in
PR #513.
Fix: read terminalNr first (the physical M12/connector socket identifier,
e.g. "X01", "X31") as the primary group key; fall back to functiondefinition
text for older EPLAN formats that omit terminalNr. Pins within each
connector group are still sorted by designation using natural sort.
Also remove the dashed inter-group separator lines; the existing 5 px
gap between groups provides sufficient visual separation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous grouping compared `connectionDesignation` values (e.g. "1",
"2", "3") to detect group boundaries. Those values are pin position
numbers within a terminal block, not functional group identifiers, so
devices like the ABB ACS880 produced 11+ tiny single-pin "groups" instead
of the ~8 functional blocks (AC-IN, Motor-OUT, DC-Bus, Brake-Resistor,
Analog-I/O, Digital-I/O, ...) the reviewer identified.
Fix:
- Add `group` field to EdzPin, populated from the `functiondefinition`
attribute on <functiontemplate> (newer EPLAN) or its parent <function>
element (older EPLAN).
- Sort pins by group first (preserving XML appearance order per group),
then by designation within each group using natural sort.
- Use `groupKey()` — group when present, designation as fallback — for
the group-break detection that drives separator lines and Y-gaps.
Parts without any functiondefinition data retain the previous
designation-based behaviour unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- StyleEditor: QGridLayout(this) pre-empted the widget's layout slot,
causing setLayout(main_layout) to silently fail and orphan main_layout.
Fix: use QGridLayout() without a parent so setLayout() succeeds.
- ExportDialog: ~ExportDialog() was empty, leaving ExportDiagramLine
heap objects in diagram_lines_ unfreed. Fix: qDeleteAll(diagram_lines_).
- GenericPanel::getItemForDiagram: when called without the bool* created
arg, it created a parentless QTreeWidgetItem that callers immediately
discarded. Fix: return nullptr when created==nullptr and item not found
(all callers already guard with if (item)).
- ElementScene: m_paste_area (created in initPasteArea) was temporarily
added/removed from the scene during XML loading but never freed in the
destructor. Fix: delete it if not currently in the scene.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Terminal stores its TerminalData* as member d but never deletes it.
Every Element creation (placing on diagram, loading icon for the
element browser, drag previews) leaks one TerminalData per terminal.
ASan confirmed 112 leaked objects (9856 bytes) in a short session
across four call sites all rooted in Element::parseTerminal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pins are already sorted by designation (see EdzPart::parse natural sort).
This change makes same-designation groups visually obvious in the element
symbol by:
- Inserting a 5 px gap between consecutive terminals whose designation
differs, so each group reads as a distinct block.
- Drawing a thin dashed horizontal line through each gap, mirroring
the grouped-I/O style shown in typical manufacturer datasheets.
The body rectangle and bounding box grow automatically to accommodate the
extra gaps, so no fixed sizes change. Unique terminal names (added in the
previous commit) are unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two issues raised in PR review:
1. Copyright year: new files carried "2006" (the project's founding year).
Updated to "2006-2026" to reflect the actual authorship period.
2. Duplicate terminal names: EPLAN parts can have multiple connection
templates sharing the same connectionDesignation (e.g. a drive where
both wire entries of terminal "1" are labelled "1"). QET requires
unique terminal names for wiring and terminal-diagram generation.
Resolution order in EdzElementBuilder::build():
- Unique designation → used as-is.
- Duplicated designation with distinct sanitised description →
"designation_description" (e.g. "1_L_P").
- Otherwise → numeric suffix: "1", "1_2", "1_3", …
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. machine_info.h: zero-initialise Screen struct members
Max_width, Max_height, count, width[] and height[] were bare
int32_t with no initialiser. The comparisons in
init_get_Screen_info() read them before any write, producing
undefined behaviour flagged by Valgrind as 'Conditional jump
or move depends on uninitialised value(s)'.
2. main.cpp: pre-initialise MachineInfo on the main thread
MachineInfo::instance() was first called inside QtConcurrent::run(),
causing its constructor (which calls qApp->screens()) to run on a
background thread. QScreen methods are not thread-safe in Qt5.
Calling instance() once on the main thread before the worker
launches guarantees the singleton is fully built first; subsequent
calls from the worker just return the cached pointer.
3. qetdiagrameditor.h: move m_first_show before the QActionGroup members
C++ initialises members in declaration order. m_first_show was
declared after the QActionGroup members (line 256 vs 168). During
construction of m_row_column_actions_group(this), Qt dispatches a
QObject parent-change event that reaches QETDiagramEditor::event(),
which reads m_first_show before it has been initialised.
Moving the declaration to the top of the first private: block
ensures it is initialised before any member that can trigger events.
All three found via Valgrind --tool=memcheck on Ubuntu 22.04 / Qt 5.15.3.
Relates-to: PR #514 (same QtConcurrent thread-safety pattern).
Reviewer requested configdir/datadir instead of cached for consistency
with the surrounding code style.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
QStandardPaths::writableLocation() is not thread-safe in Qt5.
ElementsCollectionModel::reload() launches:
QtConcurrent::map(m_items_list_to_setUp, setUpData)
Each worker calls FileElementCollectionItem::setUpData()
→ collectionPath() → isCollectionRoot()
→ QETApp::userMacrosDir() → QETApp::dataDir()
→ QStandardPaths::writableLocation() ← SIGSEGV (null deref)
The crash was confirmed by Valgrind (address 0x0, inside
libQt5Core's writableLocation internals).
Fix: replace the bare QStandardPaths calls in dataDir() and
configDir() with a C++11 static-local lambda. The compiler
guarantees the lambda body runs exactly once across all threads
(magic statics, ISO C++11 §6.7). After the first (main-thread)
call the result is returned lock-free.
Relates-to: #492 (same QtConcurrent lifetime pattern fixed in
QETProject::writeBackup by PR #512).
- EdzArchive checks the archive magic up front: gives a clear message for
zip-format .edz (not yet supported) and unrecognised data, instead of an
opaque 7z decode error.
- Trim the vendored LZMA SDK headers to the decode closure actually used
(removes 21 unused encoder/multithread/Xz/Aes headers; 18 .c + 18 .h remain).
- Add sources/import/edz/README.md documenting the feature, the data mapping
and the bundled SDK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the QProcess->7z shim with the public-domain LZMA SDK (23.01) 7-Zip
reader, vendored under sources/import/edz/lzma/ (decode-only subset). New
edzsevenzip.cpp wraps SzArEx_Open/SzArEx_Extract and writes entries via Qt, so
EdzArchive no longer needs an external 7-Zip at runtime. Enables the C language
in CMake for the vendored sources.
Verified: the bundled decoder extracts all three ifm sample .edz and the
generated elements still match the Python oracle exactly (byte-correct decode),
with no 7z on the path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wires EPLAN .edz import into the elements panel. EdzImporter orchestrates
EdzArchive -> EdzPart -> EdzElementBuilder and writes the generated .elmt into a
destination collection folder (named by order number). A right-click
"Importer une piece EPLAN (.edz)..." action on a writable collection directory
opens a file picker, runs the importer into that folder's fileSystemPath() and
reloads the panel; errors surface via QetMessageBox. Modeled on newElement().
EdzImporter verified headless against the Python oracle for KG6000/MFH200/
R1D200; the panel wiring is built/tested via the WSL Qt5+KF5 build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port of the edz2qet.py prototype's mapping to C++. EdzPart parses an EPLAN
part.xml into a portable model (identity/metadata, localized names, connection
list); EdzElementBuilder turns that into a QET element (generic symbol: body
rectangle + one west-facing terminal per pin, per-pin labels, localized <name>s,
and elementInformations for the BOM).
Pins are natural-sorted by designation so they stack 1,2,3,4 regardless of the
order EPLAN lists them (MFH200 lists 1,3,4,2). Output verified structurally
identical (uuids aside) to the Python oracle for three ifm samples — KG6000
(4-pin), MFH200 (4-pin, reordered) and R1D200 (5-pin, incl. Dutch name) — and
the generated element loads in the QET editor with correct UTF-8.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First piece of native EPLAN Data Portal (.edz) import. A .edz is a 7-Zip
archive; QET previously had no archive handling. EdzArchive unpacks one to a
temporary directory and locates the contained part.xml.
The extraction backend is isolated behind extractWithSevenZipCli() so it can be
replaced with a bundled decompressor later without touching callers; this M0
step shells out to a 7-Zip CLI via QProcess. Verified on three ifm sample
parts (KG6000, MFH200, R1D200 — all 7z), plus the corrupt/missing-file paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
writeBackup() fires QtConcurrent::run(QET::writeToFile, ..., &m_backup_file)
fire-and-forget: the QFuture was discarded and nothing kept m_backup_file
alive until the worker finished. If the QETProject was destroyed first, the
worker wrote through the freed member -> use-after-free crash in
QET::writeToFile (intermittent; ~1/6 on short-lived CLI runs).
Store the QFuture and waitForFinished() in ~QETProject (and before
setFilePath() re-points the managed backup file). Also skip launching a new
backup while one is still running, so two threads never write m_backup_file
at once.
The Qt6 path is still a TODO stub and the QtConcurrent block is KF5-only, so
this affects only the Qt5/KF5 build that actually has the backup code.
Per review (plc-user): scope the reset to items currently painted with the
red Dense4Pattern instead of clearing every item's background. This avoids
clobbering other backgrounds (e.g. the amber "show this dir" highlight)
and skips needless item updates on large collections.
ElementsCollectionModel::highlightUnusedElement() only ever painted the
currently-unused elements red; it never cleared the background of items
that were no longer unused. So when an element was re-added to a project
and saved, its red 'unused' highlight persisted until the model was
rebuilt from scratch.
Reset every item's background before re-applying the highlight to the
current unused set.
The Windows search list for the qet_tb_generator plugin included
`~/Application Data/qet/qet_tb_generator.exe` as a fallback. That legacy
junction path is the same inaccessible location the standard-directories
change moved QET away from, and it never matched a pip install anyway:
`pip install qet_tb_generator` puts the executable in the Python Scripts
directory (`...\PythonXX\Scripts` on PATH, or
`%APPDATA%\Python\PythonXX\Scripts` for --user installs), not in
`QETApp::dataDir()`.
Pip installs are already found via QStandardPaths::findExecutable (PATH),
and manual binary drops via dataDir()/binary and the working directory.
The legacy entry only matched old manual drops into the inaccessible
folder, so remove it.
Refs qelectrotech-source-mirror#199
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
QETApp::languagesPath() defaulted to applicationDirPath() + "/lang/".
The Windows installer puts the executable in a bin/ subfolder while the
lang/ folder sits next to it (../lang), so that default points at a
non-existent bin/lang/ — qetTranslator.load() fails and setLanguage()
silently falls back to the French source language. This is the root
cause behind the long-standing 'language won't change / resets to
French' reports, and why launching via 'Lancer QET.bat' (which passes
--lang-dir=lang/) works around it.
When the folder next to the binary doesn't exist, fall back to the
sibling ../lang folder if present. Behaviour is unchanged for builds
that already ship lang/ next to the binary, and for the QET_LANG_PATH
and --lang-dir paths.
Fixes#86.
Saving a read-only project to a writable location (e.g. Save As to /tmp)
left it marked read-only, so it stayed uneditable until closed and
reopened. Two issues in QETProject::write():
- The guard refused to write whenever QFileInfo(path).isWritable() was
false. For a Save As to a *new* file that test is always false (the file
doesn't exist yet), so it could wrongly block saving a read-only project
elsewhere. Now it checks the directory's writability for a new file.
- After a successful write the read-only flag was never cleared. Since the
file was just written, it is writable, so clear it (setReadOnly(false)
emits readOnlyChanged, re-enabling editing live).
Fixes#217.
langFromSetting() truncated the system locale to two letters
(QLocale::system().name().left(2)), so a user on the default 'Système'
language whose locale is regional got the base-language translation
instead of their regional one. QET ships qet_pt_BR, qet_nl_BE and
qet_nl_NL, so e.g. a Brazilian user saw European Portuguese (and
untranslated strings fell back to the French source).
Keep the full locale name and, in setLanguage(), try the exact
translation, then the base language, then English (French stays the
native source). Brazilian/Belgian/Dutch users on 'system' now get their
regional translation; everyone else is unaffected.
Refs #421.
When a title block template uses custom variables (e.g. %{department},
%{owner}), the user previously had to declare each one by hand in the
folio properties 'Custom' tab before a value could be entered. Now the
template's undefined custom variables are added automatically, so the
user only fills in the values.
- listOfVariables() now extracts %{name} placeholders with a regex
(deduplicated) instead of a crude '%' strip that returned '{name}'.
- The folio properties widget merges the template's custom variables into
the Custom tab both on open (setProperties) and when the template is
changed, preserving any values already entered and skipping the
standard fields (title, author, date, ...) which have their own inputs.
Fixes#271 (variable auto-population; the revision-history request in the
thread is a separate feature).
The Save As 'location' dialog used a QFileNameEdit (accepts only
[0-9a-z_-.]) but labelled it 'New element name', which is confusing —
QET also has a separate, translatable display name shown in the
collection. Users reasonably tried to type a display name (spaces,
capitals) and it was rejected.
Rename the placeholder to 'Element file name' and add a tooltip noting
the accepted characters and that the display name is edited separately
in the element properties. Updates the English translation; other
languages fall back until re-translated.
Fixes#469.
QETProject schedules an asynchronous crash-recovery backup on construction
(writeBackup() -> QtConcurrent::run(QET::writeToFile, ..., &m_backup_file)).
In one-shot CLI mode the QETProject is destroyed as soon as the command
returns, while that background write still references its m_backup_file
member — an intermittent use-after-free segfault during teardown (~1 in 6
runs; observed on --resave and --set-titleblock).
A crash-recovery backup is meaningless for a short-lived headless command,
so add QETProject::setBackupEnabled(false), called from the CLI entry in
main(). writeBackup() then early-returns, so no background write is ever
launched. Fixes the crash for all CLI commands. See #492.
The first write-to-project CLI command, aimed at CI / revision workflows:
stamp title-block metadata onto every folio (and the project default),
then save. Each argument is key=value:
qelectrotech --set-titleblock in.qet out.qet revision=B date=today
Standard keys map to the documented title-block fields (title, author,
date, plant, location, revision, version, filename); date=today uses the
current date and an explicit date forces UseDateValue mode; any other key
is stored as a custom title-block field. Assignments are parsed up front
so a malformed one fails before writing.
Addresses the 'saving' side of the CLI-for-scripts request (#162).
Wire the shared PdfLinks helper into the headless --export-pdf path so
CLI-exported PDFs get the same internal cross-reference / folio-report
navigation as the GUI print export.
For each page, after rendering, the scene-to-page geometry is rebuilt
from the QPdfWriter (96 dpi, zero margins, page sized to the diagram so
the scale is ~1 with no centering) — deliberately NOT reusing the
QPrinter-based mapping — and passed to PdfLinks::injectCrossRefLinks().
After the painter closes, PdfLinks::convertUriToGoTo() rewrites the URI
annotations into native GoTo/FitR actions.
Builds on the helper extracted in the previous commit; no change to the
other CLI tools.
Move the PDF cross-reference hyperlink logic out of ProjectPrintWindow
into a standalone translation unit so it can be reused (the CLI PDF
export will call it next):
- injectCrossRefLinks(): emits the URI link annotations for a diagram's
cross-references and folio reports. The scene-to-page mapping is passed
in as a PageGeometry (transform + devToPdf + source-rect lookup) so each
caller supplies its own correct geometry, rather than the helper assuming
a QPrinter.
- convertUriToGoTo(): the PDF post-processor, moved verbatim.
ProjectPrintWindow stays a pure caller: it builds its PageGeometry from the
printer page layout exactly as before and calls the helper. No behavioural
change to GUI PDF export; no class-structure changes.
Per review guidance on #483.