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".
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.
The comments describing the terminal_names layout were inherited from a
previous version and no longer matched the actual assignment order:
terminal_names << nc_name << no_name << common_name;
i.e. [0]=NC, [1]=NO, [2]=Common
Update all affected comments to reflect the current storage order.
On macOS arm64 (Apple Silicon, Sequoia), exporting a PDF via
QPrintPreviewWidget leaves a black screen with only the mouse cursor
visible. Cmd+Tab restores the display; the exported PDF itself is
correct and clickable cross-reference links work fine.
Root cause
----------
requestPaint() is a slot connected to QPrintPreviewWidget::paintRequested.
Inside this slot the code was calling painter.end() manually, then
pdfConvertUriToGoTo(). On macOS arm64 the Qt5 paint cycle backed by
Metal/CALayer is asynchronous: closing the QPainter from *within* the
paintRequested slot interrupts the compositor before it has flushed the
backing store. The window goes black and never repaints because the
close() that follows immediately destroys it.
On x86_64 / older macOS (raster/CoreGraphics backend) the paint cycle is
synchronous, so the same code happened to work.
Fix
---
1. Remove the manual painter.end() and pdfConvertUriToGoTo() call from
requestPaint(). The QPainter is stack-allocated; it destructs normally
when the slot returns, which is the correct moment to flush the PDF.
2. In print(), capture the output file name before m_preview->print(),
then defer both pdfConvertUriToGoTo() and this->close() to the next
event-loop iteration via QTimer::singleShot(0, ...). This gives the
Metal compositor one full event-loop turn to finish compositing the
backing store before the window is torn down.
The fix is a no-op on all other platforms: QTimer::singleShot(0) posts
an event that fires in the very next iteration, so there is no perceptible
delay.
Tested
------
- macOS Sequoia 15.x, Apple M-series, Qt 5.15.x (arm64): black screen gone
- macOS 10.15 x86_64 VM, Qt 5.15.x: no regression
- Linux/Debian Qt 5.15.x: no regression
- PDF cross-reference links and GoTo/FitR destinations: unaffected
Fixes: black screen after PDF export on macOS arm64
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.
Fix a use-after-free crash (SIGSEGV in QRegion::begin, Qt5Gui+0x49af60)
confirmed by analysis of 19 coredumps. The crash was triggered when the
scene viewport clip region was freed during zoom/resize events while
QPicture::play() replayed drawPolyline commands through the scene painter.
Qt's raster engine then dereferenced a stale QRegionData pointer.
Root cause: CrossRefItem used three nested QPicture objects (m_drawing,
m_hdr_no_ctc, m_hdr_nc_ctc). The nested drawPicture() calls amplified
the use-after-free risk on any repaint event.
Fix: remove all QPicture from CrossRefItem entirely.
- updateLabel() now uses a QImage-backed dummy painter to compute
m_bounding_rect, m_shape_path and m_hovered_contacts_map geometry.
A bool m_update_map flag prevents the map from being overwritten
during paint().
- paint() calls drawAsCross()/drawAsContacts() directly on the scene
painter — no QPicture::play() anywhere in the class.
- buildHeaderContact() now draws NO/NC symbols directly onto the painter
instead of recording them into QPicture members.
Also fix mouseDoubleClickEvent: the element under the click is now found
directly from m_hovered_contacts_map using the event position, rather
than relying on m_hovered_contact which could be reset by hoverMoveEvent
between the two clicks of a double-click.
Also remove setBold(true) on terminal name labels: the Qt PDF/SVG/print
engine rendered bold at 4pt as extremely thick glyphs, making exports
unreadable. Normal weight at 4pt is correct and legible on all backends.
Fixes: SIGSEGV in CrossRefItem::paint() on zoom/resize
Fixes: double-click navigation unreliable on Xref contact symbols
Fixes: terminal name labels unreadable in PDF/SVG/print export
Add a new boolean property 'showTerminalName' (default: true) to
XRefProperties, with full persistence in XML and QSettings.
A new checkbox "Afficher les numéros de bornes dans les Xrefs" is
added to the XRefPropertiesWidget in the main display group (not in
the cross-only group), so it is active in both Cross and Contacts modes.
When unchecked, terminal names are hidden in all three rendering paths:
- drawContact() (Contacts mode: NO/NC/SW symbols)
- fillCrossRef() (Cross mode: NO and NC columns)
- setUpCrossBoundingRect() (Cross mode: bounding rect sizing)
Backward compatible: existing project files without the attribute
default to showTerminalName=true (no visual change).
Files changed:
sources/properties/xrefproperties.h
sources/properties/xrefproperties.cpp
sources/ui/xrefpropertieswidget.ui
sources/ui/xrefpropertieswidget.cpp
sources/qetgraphicsitem/crossrefitem.cpp
The previous sort used QString::toInt() to order terminal names,
which returns 0 for any string containing a non-numeric prefix
(e.g. "R1", "R2", "L1", "L2"...). This caused undefined sort order
and incorrect pole pairing in the Xref contact mirror.
Example: a 4-pole NC power contact with terminals R1..R8 was
displaying R1/R3, R5/R7, R2/R4, R6/R8 instead of the correct
R1/R2, R3/R4, R5/R6, R7/R8.
Fix: extract the trailing numeric part of each terminal name and
compare prefixes separately. If both names share the same prefix
and both have a trailing number, sort numerically on that number;
otherwise fall back to full string comparison.
This covers all naming conventions: "1"/"2"/"3", "R1"/"R2"/"R3",
"L1"/"L2"/"L3", etc.
Applied in both drawContact() and setUpCrossBoundingRect().
Three bugs were causing a crash (SIGSEGV in QRegion::begin) when
a power NC slave element was placed on a folio, even before linking
it to a master element.
1. m_drawing (QPicture) was never reset between updateLabel() calls.
QPicture accumulates paint commands — calling qp.begin() on an
existing QPicture appends to it rather than replacing its content.
After several updates (load, move, hover...) the picture became
corrupted and crashed on play().
Fix: reset m_drawing = QPicture() at the start of updateLabel().
2. m_drawed_contacts was only initialized to 0 in drawAsContacts(),
but not in drawAsCross(). When drawing in Cross mode, fillCrossRef()
called drawContact() with an uninitialized m_drawed_contacts value,
producing a garbage offset. The NC contact symbol uses drawPolyline()
with a sub-pixel Y coordinate (offset+2.5); with a random offset Qt
generated an invalid QRegion and crashed.
This explains why NC contacts crashed but NO contacts did not: the
NO symbol only uses drawLine() which is more tolerant of bad coords.
Fix: add m_drawed_contacts = 0 at the start of drawAsCross().
3. setUpCrossBoundingRect() used QRectF() (null rect) as the reference
rect for painter.boundingRect(), which can return invalid dimensions.
Additionally, height was accumulated incorrectly: united() + setHeight()
doubled the height at each iteration, causing an exponentially growing
bounding rect with multiple contacts.
Fix: use QRectF(0, 0, 500, 20) as reference rect and accumulate
height and width independently.
Extend TerminalData::Type enum with three new semantic values:
- No : Normally Open terminal of a switch (SW) contact
- Nc : Normally Closed terminal of a switch (SW) contact
- Common : Common terminal of a switch (SW) contact
Update typeToString() and typeFromString() accordingly.
Fully backward compatible: existing Generic/Inner/Outer types
are unchanged. Elements without typed terminals fall back
to the previous behavior (first 2 named terminals).
terminal: expose terminalType() as public accessor
Add Terminal::terminalType() returning the TerminalData::Type
of this terminal. This allows crossrefitem and other consumers
to filter terminals by semantic role (No, Nc, Common) without
accessing TerminalData internals directly.
terminaleditor: add No, Nc, Common entries to type combobox
Expose the three new TerminalData types (No, Nc, Common) in
the element editor UI so users can assign a semantic role to
each terminal of a SW contact element.
Also fix a pre-existing bug in updateForm() where m_type_cb
was incorrectly using m_orientation_cb->findData() instead
of m_type_cb->findData(), preventing the type from being
restored correctly when selecting a terminal.
terminaleditor: add No, Nc, Common entries to type combobox
Expose the three new TerminalData types (No, Nc, Common) in
the element editor UI so users can assign a semantic role to
each terminal of a SW contact element.
Also fix a pre-existing bug in updateForm() where m_type_cb
was incorrectly using m_orientation_cb->findData() instead
of m_type_cb->findData(), preventing the type from being
restored correctly when selecting a terminal.
When a slave element has named terminals in its element definition
(.elmt), the terminal names (e.g. 13/14 for NO, 11/12 for NC,
12/13/14 for SW) are now displayed on each side of the contact
symbol in the cross-reference view.
- NO/NC contacts: name[0] on the left, name[1] on the right
- SW contacts: name[0] (NO) top-left, name[1] (common) top-right,
name[2] (NC) bottom-left
Terminal names are read from Terminal::name() which is populated
from TerminalData::m_name during element parsing. If terminals are
not named, nothing is displayed (fully backward compatible).
Users are expected to name their own terminals in the element
editor to avoid duplicating elements in the official collection.
Due to the changes made in the commit "Add highlight current page in
ProjectView", there is a problem when moving diagrams in the ProjectView
using the keyboard. The diagrams lose focus after being moved.
The cause is: The DiagramItem loses its selection before the move
function is executed.
The code has been adjusted.
Disable the QTabWidget internal scroll buttons and add own buttons for
scroll 'one page left' and scroll 'one page right'. The scrolled
diagrams will be activated.
corresponding to operation of project and diagram tabs
- click on the item activates the corresponding diagram or project.
- double click opens the corresponding properties editor.
- selecting with the up and down arrow keys has the same effect.