Three new QUndoCommand subclasses (AddDiagramCommand, RemoveDiagramCommand,
MoveDiagramCommand) pushed onto the project's existing (already
project-scoped) undo stack, so folio structure edits are undoable
alongside every item-level edit already on that stack.
- QETProject::addDiagram()/detachDiagram() are the shared attach/detach
primitives: they mutate the diagram list, connect/disconnect the two
per-diagram signals set up at add time, and emit diagramAdded/
diagramRemoved. AddDiagramCommand and RemoveDiagramCommand call these
(via friend access) for both redo and undo, so a removed diagram is
parked rather than destroyed -- it's only actually deleted if the
command itself falls out of undo history while still detached.
- ProjectView reacts to diagramRemoved the same way it already reacted to
diagramAdded (tearing down/rebuilding the tab), so both directions of
both commands go through the same reactive path every other diagram
listener (project database, cross-references, generic panel) already
relies on.
- MoveDiagramCommand wraps a new ProjectView::setDiagramPosition(), which
performs the tab move and the project's diagramOrderChanged() list
reorder synchronously in one step, instead of relying on the queued
tabMoved connection (needed for interactive drag-and-drop) to catch up
later -- avoiding a second, redundant reorder from that queued call.
- Multi-folio delete and multi-folio move (QETDiagramEditor::removeDiagrams()
and the moveDiagram*(QList<Diagram*>) batch slots) wrap their per-diagram
loop in QUndoStack::beginMacro()/endMacro(), so a multi-select action is
one undo step, matching current UX.
- Softened the delete confirmation's "this change is irreversible" wording
now that it no longer is.
Verified headlessly (Xvfb + xdotool + scrot): add/undo/redo, delete/undo/
redo (single and multi-select, single undo step for the batch), and
move/undo/redo all behave correctly against a 7-folio project.
Adds a ProjectUsageTracker (sources/project/) that accumulates how
long a project has been the active tab, using QElapsedTimer so the
value is computed on demand rather than via polling. It's hosted on
ProjectPropertiesHandler per that class's own stated design intent
("all new properties should be managed by this class").
The accumulated time is persisted as a new <usage time_spent="N"
enabled="true|false"/> element, a sibling of <properties> in the
project XML, written/read by new QETProject::writeUsageXml()/
readUsageXml(). It rides along on the existing autosave path for
free, since writeBackup() already serializes the full project via
toXml().
QETDiagramEditor::subWindowActivated() now pauses every open
project's tracker except the one whose tab just became current, so
switching between several open projects keeps each project's tracked
time isolated.
Surfaced in the existing Project Properties "Général" page: a
"Temps passé sur ce projet" display, a "Réinitialiser" button, and
an opt-out checkbox ("uniquement enregistré localement dans ce
fichier" - this is local-only, never transmitted anywhere).
Verified beyond compiling: full CMake build, then an actual runtime
session confirming the saved XML's time_spent value, that closing
and reopening the project round-trips and resumes timing, that the
reset button works, and - the key correctness check - that with two
projects open, the inactive one's time_spent stays frozen while the
active one accumulates real elapsed time, over the same interval.
See discussion #576.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
QET_LANG_PATH was "l10n/" for WIN32, a value that appears nowhere else in
the tree: the MSI shortcuts pass --lang-dir="[INSTALLDIR]lang/", and the
windows-build workflow copies the .qm files into files/lang/. So the
compiled-in default pointed at a directory no packaging creates, leaving
the command-line argument to do all the work.
Align it with what is actually shipped, so the binary-relative lookup
added in the previous commit can find the translations on its own.
Listing GuiPrivate unconditionally in QET_COMPONENTS breaks the whole
Qt5 configure: find_package(Qt5 COMPONENTS GuiPrivate) looks for a
Qt5GuiPrivateConfig.cmake that has never existed - Qt5 creates the
Qt5::GuiPrivate target implicitly together with Gui. Only Qt6 requires
(and provides) the explicit component.
Move the request into a QT_VERSION_MAJOR-guarded find_package after the
main one, both for the application and for tests/catch (whose targets
link Qt::GuiPrivate via QET_PRIVATE_LIBRARIES). Fixes the msys2/Qt5
Windows CI configure failure:
"Could not find a package configuration file provided by Qt5GuiPrivate".
Verified: Qt 6.11 configure passes and the Qt6::GuiPrivate target is
created (the private-module warning now fires from the guarded call).
The Qt5 path simply no longer requests the component, restoring the
pre-existing implicit behaviour.
Provide a small KAutoSaveFile-compatible implementation for the no-KF5 build path and use it to keep the existing crash-recovery code active when BUILD_WITH_KF5=OFF.
The normal KF5 build still uses the KDE KAutoSaveFile implementation.
Assisted-by: pi coding agent / Mika (OpenAI GPT-5.5)
The BUILD_WITH_KF5 option was checked with DEFINED, so passing -DBUILD_WITH_KF5=OFF still entered the KF5 setup path.
Skip the KF5 setup when disabled and provide small Qt-only replacements for the KDE color widgets used by .ui files in that build mode.
Assisted-by: pi coding agent / Mika (OpenAI GPT-5.5)
- Resolve cmake/qet_compilation_vars.cmake conflict: keep both upstream's
cli_export.cpp/h and pdf_links.cpp/h and the EDZ source additions.
- 10-position grid alignment: pin_y values are multiples of 10 so terminals
snap cleanly to QET's default grid. group_gap raised to 10 (one full slot).
- Named connector groups get a header label (group name) placed in the gap
above the first pin, so the electrician sees block names (XDI, XPOW, …)
without reading individual terminal designations.
- Device-tag dynamic_text now uses 9pt LABEL_FONT and y = min_y - 9 so it
clears the element body and is legible at normal zoom.
- Add EPLAN Data Portal Terms of Use disclaimer to sources/import/edz/README.md.
Co-Authored-By: Claude Sonnet 4.6 <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>
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.
Implements the long-requested batch/headless export
(bugtracker #171, GitHub #309): render a project's diagrams to files
without opening the GUI.
qelectrotech --export-pdf <project.qet> <output.pdf> one multi-page PDF
qelectrotech --export-png <project.qet> <output_dir> one PNG per diagram
qelectrotech --export-svg <project.qet> <output_dir> one SVG per diagram
main.cpp detects an export request before SingleApplication is created (so the
arguments are not forwarded to a running instance), spins up a plain
QApplication for rendering, and exits with the export's status code.
Rendering reuses Diagram::render() over
BorderTitleBlock::borderAndTitleBlockRect(), the same geometry the GUI
print/export path uses, so output matches the editor. Image files are named
NN_Title.<ext>.
New files: sources/cli_export.{h,cpp}, registered in
cmake/qet_compilation_vars.cmake.
directly to the related component on its folio, framing the target element.
When a project is exported to PDF, every cross-reference becomes an internal
link. Four kinds are covered:
- **Master → contact**: the contact list on a coil/relay (`CrossRefItem`)
- **Folio report → report**: report element labels (`DynamicElementTextItem`)
- **Slave → master**: the `(folio-position)` reference shown on a slave
(both standalone `DynamicElementTextItem` and grouped `ElementTextItemGroup`)
Clicking a link navigates **inside** the open document (no new viewer
instance) and zooms to frame the target element.
1. **Injection** (`printDiagram`, only when the paint engine is a `QPdfEngine`):
link rectangles are added with `QPdfEngine::drawHyperlink()`. The scene→page
mapping is rebuilt to match exactly what `QGraphicsScene::render()` does
(top-left anchored, `KeepAspectRatio`, **no centering**), and rectangles are
passed in device pixels — `pageMatrix()` already applies the 72/resolution
scale and Y-flip internally.
2. Each link URL encodes the target page and the target element's rectangle, in
PDF points on its own page: `#page=N&fitr=L_B_R_T`.
3. **Post-processing** (`pdfConvertUriToGoTo`, run after the painter is closed):
the `/S /URI` annotations are rewritten to native `/S /GoTo` actions with a
`/D [pageObj 0 R /FitR L B R T]` destination, and the xref table is rebuilt.
Pages are enumerated from the `/Pages /Kids` tree (reliable), not by scanning
for `/Type /Page` in raw bytes.
- `sources/print/projectprintwindow.{cpp,h}` — injection + post-processing
- `sources/qetgraphicsitem/crossrefitem.{cpp,h}` — `hoveredContactsMap()` accessor; store text rect for hit area
- `sources/qetgraphicsitem/dynamicelementtextitem.h` — `slaveXrefItem()` / `masterElement()` accessors
- `sources/qetgraphicsitem/elementtextitemgroup.h` — `slaveXrefItem()` accessor
- `qelectrotech.pro`, `cmake/qet_compilation_vars.cmake` — enable Qt gui-private headers (`<private/qpdf_p.h>`)
- **Fit-to-page mode only.** Links are not injected in tiled mode (multiple
pages per folio), which would require a per-tile transform.
- Uses Qt private API (`QPdfEngine::drawHyperlink`), stable since Qt 4 but not
part of the public API; the build links against `gui-private`.
- Page-tree enumeration assumes the flat `/Kids` array Qt produces (no nested
page trees).
- The frame zoom is controlled by two constants in `destRectPdf` (`pad`,
`minSide`) and can be tuned.
- Tested on Qt5; the `/Kids` parsing and `pageMatrix` behaviour are identical on
Qt6.
Those desktop MIME types were needed only with KDE up to 3.x, as it
used to have its own desktop-based MIME type system. KDE 3 is EOL for
many years now, and there are already XDG MIME types.
These files are the bare XML definitions, and they are automatically
generated by update-mime-database (part of shared-mime-info) on update
(e.g. by distro hooks) or manually. Keeping them in the sources, and
installing them, is definitely not correct, as qelectrotech.xml is
their canonical definition.
Hence, drop them from the sources, together with references to them.