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.
The dock editor snapshotted the conductor once and never refreshed, so a
change made via the modal Edit-conductor dialog left the dock holding stale
values; on deselection apply() wrote that stale snapshot back, overwriting
the dialog's change. Subscribe to Conductor::propertiesChange and refresh via
updateUi() so the dock mirrors external edits and apply() becomes a no-op
when nothing changed in the dock.
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.
Add English translations (qet_en.ts) for the new conductor-panel strings
so non-French users don't see untranslated source text: the panel title,
the two undo captions, the apply-to-all checkbox, and the View-menu
toggle + status tip.
The apply-to-all checkbox now reuses the modal dialog's exact wording
("Appliquer les propriétés a l'ensemble des conducteurs de ce potentiel")
for consistency and so it shares the existing translation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port the modal dialog's "apply to all conductors" option into the dock
panel, resolving the open propagation question. A persisted checkbox,
pinned above the tabs, makes each edit propagate to every conductor on
the same potential in one undo step - identical semantics to
ConductorPropertiesDialog, but the choice is remembered across edits.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses the testing feedback on the conductor selection-properties
prototype:
1. Edits were never applied. The dock drives every editor through
setLiveEdit(true), which each editor overrides to connect its field
changes to apply(); ConductorPropertiesEditorWidget didn't override it,
so the call was a no-op. Implement setLiveEdit() to connect the hosted
ConductorPropertiesWidget's controls (commit-style signals) to apply().
A m_updating guard suppresses the signals emitted while the widget is
loaded programmatically, so a partial mid-load state is never committed.
2. Clicking a conductor's text label showed nothing, because the label is
a ConductorTextItem, not a Conductor. Map it to its parentConductor() in
the editor factory, mirroring the double-click-the-label dialog behaviour.
3. The panel sat at a small size hint with empty space below it. Give the
editor an Expanding vertical size policy (and a minimum height) so it
fills the dock like the other editors.
Refs #500
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Three more read-only command-line tools for verifying connectivity and
cross-reference intelligence (useful for import / migration pipelines):
qelectrotech --export-nets <project.qet> <output.json>
qelectrotech --export-links <project.qet> <output.csv>
qelectrotech --resave <project.qet> <output.qet>
- --export-nets walks Conductor::relatedPotentialConductors() to group
every electrically-connected terminal into a net (potential), following
folio reports and terminal blocks across all folios. Output is JSON:
per net, the wire number and the list of {element, terminal, folio}.
This is the connectivity ground truth.
- --export-links reports each linkable element (master/slave/report/
terminal), its link type and the elements it links to, flagging
masters/slaves with no link as UNRESOLVED. Verifies coil<->contact
cross-references. Verified on examples/industrial.qet: 436 linkable
(76 master, 41 slave, ...), 37 unresolved.
- --resave loads the project and writes its XML back out, so an external
diff can reveal markup QET silently normalises on load
(tolerated-but-invalid XML). Round-trip verified: the re-saved project
reloads with identical diagram/element/conductor counts.
Extends the headless command-line interface with three read-only tools
aimed at validating projects and element libraries (useful for batch
import / migration pipelines):
qelectrotech --info <project.qet> [output.json]
qelectrotech --export-bom <project.qet> <output.csv>
qelectrotech --check-elements <element.elmt | directory>
- --info dumps a structural summary as JSON straight from QET's loaded
model: per-diagram element / conductor counts, page size, and the
number of unconnected ("free") terminals, plus project totals. Because
it uses the real loader it reports what the editor actually sees.
- --export-bom writes a bill of materials (one row per element) as CSV,
querying the project's own element_nomenclature_view (the same source
as the GUI BOM export). updateDB() is called first so the database is
populated in a headless run.
- --check-elements validates one .elmt file, or every .elmt under a
directory (recursively), against the element schema: XML well-formed,
root <definition type="element">, a usable bounding box, and terminal
count. Reports OK / WARN / FAIL per file and a summary; exit code is
non-zero if any file fails. Verified against the full bundled
collection (8483 elements): 0 false failures, agreeing with QET's own
loader (e.g. a negative-height element it tolerates is a WARN, not a
FAIL).
run() is restructured to handle the differing argument arity (info takes
an optional output, check-elements takes a path rather than a project).
renderDiagram() had a no-op stub: was_drawing_grid was set to false and
Q_UNUSED'd, so the editor grid still leaked into exported PDF/PNG/SVG.
Toggle Diagram::setDisplayGrid(false) around the render and restore the
previous state afterwards. Fixes all three export formats (they share
renderDiagram).
Reported by scorpio810 on #483.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends the headless command-line export with two CSV outputs:
qelectrotech --export-cables <project.qet> <output.csv> wiring list
qelectrotech --export-wires <project.qet> <output.csv> wire numbers
- --export-cables reuses WiringListExport (one row per conductor).
- --export-wires reuses ConductorNumExport::wiresNum() (distinct wire numbers).
WiringListExport::toCsv() mixed CSV generation with the file dialog and
writing. Extracted the generation into a new const method toCsvString()
that returns the CSV; toCsv() now calls it and writes the result. This
makes the wiring list usable headlessly with no behavioural change to the
GUI export.
Addresses part of the CLI export requests (#162, #309): @pkess specifically
asked to "export all connections as a list".