Compare commits

...

59 Commits

Author SHA1 Message Date
Laurent Trinques 36d0121038 Merge pull request #489 from ispyisail/cli-tools
CLI: add verification & data-export tools (info, BOM, nets, links, check-elements, resave)
2026-06-11 13:42:59 +02:00
Laurent Trinques 8235ecdbc9 Merge pull request #488 from Kellermorph/master
Issues 482
2026-06-11 13:32:30 +02:00
Shane Ringrose b6e4cd4786 CLI: add --export-nets, --export-links and --resave
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.
2026-06-11 23:23:13 +12:00
Shane Ringrose fb35027624 CLI: add --info, --export-bom and --check-elements verification tools
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).
2026-06-11 23:23:13 +12:00
Kellermorph 08a441d1f6 Issues 482 2026-06-11 13:22:01 +02:00
Laurent Trinques e9840728b4 Merge pull request #486 from qelectrotech/revert-484-master
Revert "Update-UI-Chinese-translation"
2026-06-11 12:28:55 +02:00
Laurent Trinques ffbcd12d9b Revert "Update-UI-Chinese-translation" 2026-06-11 12:22:45 +02:00
Laurent Trinques 0a124f6695 Merge pull request #484 from zi-mozhuang/master
Update-UI-Chinese-translation
2026-06-11 12:19:03 +02:00
Laurent Trinques 3f6f99b50f Merge pull request #485 from ispyisail/fix-cli-pdf-grid
CLI export: don't draw the editor grid in PDF/PNG/SVG output
2026-06-11 12:15:42 +02:00
Shane Ringrose 42b64a7f0a CLI export: disable the editor grid in rendered output
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>
2026-06-11 22:00:07 +12:00
Laurent Trinques 361719ca74 Fix my error in .pro 2026-06-11 11:39:25 +02:00
Laurent Trinques 181e2b555d Fix missing closing parenthesis in function call in .pro 2026-06-11 11:10:08 +02:00
Laurent Trinques 420512595d Update qelectrotech.pro
add sourc'es files missing in .pro Add headless command-line export (PDF / PNG / SVG / cable-list / wire-list)
#483
2026-06-11 11:07:25 +02:00
zi-mozhuang 9b4bed361d Update qet_zh.ts 2026-06-11 16:59:31 +08:00
Laurent Trinques 339bc8700b Merge pull request #483 from ispyisail/cli-export-master
Add headless command-line export (PDF / PNG / SVG / cable-list / wire-list)
2026-06-11 10:53:23 +02:00
Shane Ringrose 87d5ae5580 CLI export: add wiring list and wire-number list (CSV)
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".
2026-06-11 12:39:43 +12:00
Shane Ringrose 1070179617 Add headless command-line export (PDF/PNG/SVG)
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.
2026-06-11 11:10:09 +12:00
Laurent Trinques 6c4711a8d0 Update qet_compilation_vars.cmake 2026-06-07 15:11:06 +02:00
Laurent Trinques 8a8a338a2e Update CMakeLists.txt 2026-06-07 15:06:23 +02:00
Laurent Trinques 407cc7a4c2 Update CMakeLists.txt 2026-06-07 14:59:36 +02:00
Laurent Trinques 5cb8930732 Update fetch_pugixml.cmake 2026-06-07 14:57:29 +02:00
Laurent Trinques a24acfac24 Update fetch_pugixml.cmake 2026-06-07 14:57:05 +02:00
Laurent Trinques 8b0b1d10d4 git submodule update --remote elements 2026-06-05 11:28:36 +02:00
Laurent Trinques 57dfa28674 fix erroneous comments in drawContact for SW terminal names
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.
2026-06-05 11:21:08 +02:00
Laurent Trinques 3848c7821a Merge pull request #479 from ChuckNr11/master
fix possible crashes in crossrefitem
2026-06-05 11:01:44 +02:00
Laurent Trinques 1572c23d51 Fix FTBFS https://github.com/qelectrotech/qelectrotech-source-mirror/
pull/477
2026-06-05 10:46:00 +02:00
achim e234f063f8 fix possible crashes in crossrefitem
fix access to QList with potentially out-of-bounds index
2026-06-04 14:49:43 +02:00
Laurent Trinques be21604ad0 Merge pull request #477 from Kellermorph/update-german-translation
Fix: Dynamic element text shifting/jumping when duplicating diagrams
2026-06-01 21:10:06 +02:00
Kellermorph e1ccc1e568 Fix: Dynamic element text shifting/jumping when duplicating diagrams 2026-06-01 11:25:25 +02:00
Laurent Trinques e202b5bc2b Merge pull request #475 from Kellermorph/update-german-translation
Update German translations for duplicate diagram feature
2026-05-31 16:05:45 +02:00
Laurent Trinques 2b7e62f901 [PATCH] print: fix black screen on macOS arm64 after PDF export
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
2026-05-31 13:01:52 +02:00
Kellermorph 23e8258ae1 Update German translations for duplicate diagram feature 2026-05-31 10:48:09 +02:00
Laurent Trinques 457d265f0a Update translation files 2026-05-30 21:27:19 +02:00
Laurent Trinques cd76b6a1d6 This adds native, clickable hyperlinks to PDF exports: cross-references jump
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.
2026-05-30 18:48:28 +02:00
Laurent Trinques b522a94556 Merge pull request #473 from Kellermorph/makro-fix
Feat: Add ability to duplicate diagrams/folios with all metadata and …
2026-05-30 05:27:35 +02:00
Kellermorph 399bc0e897 Feat: Add ability to duplicate diagrams/folios with all metadata and elements 2026-05-29 19:14:36 +02:00
Laurent Trinques de9eeed542 Merge pull request #472 from Kellermorph/makro-fix
Feature: Allow excluding specific elements from BOM (Nomenclature)
2026-05-28 14:17:46 +02:00
Kellermorph c071e92c58 Feature: Allow excluding specific elements from BOM (Nomenclature) 2026-05-28 12:23:54 +02:00
Laurent Trinques d22e4abf96 Update translations files 2026-05-28 10:00:09 +02:00
Laurent Trinques 38b91e8083 Merge pull request #471 from Kellermorph/makro-fix
Potential Isolation option for terminals
2026-05-28 09:45:04 +02:00
Kellermorph 19704cf5ca Potential Isolation option for terminals 2026-05-27 21:20:50 +02:00
Laurent Trinques 471d1f2538 Merge pull request #470 from Kellermorph/makro-fix
Fix: Wiring list filter and dynamic text timing
2026-05-27 06:19:06 +02:00
Kellermorph dc52105868 Fix: Wiring list filter and dynamic text timing 2026-05-26 18:31:37 +02:00
Laurent Trinques 1b8dc5f410 texteditor: set font size spinbox minimum to 4pt to prevent SIGSEGV 2026-05-25 13:14:57 +02:00
Laurent Trinques 26d5d019cc texteditor: fix SIGSEGV caused by font size spinbox reaching 0 2026-05-25 13:04:37 +02:00
Laurent Trinques 6dd7c2d926 crossrefitem: fix SIGSEGV crash + improve double-click navigation + fix PDF/SVG/print rendering
Windows Build / build-windows (push) Has been cancelled
Test Windows VS2026 migration / Build on windows-2025-vs2026 (push) Has been cancelled
Windows Build / deploy-pages (push) Has been cancelled
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
2026-05-24 16:00:45 +02:00
Laurent Trinques 47796e183a Update translation files 2026-05-23 19:28:22 +02:00
Laurent Trinques ddf5ffcd89 xref: add option to hide terminal names in cross-references
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
2026-05-23 19:23:43 +02:00
Laurent Trinques 7d718bb9a0 crossrefitem: fix terminal name sorting for power contacts
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().
2026-05-23 15:50:11 +02:00
Laurent Trinques d949e6eb8c crossrefitem: fix SIGSEGV when dropping power NC contact on diagram
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.
2026-05-23 15:32:28 +02:00
Laurent Trinques dbda958261 terminaldata: add No, Nc, Common types for SW contacts
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.
2026-05-23 02:09:32 +02:00
Laurent Trinques d691489165 Update translations files 2026-05-22 10:16:24 +02:00
Laurent Trinques 202ea38e40 Merge pull request #464 from Kellermorph/makro-fix
New element: Line definition
2026-05-22 10:06:20 +02:00
Kellermorph 86dafcb576 Merge branch 'master' into makro-fix 2026-05-21 20:52:18 +02:00
Kellermorph f416c2a97e New element: Line definition 2026-05-21 20:47:44 +02:00
Laurent Trinques 7426fedba3 crossrefitem: display terminal names on contact symbols in Xref
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.
2026-05-21 17:10:44 +02:00
Laurent Trinques 027050c7e7 Update windows-msi.yml 2026-05-21 14:32:07 +02:00
Laurent Trinques fc948ad963 QElectroTech.wxs — add ProductCode="$(var.ProductCode)" to <Package>
+ AllowSameVersionUpgrades="yes" to <MajorUpgrade>
windows-msi.yml — in the ‘Extract version’ step, calculate a unique GUID
based on the commit’s SHA, then pass -d ‘ProductCode=$productGuid’ to the WIX build
This ensures that each build will have a different ProductCode → MajorUpgrade will always be triggered
2026-05-21 14:10:17 +02:00
Laurent Trinques 24d075b64c msi: add The AllowSameVersionUpgrades="yes" setting forces uninstallation
even when the MSI version is identical (which is the case here, as 0.100.1.0
remains unchanged between two nightly builds).
2026-05-21 13:59:53 +02:00
79 changed files with 17649 additions and 18327 deletions
+14
View File
@@ -75,10 +75,21 @@ jobs:
$count = git rev-list HEAD --count 2>$null $count = git rev-list HEAD --count 2>$null
$rev = [int]$count + 473 $rev = [int]$count + 473
$verDisplay = "${ver}-r${rev}-${sha}_x86_64-win64" $verDisplay = "${ver}-r${rev}-${sha}_x86_64-win64"
# Generate a unique ProductCode GUID from the commit SHA
# This ensures MajorUpgrade always triggers, even for same-version builds
$fullSha = git rev-parse HEAD 2>$null
if (-not $fullSha) { $fullSha = [System.Guid]::NewGuid().ToString() }
$bytes = [System.Text.Encoding]::UTF8.GetBytes($fullSha)
$md5 = [System.Security.Cryptography.MD5]::Create().ComputeHash($bytes)
$guidBytes = [byte[]]$md5[0..15]
$productGuid = [System.Guid]::new($guidBytes).ToString().ToUpper()
echo "VERSION_MSI=$verMsi" >> $env:GITHUB_OUTPUT echo "VERSION_MSI=$verMsi" >> $env:GITHUB_OUTPUT
echo "VERSION_DISPLAY=$verDisplay" >> $env:GITHUB_OUTPUT echo "VERSION_DISPLAY=$verDisplay" >> $env:GITHUB_OUTPUT
echo "PRODUCT_GUID=$productGuid" >> $env:GITHUB_OUTPUT
Write-Host "Version MSI : $verMsi" Write-Host "Version MSI : $verMsi"
Write-Host "Version display : $verDisplay" Write-Host "Version display : $verDisplay"
Write-Host "Product GUID : $productGuid"
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 4. Install WiX v7, accept EULA and install WixUI extension # 4. Install WiX v7, accept EULA and install WixUI extension
@@ -202,10 +213,13 @@ jobs:
Write-Host " LicenseRtf : $licRtf" Write-Host " LicenseRtf : $licRtf"
Write-Host " Output : dist\$outputName" Write-Host " Output : dist\$outputName"
$productGuid = "${{ steps.version.outputs.PRODUCT_GUID }}"
wix build $wxs ` wix build $wxs `
-arch x64 ` -arch x64 `
-d "Version=$version" ` -d "Version=$version" `
-d "ProductVersion=$verDisplay" ` -d "ProductVersion=$verDisplay" `
-d "ProductCode=$productGuid" `
-d "FilesDir=$filesDir" ` -d "FilesDir=$filesDir" `
-d "LicenseRtf=$licRtf" ` -d "LicenseRtf=$licRtf" `
-ext WixToolset.UI.wixext ` -ext WixToolset.UI.wixext `
+2 -1
View File
@@ -16,7 +16,7 @@
include(cmake/hoto_update_cmake_message.cmake) include(cmake/hoto_update_cmake_message.cmake)
cmake_minimum_required(VERSION 3.14...3.19 FATAL_ERROR) cmake_minimum_required(VERSION 3.5...4.2)
project(qelectrotech project(qelectrotech
VERSION 0.100.1 VERSION 0.100.1
@@ -145,6 +145,7 @@ target_include_directories(
${QET_DIR}/sources/dataBase/ui ${QET_DIR}/sources/dataBase/ui
${QET_DIR}/sources/factory/ui ${QET_DIR}/sources/factory/ui
${QET_DIR}/sources/print ${QET_DIR}/sources/print
${QET_DIR}/sources/svg
) )
install(TARGETS ${PROJECT_NAME}) install(TARGETS ${PROJECT_NAME})
+2
View File
@@ -16,6 +16,7 @@
Name="QElectroTech" Name="QElectroTech"
Manufacturer="QElectroTech Team" Manufacturer="QElectroTech Team"
Version="$(var.Version)" Version="$(var.Version)"
ProductCode="$(var.ProductCode)"
UpgradeCode="A1B2C3D4-E5F6-7890-ABCD-EF1234567890" UpgradeCode="A1B2C3D4-E5F6-7890-ABCD-EF1234567890"
Language="1033" Language="1033"
Codepage="1252" Codepage="1252"
@@ -28,6 +29,7 @@
<!-- In-place upgrade: automatically uninstalls the previous version --> <!-- In-place upgrade: automatically uninstalls the previous version -->
<MajorUpgrade <MajorUpgrade
DowngradeErrorMessage="A newer version of QElectroTech is already installed." DowngradeErrorMessage="A newer version of QElectroTech is already installed."
AllowSameVersionUpgrades="yes"
Schedule="afterInstallInitialize" /> Schedule="afterInstallInitialize" />
<!-- Installation directory --> <!-- Installation directory -->
+1 -1
View File
@@ -25,7 +25,7 @@ if(BUILD_PUGIXML)
FetchContent_Declare( FetchContent_Declare(
pugixml pugixml
GIT_REPOSITORY https://github.com/zeux/pugixml.git GIT_REPOSITORY https://github.com/zeux/pugixml.git
GIT_TAG v1.11.4) GIT_TAG v1.15)
FetchContent_MakeAvailable(pugixml) FetchContent_MakeAvailable(pugixml)
else() else()
+7
View File
@@ -29,6 +29,7 @@ set(QET_COMPONENTS
set(QET_PRIVATE_LIBRARIES set(QET_PRIVATE_LIBRARIES
Qt::PrintSupport Qt::PrintSupport
Qt::Gui Qt::Gui
Qt::GuiPrivate # Required for QPdfEngine::drawHyperlink (PDF internal links)
Qt::Xml Qt::Xml
Qt::Svg Qt::Svg
Qt::Sql Qt::Sql
@@ -106,6 +107,8 @@ set(QET_RES_FILES
${QET_DIR}/sources/ui/configpage/generalconfigurationpage.ui ${QET_DIR}/sources/ui/configpage/generalconfigurationpage.ui
) )
set(QET_SRC_FILES set(QET_SRC_FILES
${QET_DIR}/sources/cli_export.cpp
${QET_DIR}/sources/cli_export.h
${QET_DIR}/sources/borderproperties.cpp ${QET_DIR}/sources/borderproperties.cpp
${QET_DIR}/sources/borderproperties.h ${QET_DIR}/sources/borderproperties.h
${QET_DIR}/sources/bordertitleblock.cpp ${QET_DIR}/sources/bordertitleblock.cpp
@@ -499,6 +502,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/SearchAndReplace/ui/replacefoliowidget.h ${QET_DIR}/sources/SearchAndReplace/ui/replacefoliowidget.h
${QET_DIR}/sources/SearchAndReplace/ui/searchandreplacewidget.cpp ${QET_DIR}/sources/SearchAndReplace/ui/searchandreplacewidget.cpp
${QET_DIR}/sources/SearchAndReplace/ui/searchandreplacewidget.h ${QET_DIR}/sources/SearchAndReplace/ui/searchandreplacewidget.h
${QET_DIR}/sources/svg/qetsvg.cpp
${QET_DIR}/sources/svg/qetsvg.h
${QET_DIR}/sources/titleblock/dimension.cpp ${QET_DIR}/sources/titleblock/dimension.cpp
${QET_DIR}/sources/titleblock/dimension.h ${QET_DIR}/sources/titleblock/dimension.h
@@ -713,6 +718,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/xml/terminalstripitemxml.cpp ${QET_DIR}/sources/xml/terminalstripitemxml.cpp
${QET_DIR}/sources/xml/terminalstripitemxml.h ${QET_DIR}/sources/xml/terminalstripitemxml.h
${QET_DIR}/sources/xml/terminalstriplayoutpatternxml.cpp
${QET_DIR}/sources/xml/terminalstriplayoutpatternxml.h
) )
set(TS_FILES set(TS_FILES
+509 -443
View File
File diff suppressed because it is too large Load Diff
+509 -443
View File
File diff suppressed because it is too large Load Diff
+509 -443
View File
File diff suppressed because it is too large Load Diff
+509 -444
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+509 -443
View File
File diff suppressed because it is too large Load Diff
+509 -443
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+509 -443
View File
File diff suppressed because it is too large Load Diff
+509 -443
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+507 -441
View File
File diff suppressed because it is too large Load Diff
+507 -441
View File
File diff suppressed because it is too large Load Diff
+505 -439
View File
File diff suppressed because it is too large Load Diff
+510 -444
View File
File diff suppressed because it is too large Load Diff
+505 -439
View File
File diff suppressed because it is too large Load Diff
+509 -443
View File
File diff suppressed because it is too large Load Diff
+504 -438
View File
File diff suppressed because it is too large Load Diff
+505 -439
View File
File diff suppressed because it is too large Load Diff
+509 -443
View File
File diff suppressed because it is too large Load Diff
+507 -441
View File
File diff suppressed because it is too large Load Diff
+509 -443
View File
File diff suppressed because it is too large Load Diff
+512 -446
View File
File diff suppressed because it is too large Load Diff
+505 -439
View File
File diff suppressed because it is too large Load Diff
+512 -446
View File
File diff suppressed because it is too large Load Diff
+507 -441
View File
File diff suppressed because it is too large Load Diff
+509 -443
View File
File diff suppressed because it is too large Load Diff
+507 -441
View File
File diff suppressed because it is too large Load Diff
+507 -441
View File
File diff suppressed because it is too large Load Diff
+507 -441
View File
File diff suppressed because it is too large Load Diff
+505 -439
View File
File diff suppressed because it is too large Load Diff
+297 -4861
View File
File diff suppressed because it is too large Load Diff
+505 -439
View File
File diff suppressed because it is too large Load Diff
+505 -439
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -230,7 +230,11 @@ RESOURCES += qelectrotech.qrc
TRANSLATIONS += lang/*.ts TRANSLATIONS += lang/*.ts
# Modules Qt utilises par l'application # Modules Qt utilises par l'application
QT += xml svg network sql widgets printsupport concurrent KWidgetsAddons KCoreAddons QT += xml svg network sql widgets printsupport concurrent KWidgetsAddons KCoreAddons gui-private
# Private Qt GUI headers (needed for QPdfEngine::drawHyperlink)
# gui-private should add this automatically, but some distros need it explicit
INCLUDEPATH += $$[QT_INSTALL_HEADERS]/QtGui/$$[QT_VERSION]/QtGui
# UI DESIGNER FILES AND GENERATION SOURCES FILES # UI DESIGNER FILES AND GENERATION SOURCES FILES
FORMS += $$files(sources/richtext/*.ui) \ FORMS += $$files(sources/richtext/*.ui) \
+678
View File
@@ -0,0 +1,678 @@
/*
Copyright 2006-2025 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "cli_export.h"
#include "bordertitleblock.h"
#include "conductornumexport.h"
#include "conductorproperties.h"
#include "dataBase/projectdatabase.h"
#include "diagram.h"
#include "diagramcontext.h"
#include "qetgraphicsitem/conductor.h"
#include "qetgraphicsitem/element.h"
#include "qetgraphicsitem/terminal.h"
#include "qetproject.h"
#include "wiringlistexport.h"
#include <QDir>
#include <QDirIterator>
#include <QDomDocument>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QPainter>
#include <QPdfWriter>
#include <QSet>
#include <QSqlError>
#include <QSqlQuery>
#include <QSvgGenerator>
#include <QTextStream>
namespace {
QTextStream out(stdout);
QTextStream err(stderr);
/// All CLI option flags, mapped to a short format name.
const QHash<QString, QString> &exportFlags()
{
static const QHash<QString, QString> flags {
{"--export-pdf", "pdf"},
{"--export-png", "png"},
{"--export-svg", "svg"},
{"--export-cables", "cables"},
{"--export-wires", "wires"},
{"--export-bom", "bom"},
{"--export-nets", "nets"},
{"--export-links", "links"},
{"--info", "info"},
{"--check-elements", "check"},
{"--resave", "resave"},
};
return flags;
}
/// Device tag of an element ("K1", "Q55"), falling back to its name.
QString elementLabel(Element *element)
{
const QString label = element->elementInformations()["label"].toString();
return label.isEmpty() ? element->name() : label;
}
/// Pixel rect of a diagram's border + title block (the printable page area).
QRect diagramRect(Diagram *diagram)
{
QRectF r = diagram->border_and_titleblock.borderAndTitleBlockRect();
r.adjust(0, 0, 1, 1); // include the 1px border line
return r.toAlignedRect();
}
/// A filesystem-safe per-diagram file stem: "01_Title".
QString diagramStem(Diagram *diagram, int index)
{
QString title = diagram->title();
title.replace(QRegularExpression("[^\\w \\-]"), "_");
title = title.simplified();
if (title.isEmpty())
title = "diagram";
return QStringLiteral("%1_%2")
.arg(index, 2, 10, QChar('0'))
.arg(title);
}
/// Render @p diagram into @p painter, fitting @p target to the page rect.
void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target)
{
const QRect source = diagramRect(diagram);
// Export without the editor grid: drawBackground() only paints it when
// draw_grid_ is set (default true), so toggle it off around the render
// and restore it afterwards.
const bool was_drawing_grid = diagram->displayGrid();
diagram->setDisplayGrid(false);
diagram->render(&painter, target, source, Qt::KeepAspectRatio);
diagram->setDisplayGrid(was_drawing_grid);
}
int exportPdf(QETProject &project, const QString &output)
{
const QList<Diagram *> diagrams = project.diagrams();
if (diagrams.isEmpty()) {
err << "No diagrams to export.\n";
return 1;
}
QPdfWriter writer(output);
writer.setCreator("QElectroTech");
writer.setResolution(96);
QPainter painter;
bool first = true;
for (Diagram *diagram : diagrams) {
const QRect r = diagramRect(diagram);
// Match the page to the diagram (in points: 1px @ 96dpi = 0.75pt).
const QPageSize page(QSizeF(r.width() * 72.0 / 96.0,
r.height() * 72.0 / 96.0),
QPageSize::Point);
writer.setPageSize(page);
writer.setPageMargins(QMarginsF(0, 0, 0, 0));
if (first) {
if (!painter.begin(&writer)) {
err << "Cannot open '" << output << "' for writing.\n";
return 1;
}
first = false;
} else {
writer.newPage();
}
const QRectF target(0, 0,
writer.width(), writer.height());
renderDiagram(diagram, painter, target);
}
painter.end();
out << "Exported " << diagrams.size() << " page(s) -> " << output << "\n";
return 0;
}
int exportImages(QETProject &project, const QString &format,
const QString &out_dir)
{
const QList<Diagram *> diagrams = project.diagrams();
if (diagrams.isEmpty()) {
err << "No diagrams to export.\n";
return 1;
}
QDir().mkpath(out_dir);
int index = 0;
for (Diagram *diagram : diagrams) {
++index;
const QRect r = diagramRect(diagram);
const QString path = QDir(out_dir).filePath(
diagramStem(diagram, index) + "." + format);
if (format == "svg") {
QSvgGenerator gen;
gen.setFileName(path);
gen.setSize(r.size());
gen.setViewBox(QRect(0, 0, r.width(), r.height()));
gen.setTitle(diagram->title());
QPainter painter(&gen);
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()));
painter.end();
} else { // png
QImage image(r.size(), QImage::Format_ARGB32);
image.fill(Qt::white);
QPainter painter(&image);
painter.setRenderHint(QPainter::Antialiasing, true);
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()));
painter.end();
if (!image.save(path)) {
err << "Failed to write '" << path << "'.\n";
return 1;
}
}
out << " " << path << "\n";
}
out << "Exported " << diagrams.size() << " diagram(s) -> " << out_dir << "\n";
return 0;
}
int exportCsv(QETProject &project, const QString &format, const QString &output)
{
QString csv;
if (format == "cables") {
WiringListExport wle(&project, nullptr);
csv = wle.toCsvString();
} else { // wires
ConductorNumExport cne(&project, nullptr);
csv = cne.wiresNum();
}
if (csv.isEmpty()) {
err << "Nothing to export (empty list).\n";
return 1;
}
QFile file(output);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
err << "Cannot open '" << output << "' for writing.\n";
return 1;
}
QTextStream fout(&file);
fout << csv;
file.close();
out << "Exported " << format << " list -> " << output << "\n";
return 0;
}
/// Quote a field for CSV output (RFC-4180 style, ';' delimiter).
QString csvField(const QString &value)
{
if (value.contains(';') || value.contains('"')
|| value.contains('\n') || value.contains('\r')) {
QString v = value;
v.replace('"', "\"\"");
return '"' % v % '"';
}
return value;
}
/// Bill of materials: one row per element, key component-data fields.
/// Pulls from QET's own project database (the same source as the GUI BOM
/// export), so the output matches what the editor produces.
int exportBom(QETProject &project, const QString &output)
{
// The project database is built lazily; force a (re)build before querying.
project.dataBase()->updateDB();
static const QStringList columns {
"label", "designation", "manufacturer", "manufacturer_reference",
"quantity", "location", "function", "title", "folio"
};
QSqlQuery query = project.dataBase()->newQuery(
"SELECT " % columns.join(", ") %
" FROM element_nomenclature_view ORDER BY label");
if (!query.exec()) {
err << "BOM query failed: " << query.lastError().text() << "\n";
return 1;
}
QString csv = columns.join(";") % "\n";
int rows = 0;
while (query.next()) {
QStringList values;
for (int i = 0; i < columns.size(); ++i)
values << csvField(query.value(i).toString());
csv += values.join(";") % "\n";
++rows;
}
QFile file(output);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
err << "Cannot open '" << output << "' for writing.\n";
return 1;
}
QTextStream fout(&file);
fout << csv;
file.close();
out << "Exported " << rows << " component(s) -> " << output << "\n";
return 0;
}
/// Count terminals on @p element that no conductor connects to.
int freeTerminals(Element *element)
{
int free = 0;
const QList<Terminal *> terminals = element->terminals();
for (Terminal *t : terminals)
if (t->conductorsCount() == 0)
++free;
return free;
}
/// Structural ground-truth dump of a project, as JSON, to stdout (or a file).
/// Uses QET's own loaded model, so it reports what the editor actually sees:
/// per-page element / conductor counts and unconnected terminals.
int exportInfo(QETProject &project, const QString &output)
{
const QList<Diagram *> diagrams = project.diagrams();
int total_elements = 0, total_conductors = 0, total_free = 0;
QJsonArray pages;
int index = 0;
for (Diagram *diagram : diagrams) {
++index;
const QList<Element *> elements = diagram->elements();
const int conductors = diagram->conductors().size();
int page_free = 0;
for (Element *e : elements)
page_free += freeTerminals(e);
const QRect r = diagramRect(diagram);
QJsonObject page;
page["index"] = index;
page["title"] = diagram->title();
page["folio"] = QStringLiteral("%1 of %2")
.arg(index).arg(diagrams.size());
page["width_px"] = r.width();
page["height_px"] = r.height();
page["elements"] = elements.size();
page["conductors"] = conductors;
page["free_terminals"] = page_free;
pages.append(page);
total_elements += elements.size();
total_conductors += conductors;
total_free += page_free;
}
QJsonObject root;
root["project"] = project.title();
root["diagrams"] = diagrams.size();
root["elements"] = total_elements;
root["conductors"] = total_conductors;
root["free_terminals"] = total_free;
root["pages"] = pages;
const QByteArray json =
QJsonDocument(root).toJson(QJsonDocument::Indented);
if (output.isEmpty()) {
out << QString::fromUtf8(json);
} else {
QFile file(output);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
err << "Cannot open '" << output << "' for writing.\n";
return 1;
}
file.write(json);
file.close();
out << "Wrote project info -> " << output << "\n";
}
return 0;
}
/// Validate one .elmt file against QET's element schema.
/// @return 0 = OK, 1 = warning (loads but suspicious), 2 = failure.
int checkOneElement(const QString &path)
{
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
out << "FAIL " << path << " (cannot open)\n";
return 2;
}
QDomDocument doc;
QString error;
int line = 0;
if (!doc.setContent(&file, &error, &line)) {
file.close();
out << "FAIL " << path << " (XML error line "
<< line << ": " << error << ")\n";
return 2;
}
file.close();
const QDomElement root = doc.documentElement();
if (root.tagName() != "definition" || root.attribute("type") != "element") {
out << "FAIL " << path << " (root is not <definition type=\"element\">)\n";
return 2;
}
bool w_ok = false, h_ok = false;
const double w = root.attribute("width").toDouble(&w_ok);
const double h = root.attribute("height").toDouble(&h_ok);
if (!w_ok || !h_ok || w == 0 || h == 0) {
out << "FAIL " << path << " (missing/zero bounding box "
<< root.attribute("width") << "x"
<< root.attribute("height") << ")\n";
return 2;
}
const int terminals = root.elementsByTagName("terminal").count();
// Negative dimensions are malformed but QET still loads them; surface as a
// warning rather than a failure so this agrees with QET's own loader.
if (w < 0 || h < 0) {
out << "WARN " << path << " (negative bounding box "
<< w << "x" << h << ", " << terminals << " terminals)\n";
return 1;
}
if (terminals == 0) {
out << "WARN " << path << " (loads, but 0 terminals)\n";
return 1;
}
out << "OK " << path << " (" << terminals << " terminals)\n";
return 0;
}
/// Validate a single .elmt file or every .elmt under a directory.
int checkElements(const QString &path)
{
QStringList files;
const QFileInfo info(path);
if (info.isDir()) {
QDirIterator it(path, {"*.elmt"}, QDir::Files,
QDirIterator::Subdirectories);
while (it.hasNext())
files << it.next();
files.sort();
} else if (info.isFile()) {
files << path;
} else {
err << "Not found: " << path << "\n";
return 2;
}
if (files.isEmpty()) {
err << "No .elmt files found under: " << path << "\n";
return 2;
}
int warnings = 0, failures = 0;
for (const QString &f : files) {
const int r = checkOneElement(f);
if (r == 1) ++warnings;
else if (r == 2) ++failures;
}
out << files.size() << " file(s), " << warnings
<< " warning(s), " << failures << " failure(s)\n";
return failures > 0 ? 1 : 0;
}
/// Map every element in the project to its 1-based folio (page) position.
QHash<Element *, int> folioIndex(QETProject &project)
{
QHash<Element *, int> folio;
int index = 0;
const QList<Diagram *> diagrams = project.diagrams();
for (Diagram *diagram : diagrams) {
++index;
const QList<Element *> elements = diagram->elements();
for (Element *e : elements)
folio.insert(e, index);
}
return folio;
}
/// Electrical nets: groups of terminals joined into one potential.
/// Walks QET's own potential graph, so each net is a connected component
/// of terminals across all folios. The ground truth for connectivity.
int exportNets(QETProject &project, const QString &output)
{
const QHash<Element *, int> folio = folioIndex(project);
QList<Conductor *> all_conductors;
const QList<Diagram *> diagrams = project.diagrams();
for (Diagram *diagram : diagrams)
all_conductors << diagram->conductors();
QSet<Conductor *> visited;
QJsonArray nets;
int net_no = 0;
for (Conductor *c : all_conductors) {
if (visited.contains(c))
continue;
// The whole potential this conductor belongs to. relatedPotential-
// Conductors() also fills t_list with every terminal in the net
// (following folio reports and terminal blocks too).
QList<Terminal *> t_list;
QSet<Conductor *> group = c->relatedPotentialConductors(true, &t_list);
group.insert(c);
for (Conductor *g : group)
visited.insert(g);
if (c->terminal1) t_list << c->terminal1;
if (c->terminal2) t_list << c->terminal2;
// Wire number: smallest non-empty conductor text (deterministic).
QStringList wire_nos;
for (Conductor *g : group)
if (!g->properties().text.isEmpty())
wire_nos << g->properties().text;
wire_nos.sort();
++net_no;
QJsonArray terminals;
QSet<Terminal *> seen;
for (Terminal *t : t_list) {
if (!t || seen.contains(t))
continue;
seen.insert(t);
Element *pe = t->parentElement();
QJsonObject to;
to["element"] = pe ? elementLabel(pe) : QString();
to["terminal"] = t->name();
to["folio"] = pe ? folio.value(pe, 0) : 0;
terminals.append(to);
}
QJsonObject net;
net["net"] = net_no;
net["wire_no"] = wire_nos.value(0);
net["terminals"] = terminals;
nets.append(net);
}
QJsonObject root;
root["project"] = project.title();
root["nets"] = nets.size();
root["list"] = nets;
QFile file(output);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
err << "Cannot open '" << output << "' for writing.\n";
return 1;
}
file.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
file.close();
out << "Exported " << nets.size() << " net(s) -> " << output << "\n";
return 0;
}
/// Cross-references: each linkable element (coil / contact / report) and the
/// elements it links to, flagging masters/slaves with no link as unresolved.
int exportLinks(QETProject &project, const QString &output)
{
const QHash<Element *, int> folio = folioIndex(project);
QString csv("element;link_type;linked_to;folio;status\n");
int linkable = 0, unresolved = 0;
const QList<Diagram *> diagrams = project.diagrams();
for (Diagram *diagram : diagrams) {
const QList<Element *> elements = diagram->elements();
for (Element *e : elements) {
if (e->linkType() == Element::Simple)
continue;
++linkable;
const QList<Element *> linked = e->linkedElements();
QStringList names;
for (Element *le : linked)
names << elementLabel(le) % "(f"
% QString::number(folio.value(le, 0)) % ")";
QString status = "linked";
if ((e->linkType() == Element::Master
|| e->linkType() == Element::Slave)
&& linked.isEmpty()) {
status = "UNRESOLVED";
++unresolved;
}
csv += csvField(elementLabel(e)) % ";"
% e->linkTypeToString() % ";"
% csvField(names.join(", ")) % ";"
% QString::number(folio.value(e, 0)) % ";"
% status % "\n";
}
}
QFile file(output);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
err << "Cannot open '" << output << "' for writing.\n";
return 1;
}
QTextStream fout(&file);
fout << csv;
file.close();
out << "Exported " << linkable << " linkable element(s), "
<< unresolved << " unresolved -> " << output << "\n";
return 0;
}
/// Round-trip: load the project and write its XML back out, so an external
/// diff can reveal markup QET silently normalises (tolerated-but-invalid XML).
int resaveProject(QETProject &project, const QString &output)
{
const QDomDocument doc = project.toXml();
QFile file(output);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
err << "Cannot open '" << output << "' for writing.\n";
return 1;
}
QTextStream fout(&file);
fout << doc.toString(4);
file.close();
out << "Re-saved project -> " << output << "\n";
return 0;
}
} // anonymous namespace
namespace CLIExport {
bool isExportRequest(const QStringList &args)
{
for (const QString &a : args)
if (exportFlags().contains(a))
return true;
return false;
}
int run(const QStringList &args)
{
QString flag;
QStringList rest;
for (int i = 0; i < args.size(); ++i) {
if (exportFlags().contains(args.at(i))) {
flag = args.at(i);
for (int j = i + 1; j < args.size(); ++j)
rest << args.at(j);
break;
}
}
const QString format = exportFlags().value(flag);
// --check-elements operates on an element file/directory, not a project.
if (format == "check") {
if (rest.isEmpty()) {
err << "Usage: qelectrotech --check-elements "
"<element.elmt | directory>\n";
return 2;
}
return checkElements(rest.at(0));
}
const QString input = rest.value(0);
if (input.isEmpty()) {
err << "Usage: qelectrotech " << flag << " <project.qet> <output>\n";
return 2;
}
if (!QFileInfo::exists(input)) {
err << "Project not found: " << input << "\n";
return 2;
}
QETProject project(input);
if (project.state() != QETProject::Ok) {
err << "Failed to open project: " << input
<< " (state " << project.state() << ")\n";
return 1;
}
// --info writes JSON to stdout, or to an optional output file.
if (format == "info")
return exportInfo(project, rest.value(1));
const QString output = rest.value(1);
if (output.isEmpty()) {
err << "Usage: qelectrotech " << flag
<< " <project.qet> <output>\n";
return 2;
}
if (format == "pdf")
return exportPdf(project, output);
if (format == "cables" || format == "wires")
return exportCsv(project, format, output);
if (format == "bom")
return exportBom(project, output);
if (format == "nets")
return exportNets(project, output);
if (format == "links")
return exportLinks(project, output);
if (format == "resave")
return resaveProject(project, output);
return exportImages(project, format, output);
}
} // namespace CLIExport
+74
View File
@@ -0,0 +1,74 @@
/*
Copyright 2006-2025 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CLI_EXPORT_H
#define CLI_EXPORT_H
#include <QStringList>
/**
@brief Headless command-line export.
Implements the long-requested batch/headless export
(qelectrotech.org bugtracker #171, GitHub #309): render a project's
diagrams to files without opening the GUI.
Detected and handled in main() before the GUI is created.
*/
namespace CLIExport {
/**
@brief True if @p args request a CLI export
(i.e. contain one of the export options).
*/
bool isExportRequest(const QStringList &args);
/**
@brief Run the CLI export described by @p args.
@return process exit code (0 on success).
Usage:
qelectrotech --export-pdf <project.qet> <output.pdf>
qelectrotech --export-png <project.qet> <output_dir>
qelectrotech --export-svg <project.qet> <output_dir>
qelectrotech --export-cables <project.qet> <output.csv>
qelectrotech --export-wires <project.qet> <output.csv>
qelectrotech --export-bom <project.qet> <output.csv>
qelectrotech --export-nets <project.qet> <output.json>
qelectrotech --export-links <project.qet> <output.csv>
qelectrotech --info <project.qet> [output.json]
qelectrotech --check-elements <element.elmt | directory>
qelectrotech --resave <project.qet> <output.qet>
PDF: one multi-page document (one diagram per page).
PNG/SVG: one file per diagram, named <output_dir>/<NN>_<title>.<ext>.
cables: wiring list (one row per conductor) as CSV.
wires: list of distinct wire numbers as CSV.
bom: bill of materials (one row per element) as CSV.
nets: electrical nets (connected-terminal groups) as JSON.
links: element cross-references (coil/contact) as CSV, with
unresolved links flagged.
info: structural project summary as JSON (stdout, or a file) —
per-page element / conductor counts and unconnected terminals.
check-elements: validate .elmt file(s) against the element schema.
resave: load and rewrite the project XML (round-trip integrity).
*/
int run(const QStringList &args);
}
#endif // CLI_EXPORT_H
+2 -2
View File
@@ -383,7 +383,7 @@ void projectDataBase::createElementNomenclatureView()
"ei.supplier_auxiliary4 AS supplier_auxiliary4," "ei.supplier_auxiliary4 AS supplier_auxiliary4,"
"ei.quantity_auxiliary4 AS quantity_auxiliary4," "ei.quantity_auxiliary4 AS quantity_auxiliary4,"
"ei.unity_auxiliary4 AS unity_auxiliary4," "ei.unity_auxiliary4 AS unity_auxiliary4,"
"ei.exclude_from_bom AS exclude_from_bom,"
"d.pos AS diagram_position," "d.pos AS diagram_position,"
"e.type AS element_type," "e.type AS element_type,"
@@ -392,7 +392,7 @@ void projectDataBase::createElementNomenclatureView()
"di.folio AS folio," "di.folio AS folio,"
"e.pos AS position " "e.pos AS position "
" FROM element_info ei, diagram_info di, element e, diagram d" " FROM element_info ei, diagram_info di, element e, diagram d"
" WHERE ei.element_uuid = e.uuid AND e.diagram_uuid = d.uuid AND di.diagram_uuid = d.uuid"); " WHERE ei.element_uuid = e.uuid AND e.diagram_uuid = d.uuid AND di.diagram_uuid = d.uuid AND (ei.exclude_from_bom IS NOT 'true')");
QSqlQuery query(m_data_base); QSqlQuery query(m_data_base);
if (!query.exec(create_view)) { if (!query.exec(create_view)) {
+6 -1
View File
@@ -373,6 +373,11 @@ QString ElementQueryWidget::queryStr() const
where.clear(); where.clear();
} }
QString exclude_condition = "(exclude_from_bom IS NULL OR exclude_from_bom != '1')";
filter_ += " AND " + exclude_condition;
// -------------------------------------------------------------
if (where.isEmpty() && !filter_.isEmpty()) { if (where.isEmpty() && !filter_.isEmpty()) {
filter_.remove(0, 4); //Remove the first " AND" of filter. filter_.remove(0, 4); //Remove the first " AND" of filter.
filter_.prepend( " WHERE"); filter_.prepend( " WHERE");
@@ -456,7 +461,7 @@ void ElementQueryWidget::setUpItems()
{ {
for(QString key : QETInformation::elementInfoKeys()) for(QString key : QETInformation::elementInfoKeys())
{ {
if (key == "formula") if (key == "formula" || key == "exclude_from_bom")
continue; continue;
auto item = new QListWidgetItem(QETInformation::translatedInfoKey(key), ui->m_var_list); auto item = new QListWidgetItem(QETInformation::translatedInfoKey(key), ui->m_var_list);
+3 -2
View File
@@ -142,10 +142,11 @@ class Diagram : public QGraphicsScene
void wheelEvent (QGraphicsSceneWheelEvent *event) override; void wheelEvent (QGraphicsSceneWheelEvent *event) override;
void keyPressEvent (QKeyEvent *event) override; void keyPressEvent (QKeyEvent *event) override;
void keyReleaseEvent (QKeyEvent *) override; void keyReleaseEvent (QKeyEvent *) override;
void correctTextPos(Element* elmt);
void restoreText(Element* elmt);
public: public:
void correctTextPos(Element* elmt);
void restoreText(Element* elmt);
QUuid uuid(); QUuid uuid();
void setEventInterface (DiagramEventInterface *event_interface); void setEventInterface (DiagramEventInterface *event_interface);
void clearEventInterface(); void clearEventInterface();
+3 -2
View File
@@ -220,7 +220,7 @@ void DynamicTextFieldEditor::fillInfoComboBox()
QStringList strl; QStringList strl;
auto type = elementEditor()->elementScene()->elementData().m_type; auto type = elementEditor()->elementScene()->elementData().m_type;
if(type & ElementData::AllReport) { if((type & ElementData::AllReport) || (type == ElementData::ConductorDefinition)) {
strl = QETInformation::folioReportInfoKeys(); strl = QETInformation::folioReportInfoKeys();
} }
else { else {
@@ -365,7 +365,8 @@ void DynamicTextFieldEditor::on_m_text_from_cb_activated(int index) {
void DynamicTextFieldEditor::on_m_composite_text_pb_clicked() void DynamicTextFieldEditor::on_m_composite_text_pb_clicked()
{ {
bool isReport = false; bool isReport = false;
if (elementEditor()->elementScene()->elementData().m_type & ElementData::AllReport) { auto type = elementEditor()->elementScene()->elementData().m_type;
if ((type & ElementData::AllReport) || (type == ElementData::ConductorDefinition)) {
isReport = true; isReport = true;
} }
@@ -133,6 +133,7 @@ void ElementPropertiesEditorWidget::setUpInterface()
ui->m_base_type_cb->addItem (tr("Renvoi de folio précédent"), ElementData::PreviousReport); ui->m_base_type_cb->addItem (tr("Renvoi de folio précédent"), ElementData::PreviousReport);
ui->m_base_type_cb->addItem (tr("Bornier"), ElementData::Terminal); ui->m_base_type_cb->addItem (tr("Bornier"), ElementData::Terminal);
ui->m_base_type_cb->addItem (tr("Vignette"), ElementData::Thumbnail); ui->m_base_type_cb->addItem (tr("Vignette"), ElementData::Thumbnail);
ui->m_base_type_cb->addItem (tr("Définition de conducteur"), ElementData::ConductorDefinition);
// Slave option // Slave option
ui->m_state_cb->addItem(tr("Normalement ouvert"), ElementData::NO); ui->m_state_cb->addItem(tr("Normalement ouvert"), ElementData::NO);
@@ -188,6 +189,9 @@ void ElementPropertiesEditorWidget::updateTree()
case ElementData::PreviousReport: case ElementData::PreviousReport:
ui->m_tree->setDisabled(true); ui->m_tree->setDisabled(true);
break; break;
case ElementData::ConductorDefinition:
ui->m_tree->setDisabled(true);
break;
case ElementData::Master: case ElementData::Master:
ui->m_tree->setEnabled(true); ui->m_tree->setEnabled(true);
break; break;
+24 -2
View File
@@ -737,9 +737,10 @@ bool QETElementEditor::checkElement()
QList<QETWarning> errors; QList<QETWarning> errors;
// Warning #1: Element haven't got terminal // Warning #1: Element haven't got terminal
// (except for report, because report must have one terminal and this checking is do below) // (except for report and conductor definition, because they must have one terminal and this checking is done below)
if (!m_elmt_scene -> containsTerminals() && if (!m_elmt_scene -> containsTerminals() &&
!(m_elmt_scene->elementData().m_type & ElementData::AllReport)) { !(m_elmt_scene->elementData().m_type & ElementData::AllReport) &&
m_elmt_scene->elementData().m_type != ElementData::ConductorDefinition) {
warnings << qMakePair( warnings << qMakePair(
tr("Absence de borne", "warning title"), tr("Absence de borne", "warning title"),
tr( tr(
@@ -771,6 +772,27 @@ bool QETElementEditor::checkElement()
} }
} }
// Check conductor definition element
if (m_elmt_scene->elementData().m_type == ElementData::ConductorDefinition)
{
int terminal =0;
for(auto qgi : m_elmt_scene -> items()) {
if (qgraphicsitem_cast<PartTerminal *>(qgi)) {
terminal ++;
}
}
// Error: Conductor definition must have exactly one terminal
if (terminal != 1) {
errors << qMakePair (tr("Nombre de bornes incorrect"),
tr("<br><b>Erreur</b> :"
"<br>Les définitions de conducteur ne peuvent posséder qu'une seule borne."
"<br><b>Solution</b> :"
"<br>Vérifier que l'élément ne possède qu'une seule borne"));
}
}
if (!errors.count() && !warnings.count()) { if (!errors.count() && !warnings.count()) {
return(true); return(true);
} }
+4 -1
View File
@@ -61,7 +61,7 @@ void TerminalEditor::updateForm()
ui->m_y_dsb->setValue(m_part->property("y").toReal()); ui->m_y_dsb->setValue(m_part->property("y").toReal());
ui->m_orientation_cb->setCurrentIndex(ui->m_orientation_cb->findData(m_part->property("orientation"))); ui->m_orientation_cb->setCurrentIndex(ui->m_orientation_cb->findData(m_part->property("orientation")));
ui->m_name_le->setText(m_part->terminalName()); ui->m_name_le->setText(m_part->terminalName());
ui->m_type_cb->setCurrentIndex(ui->m_orientation_cb->findData(m_part->terminalType())); ui->m_type_cb->setCurrentIndex(ui->m_type_cb->findData(m_part->terminalType()));
activeConnections(true); activeConnections(true);
} }
@@ -122,6 +122,9 @@ void TerminalEditor::init()
ui->m_type_cb->addItem(tr("Générique"), TerminalData::Generic); ui->m_type_cb->addItem(tr("Générique"), TerminalData::Generic);
ui->m_type_cb->addItem(tr("Bornier intérieur"), TerminalData::Inner); ui->m_type_cb->addItem(tr("Bornier intérieur"), TerminalData::Inner);
ui->m_type_cb->addItem(tr("Bornier extérieur"), TerminalData::Outer); ui->m_type_cb->addItem(tr("Bornier extérieur"), TerminalData::Outer);
ui->m_type_cb->addItem(tr("NO (contact SW)"), TerminalData::No);
ui->m_type_cb->addItem(tr("NC (contact SW)"), TerminalData::Nc);
ui->m_type_cb->addItem(tr("Commun (contact SW)"), TerminalData::Common);
} }
/** /**
+1
View File
@@ -335,6 +335,7 @@ void TextEditor::setUpWidget(QWidget *parent)
m_size_sb = new QSpinBox(parent); m_size_sb = new QSpinBox(parent);
m_size_sb->setObjectName(QString::fromUtf8("m_size_sb")); m_size_sb->setObjectName(QString::fromUtf8("m_size_sb"));
m_size_sb->setMinimum(4);
gridLayout->addWidget(m_size_sb, 2, 1, 1, 1); gridLayout->addWidget(m_size_sb, 2, 1, 1, 1);
+58 -1
View File
@@ -16,7 +16,6 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>. along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "elementspanelwidget.h" #include "elementspanelwidget.h"
#include "diagram.h" #include "diagram.h"
#include "editor/ui/qetelementeditor.h" #include "editor/ui/qetelementeditor.h"
#include "elementscategoryeditor.h" #include "elementscategoryeditor.h"
@@ -26,6 +25,7 @@
#include "titleblock/templatedeleter.h" #include "titleblock/templatedeleter.h"
#include <QFileInfo> #include <QFileInfo>
#include <QMessageBox> #include <QMessageBox>
#include "qetgraphicsitem/element.h"
/* /*
When the ENABLE_PANEL_WIDGET_DND_CHECKS flag is set, the panel When the ENABLE_PANEL_WIDGET_DND_CHECKS flag is set, the panel
@@ -60,6 +60,7 @@ ElementsPanelWidget::ElementsPanelWidget(QWidget *parent) : QWidget(parent) {
prj_edit_prop = new QAction(QET::Icons::DialogInformation, tr("Propriétés du projet"), this); prj_edit_prop = new QAction(QET::Icons::DialogInformation, tr("Propriétés du projet"), this);
prj_prop_diagram = new QAction(QET::Icons::DialogInformation, tr("Propriétés du folio"), this); prj_prop_diagram = new QAction(QET::Icons::DialogInformation, tr("Propriétés du folio"), this);
prj_add_diagram = new QAction(QET::Icons::DiagramAdd, tr("Ajouter un folio"), this); prj_add_diagram = new QAction(QET::Icons::DiagramAdd, tr("Ajouter un folio"), this);
prj_duplicate_diagram = new QAction(QET::Icons::IC_CopyFile, tr("Copier et coller"), this);
prj_del_diagram = new QAction(QET::Icons::DiagramDelete, tr("Supprimer ce folio"), this); prj_del_diagram = new QAction(QET::Icons::DiagramDelete, tr("Supprimer ce folio"), this);
prj_move_diagram_up = new QAction(QET::Icons::GoUp, tr("Remonter ce folio"), this); prj_move_diagram_up = new QAction(QET::Icons::GoUp, tr("Remonter ce folio"), this);
prj_move_diagram_down = new QAction(QET::Icons::GoDown, tr("Abaisser ce folio"), this); prj_move_diagram_down = new QAction(QET::Icons::GoDown, tr("Abaisser ce folio"), this);
@@ -100,6 +101,7 @@ ElementsPanelWidget::ElementsPanelWidget(QWidget *parent) : QWidget(parent) {
connect(prj_prop_diagram, SIGNAL(triggered()), this, SLOT(editDiagramProperties())); connect(prj_prop_diagram, SIGNAL(triggered()), this, SLOT(editDiagramProperties()));
connect(prj_add_diagram, SIGNAL(triggered()), this, SLOT(newDiagram())); connect(prj_add_diagram, SIGNAL(triggered()), this, SLOT(newDiagram()));
connect(prj_del_diagram, SIGNAL(triggered()), this, SLOT(deleteDiagram())); connect(prj_del_diagram, SIGNAL(triggered()), this, SLOT(deleteDiagram()));
connect(prj_duplicate_diagram, SIGNAL(triggered()), this, SLOT(duplicateDiagram()));
connect(prj_move_diagram_up, SIGNAL(triggered()), this, SLOT(moveDiagramUp())); connect(prj_move_diagram_up, SIGNAL(triggered()), this, SLOT(moveDiagramUp()));
connect(prj_move_diagram_down, SIGNAL(triggered()), this, SLOT(moveDiagramDown())); connect(prj_move_diagram_down, SIGNAL(triggered()), this, SLOT(moveDiagramDown()));
connect(prj_move_diagram_top, SIGNAL(triggered()), this, SLOT(moveDiagramUpTop())); connect(prj_move_diagram_top, SIGNAL(triggered()), this, SLOT(moveDiagramUpTop()));
@@ -447,6 +449,7 @@ void ElementsPanelWidget::updateButtons()
} }
prj_del_diagram -> setEnabled(is_writable); prj_del_diagram -> setEnabled(is_writable);
prj_duplicate_diagram -> setEnabled(is_writable);
prj_move_diagram_up -> setEnabled(is_writable && min_position > 0); prj_move_diagram_up -> setEnabled(is_writable && min_position > 0);
prj_move_diagram_down -> setEnabled(is_writable && max_position < project_diagrams_count - 1); prj_move_diagram_down -> setEnabled(is_writable && max_position < project_diagrams_count - 1);
prj_move_diagram_top -> setEnabled(is_writable && min_position > 0); prj_move_diagram_top -> setEnabled(is_writable && min_position > 0);
@@ -501,6 +504,7 @@ void ElementsPanelWidget::handleContextMenu(const QPoint &pos) {
case QET::Diagram: case QET::Diagram:
context_menu -> addAction(prj_prop_diagram); context_menu -> addAction(prj_prop_diagram);
context_menu -> addAction(prj_del_diagram); context_menu -> addAction(prj_del_diagram);
context_menu -> addAction(prj_duplicate_diagram);
context_menu -> addAction(prj_move_diagram_top); context_menu -> addAction(prj_move_diagram_top);
context_menu -> addAction(prj_move_diagram_upx10); context_menu -> addAction(prj_move_diagram_upx10);
context_menu -> addAction(prj_move_diagram_upx100); context_menu -> addAction(prj_move_diagram_upx100);
@@ -593,3 +597,56 @@ void ElementsPanelWidget::keyPressEvent(QKeyEvent *e) {
break; break;
} }
} }
/**
* Duplicates the selected folios (pages) along with their content
* and properties, and cleanly resolves cross-references.
*/
void ElementsPanelWidget::duplicateDiagram()
{
QList<Diagram *> diagrams_to_duplicate = elements_panel->selectedDiagrams();
if (diagrams_to_duplicate.isEmpty()) return;
QETProject *project = diagrams_to_duplicate.first()->project();
if (!project || project->isReadOnly()) return;
for (Diagram *source_diagram : diagrams_to_duplicate) {
Diagram *new_diagram = project->addNewDiagram();
if (!new_diagram) continue;
QString template_name = source_diagram->border_and_titleblock.titleBlockTemplateName();
new_diagram->setTitleBlockTemplate(template_name);
TitleBlockProperties tbp = source_diagram->border_and_titleblock.exportTitleBlock();
new_diagram->border_and_titleblock.importTitleBlock(tbp);
BorderProperties bp = source_diagram->border_and_titleblock.exportBorder();
new_diagram->border_and_titleblock.importBorder(bp);
for (QGraphicsItem *item : source_diagram->items()) {
if (Element *elmt = dynamic_cast<Element *>(item)) {
source_diagram->correctTextPos(elmt);
}
}
QDomDocument doc = source_diagram->toXml();
QDomElement diagram_elmt = doc.documentElement();
for (QGraphicsItem *item : source_diagram->items()) {
if (Element *elmt = dynamic_cast<Element *>(item)) {
source_diagram->restoreText(elmt);
}
}
new_diagram->fromXml(diagram_elmt, QPointF(0, 0), false, nullptr);
for (QGraphicsItem *item : new_diagram->items()) {
if (Element *elmt = dynamic_cast<Element *>(item)) {
new_diagram->restoreText(elmt);
}
}
}
elements_panel->reload();
}
+2
View File
@@ -47,6 +47,7 @@ class ElementsPanelWidget : public QWidget {
*prj_prop_diagram, *prj_prop_diagram,
*prj_add_diagram, *prj_add_diagram,
*prj_del_diagram, *prj_del_diagram,
*prj_duplicate_diagram,
*prj_move_diagram_up, *prj_move_diagram_up,
*prj_move_diagram_top, *prj_move_diagram_top,
*prj_move_diagram_down, *prj_move_diagram_down,
@@ -88,6 +89,7 @@ class ElementsPanelWidget : public QWidget {
void editDiagramProperties(); void editDiagramProperties();
void newDiagram(); void newDiagram();
void deleteDiagram(); void deleteDiagram();
void duplicateDiagram();
void moveDiagramUp(); void moveDiagramUp();
void moveDiagramDown(); void moveDiagramDown();
void moveDiagramUpTop(); void moveDiagramUpTop();
+16
View File
@@ -15,6 +15,7 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>. along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "cli_export.h"
#include "machine_info.h" #include "machine_info.h"
#include "qet.h" #include "qet.h"
#include "qetapp.h" #include "qetapp.h"
@@ -22,6 +23,8 @@
#include "utils/macosxopenevent.h" #include "utils/macosxopenevent.h"
#include "utils/qetsettings.h" #include "utils/qetsettings.h"
#include <QApplication>
#include <QStyleFactory> #include <QStyleFactory>
#include <QtConcurrentRun> #include <QtConcurrentRun>
@@ -194,6 +197,19 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
#endif #endif
// Headless command-line export: render a project to PDF/PNG/SVG without
// opening the GUI, then exit. Must be handled before SingleApplication
// (which would forward the args to an already-running instance).
{
QStringList raw_args;
for (int i = 0; i < argc; ++i)
raw_args << QString::fromLocal8Bit(argv[i]);
if (CLIExport::isExportRequest(raw_args)) {
QApplication export_app(argc, argv);
return CLIExport::run(export_app.arguments());
}
}
SingleApplication app(argc, argv, true); SingleApplication app(argc, argv, true);
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
//Handle the opening of QET when user double click on a .qet .elmt .tbt file //Handle the opening of QET when user double click on a .qet .elmt .tbt file
+469 -3
View File
@@ -21,9 +21,16 @@
#include "../qeticons.h" #include "../qeticons.h"
#include "../qetproject.h" #include "../qetproject.h"
#include "../qetversion.h" #include "../qetversion.h"
#include "../qetgraphicsitem/crossrefitem.h"
#include "../qetgraphicsitem/dynamicelementtextitem.h"
#include "../qetgraphicsitem/elementtextitemgroup.h"
#include "ui_projectprintwindow.h" #include "ui_projectprintwindow.h"
// Private Qt PDF engine for drawHyperlink() — not public API, stable since Qt4
// Requires QT += gui-private in qelectrotech.pro
#include <private/qpdf_p.h>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
# include <QDesktopWidget> # include <QDesktopWidget>
#else #else
@@ -37,6 +44,11 @@
#include <QPrintDialog> #include <QPrintDialog>
#include <QPrintPreviewWidget> #include <QPrintPreviewWidget>
#include <QScreen> #include <QScreen>
#include <QFile>
#include <QRegularExpression>
#include <QMap>
#include <QTimer>
#include <QVector>
/** /**
* @brief ProjectPrintWindow::ProjectPrintWindow * @brief ProjectPrintWindow::ProjectPrintWindow
@@ -188,6 +200,241 @@ ProjectPrintWindow::~ProjectPrintWindow()
* @brief ProjectPrintWindow::requestPaint * @brief ProjectPrintWindow::requestPaint
* @param slot called when m_preview emit paintRequested * @param slot called when m_preview emit paintRequested
*/ */
/**
* @brief ProjectPrintWindow::pdfConvertUriToGoTo
* Post-processes a Qt-generated PDF to replace URI link annotations
* (file:///path/to/file.pdf#page=N) with native PDF GoTo actions
* ([pageObj 0 R /Fit]). This makes cross-reference links work in all
* PDF viewers regardless of where the file is stored.
*
* The function:
* 1. Reads the PDF as raw bytes.
* 2. Collects page object numbers in document order by scanning for
* objects that contain "/Type /Page" (but not "/Type /Pages").
* 3. Replaces every annotation action block
* /S /URI\n/URI (file://...#page=N)
* with
* /S /GoTo\n/D [<pageObj> 0 R /Fit]
* 4. Rebuilds the cross-reference table (offsets change because the
* replacement strings have different lengths).
* 5. Writes the result back to the same file.
*
* The function is intentionally conservative: if any step fails (file
* not found, malformed PDF, no URI annotations) it returns silently
* without corrupting the file.
*/
static void pdfConvertUriToGoTo(const QString &pdfPath)
{
// --- 1. Read raw bytes ---
QFile f(pdfPath);
if (!f.open(QIODevice::ReadOnly)) return;
QByteArray data = f.readAll();
f.close();
// --- 2. Collect page object numbers in document order ---
// Read them from the page tree (/Type /Pages -> /Kids [ N 0 R ... ]).
// This is reliable; scanning raw bytes for "/Type /Page" is NOT: that
// marker also occurs inside content streams, and a forward lookahead
// wrongly tags neighbouring objects (it found 280 "pages" for a 137-page
// document). Qt writes a single, flat /Kids array listing every page.
QVector<int> pageObjs;
{
int pagesPos = data.indexOf("/Type /Pages");
int kidsPos = (pagesPos == -1) ? -1 : data.indexOf("/Kids", pagesPos);
int lb = (kidsPos == -1) ? -1 : data.indexOf('[', kidsPos);
int rb = (lb == -1) ? -1 : data.indexOf(']', lb);
if (lb != -1 && rb != -1 && rb > lb) {
const QString kids =
QString::fromLatin1(data.mid(lb + 1, rb - lb - 1));
QRegularExpression re(QStringLiteral("(\\d+)\\s+\\d+\\s+R"));
auto it = re.globalMatch(kids);
while (it.hasNext()) {
int objNum = it.next().captured(1).toInt();
if (objNum > 0) pageObjs.append(objNum);
}
}
}
if (pageObjs.isEmpty()) return; // nothing to do
// --- 3. Replace URI annotations with GoTo ---
// Pattern (Qt always writes exactly this):
// /S /URI\n/URI (file:///...<anything>#page=N)\n
// or (older patches without file://):
// /S /URI\n/URI (page=N)\n
bool changed = false;
{
// We do a manual scan to handle variable-length replacements.
QByteArray out;
out.reserve(data.size());
const QByteArray sUri = "/S /URI\n/URI (";
const QByteArray sGoTo = "/S /GoTo\n/D [";
int pos = 0;
while (pos < data.size()) {
int found = data.indexOf(sUri, pos);
if (found == -1) {
out.append(data.mid(pos));
break;
}
// Copy everything up to the match
out.append(data.mid(pos, found - pos));
// Find closing ')' of the URI value
int uriStart = found + sUri.size();
int closeParen = data.indexOf(')', uriStart);
if (closeParen == -1) {
// Malformed — copy rest verbatim
out.append(data.mid(found));
pos = data.size();
break;
}
QByteArray uriVal = data.mid(uriStart, closeParen - uriStart);
// Extract page number: look for #page=N or bare page=N
int pageNum = -1;
int hashPos = uriVal.lastIndexOf("#page=");
int digitStart = -1;
if (hashPos != -1) {
digitStart = hashPos + 6;
} else if (uriVal.startsWith("page=")) {
digitStart = 5;
}
if (digitStart != -1) {
// Take only the leading digits: the fragment may carry extra
// parameters after the page number (e.g. "22&fitr=15_489_..."),
// and QByteArray::toInt() would fail on the whole remainder.
int e = digitStart;
while (e < uriVal.size()
&& uriVal[e] >= '0' && uriVal[e] <= '9')
++e;
if (e > digitStart)
pageNum = uriVal.mid(digitStart, e - digitStart).toInt();
}
if (pageNum >= 1 && pageNum <= pageObjs.size()) {
// Valid page reference — emit GoTo action.
int pageObjNum = pageObjs[pageNum - 1];
// Optional precise destination: &fitr=Left_Bottom_Right_Top
// (integer PDF points). If present -> /FitR (frame the element);
// otherwise -> /Fit (whole page, top).
QByteArray dest = " /Fit]";
int fr = uriVal.indexOf("fitr=");
if (fr != -1) {
QByteArray rest = uriVal.mid(fr + 5);
// stop at first char that is not part of the number list
int end = 0;
while (end < rest.size()
&& ((rest[end] >= '0' && rest[end] <= '9')
|| rest[end] == '_' || rest[end] == '-'))
++end;
QList<QByteArray> parts = rest.left(end).split('_');
if (parts.size() == 4) {
dest = " /FitR " + parts[0] + " " + parts[1] + " "
+ parts[2] + " " + parts[3] + "]";
}
}
QByteArray goTo = sGoTo
+ QByteArray::number(pageObjNum)
+ " 0 R" + dest;
out.append(goTo);
changed = true;
} else {
// Unknown page — keep original URI
out.append(sUri);
out.append(uriVal);
out.append(')');
}
pos = closeParen + 1; // skip past ')'
}
if (!changed) return; // nothing was replaced
data = out;
}
// --- 4. Rebuild xref table ---
// Find start of existing xref (last occurrence)
int xrefStart = data.lastIndexOf("\nxref\n");
if (xrefStart == -1) xrefStart = data.lastIndexOf("\nxref ");
if (xrefStart == -1) return; // malformed PDF
++xrefStart; // skip the leading '\n'
QByteArray body = data.left(xrefStart);
// Collect all object offsets from the body
QMap<int, int> offsets; // objNum -> byte offset
{
const QByteArray objMarker = " 0 obj";
int pos = 0;
while ((pos = body.indexOf(objMarker, pos)) != -1) {
int numStart = pos - 1;
while (numStart > 0 && body[numStart-1] != '\n' && body[numStart-1] != '\r')
--numStart;
QByteArray numStr = body.mid(numStart, pos - numStart).trimmed();
bool ok = false;
int objNum = numStr.toInt(&ok);
if (ok && objNum > 0)
offsets[objNum] = numStart;
++pos;
}
}
if (offsets.isEmpty()) return;
int maxObj = offsets.lastKey();
// Build xref table
QByteArray xref;
xref += "xref\n";
xref += "0 " + QByteArray::number(maxObj + 1) + "\n";
xref += "0000000000 65535 f \n";
for (int i = 1; i <= maxObj; ++i) {
if (offsets.contains(i)) {
xref += QByteArray::number(offsets[i]).rightJustified(10, '0')
+ " 00000 n \n";
} else {
xref += "0000000000 65535 f \n";
}
}
// Find trailer dict from the original xref section
int trailerPos = data.indexOf("trailer", xrefStart);
int trailerEnd = -1;
if (trailerPos != -1) {
trailerEnd = data.indexOf("%%EOF", trailerPos);
if (trailerEnd != -1) trailerEnd += 5;
}
QByteArray trailer;
if (trailerPos != -1 && trailerEnd != -1)
trailer = data.mid(trailerPos, trailerEnd - trailerPos);
else
trailer = "trailer\n<<>>\n%%EOF";
int newXrefOffset = body.size();
QByteArray result;
result.reserve(body.size() + xref.size() + trailer.size() + 30);
result += body;
result += xref;
result += trailer;
result += "\nstartxref\n";
result += QByteArray::number(newXrefOffset);
result += "\n%%EOF\n";
// --- 5. Write back ---
QFile out(pdfPath);
if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) return;
out.write(result);
out.close();
}
void ProjectPrintWindow::requestPaint() void ProjectPrintWindow::requestPaint()
{ {
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
@@ -214,13 +461,47 @@ void ProjectPrintWindow::requestPaint()
return; return;
} }
// Build diagram -> first physical PDF page number map (1-based)
// Must be done before the print loop since page numbers depend on order
QMap<Diagram*, int> diagramPageMap;
{
int pageNum = 1;
for (auto diagram : selectedDiagram()) {
diagramPageMap.insert(diagram, pageNum);
// Each diagram may span multiple pages if not fit_page
if (!ui->m_fit_in_page_cb->isChecked()) {
auto option = exportProperties();
bool full_page = m_printer->fullPage();
int h = horizontalPagesCount(diagram, option, full_page);
int v = verticalPagesCount(diagram, option, full_page);
pageNum += h * v;
} else {
pageNum += 1;
}
}
}
bool first = true; bool first = true;
QPainter painter(m_printer); QPainter painter(m_printer);
// A real PDF export uses the QPdfEngine; the on-screen preview uses a
// preview paint engine. We only post-process when actually writing a PDF.
const bool pdfExport =
(m_printer->outputFormat() == QPrinter::PdfFormat)
&& (dynamic_cast<QPdfEngine*>(painter.paintEngine()) != nullptr);
for (auto diagram : selectedDiagram()) for (auto diagram : selectedDiagram())
{ {
first ? first = false : m_printer->newPage(); first ? first = false : m_printer->newPage();
printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer); printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer, diagramPageMap);
} }
// Note: do NOT call painter.end() or pdfConvertUriToGoTo() here.
// We are inside the paintRequested slot: the QPrintPreviewWidget still
// owns the paint cycle. On macOS arm64 (Metal/CALayer compositor),
// closing the QPainter manually inside this slot leaves the backing
// store in an undefined state, producing a black screen after export.
// pdfConvertUriToGoTo() is deferred to print() via QTimer::singleShot(0).
} }
/** /**
@@ -230,7 +511,7 @@ void ProjectPrintWindow::requestPaint()
* @param fit_page * @param fit_page
* @param printer * @param printer
*/ */
void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer) void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap<Diagram*, int> &diagramPageMap)
{ {
////Prepare the print//// ////Prepare the print////
@@ -317,6 +598,171 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter
} }
} }
////Inject PDF cross-reference links////
if (printer->outputFormat() == QPrinter::PdfFormat && fit_page) {
auto *pdfEngine = dynamic_cast<QPdfEngine*>(painter->paintEngine());
if (pdfEngine) {
// QGraphicsScene::render() fait save()/restore() : worldTransform()
// est revenu a l'identite ici. On reconstruit DONC explicitement la
// transform appliquee par :
// diagram->render(painter, QRectF(), diagram_rect, KeepAspectRatio)
// cible vide => painter->viewport() ; source = diagram_rect ; centre.
const QRectF target = QRectF(painter->viewport());
const QRectF source = QRectF(diagram_rect); // meme source que render()
// render() ANCRE en haut-gauche (pas de centrage) :
// translate(target.topLeft) . scale(s,s) . translate(-source.topLeft)
// On reproduit EXACTEMENT ca — surtout PAS de (target-source*s)/2.
const qreal s = qMin(target.width() / source.width(),
target.height() / source.height());
QTransform fit;
fit.translate(target.x(), target.y());
fit.scale(s, s);
fit.translate(-source.x(), -source.y()); // scene -> pixels device
// IMPORTANT : QPdfEngine::drawHyperlink() applique lui-meme
// pageMatrix() (echelle 72/resolution + inversion de Y + marges).
// On lui passe donc le rectangle en PIXELS DEVICE, sans aucune
// conversion en points ni flip de notre cote.
const QRectF pageBounds(0, 0, target.width(), target.height());
// ---- Device-pixels -> PDF points, replicating QPdfEnginePrivate::pageMatrix()
// (same geometry for every page: same printer, page size and margins). ----
const qreal pt_scale = 72.0 / printer->resolution();
const qreal fullH_pt = printer->pageLayout().fullRectPoints().height();
const bool fullPageMode =
(printer->pageLayout().mode() == QPageLayout::FullPageMode);
const QRect paintPx =
printer->pageLayout().paintRectPixels(printer->resolution());
auto devToPdf = [=](const QPointF &d) -> QPointF {
qreal dx = d.x(), dy = d.y();
if (!fullPageMode) { dx += paintPx.left(); dy += paintPx.top(); }
return QPointF(pt_scale * dx, fullH_pt - pt_scale * dy);
};
// Compute, in PDF points on its OWN page, the rectangle to frame for a
// target element (used as a /FitR destination so the link zooms onto it).
auto destRectPdf = [&](Element *tgt) -> QRectF {
Diagram *dg = tgt ? tgt->diagram() : nullptr;
if (!dg) return QRectF();
const QRectF srcT = QRectF(diagramRect(dg, exportProperties()));
if (srcT.width() <= 0.0 || srcT.height() <= 0.0) return QRectF();
const qreal sT = qMin(target.width() / srcT.width(),
target.height() / srcT.height());
QTransform fitT;
fitT.translate(target.x(), target.y());
fitT.scale(sT, sT);
fitT.translate(-srcT.x(), -srcT.y());
QRectF elemScene = tgt->mapRectToScene(tgt->boundingRect());
// Frame the element with a little context, and enforce a minimum
// framed size so tiny contacts don't zoom in extremely.
const qreal pad = 25.0;
elemScene.adjust(-pad, -pad, pad, pad);
const qreal minSide = 160.0;
if (elemScene.width() < minSide)
elemScene.adjust(-(minSide - elemScene.width()) / 2.0, 0,
(minSide - elemScene.width()) / 2.0, 0);
if (elemScene.height() < minSide)
elemScene.adjust(0, -(minSide - elemScene.height()) / 2.0,
0, (minSide - elemScene.height()) / 2.0);
const QRectF devT = fitT.mapRect(elemScene);
const QPointF a = devToPdf(devT.topLeft());
const QPointF b = devToPdf(devT.bottomRight());
return QRectF(QPointF(qMin(a.x(), b.x()), qMin(a.y(), b.y())),
QPointF(qMax(a.x(), b.x()), qMax(a.y(), b.y())));
};
auto injectLink = [&](const QRectF &sceneRect, Element *targetElmt) {
if (!targetElmt || !targetElmt->diagram()) return;
const int targetPage =
diagramPageMap.value(targetElmt->diagram(), -1);
if (targetPage < 1) return;
const QRectF devRect = fit.mapRect(sceneRect);
if (!devRect.isValid() || !pageBounds.intersects(devRect)) return;
QString frag = QString("page=%1").arg(targetPage);
const QRectF d = destRectPdf(targetElmt); // /FitR L_B_R_T
if (d.isValid())
frag += QString("&fitr=%1_%2_%3_%4")
.arg(qRound(d.left())).arg(qRound(d.top()))
.arg(qRound(d.right())).arg(qRound(d.bottom()));
QUrl url = QUrl::fromLocalFile(printer->outputFileName());
url.setFragment(frag);
pdfEngine->drawHyperlink(devRect, url);
};
for (auto *item : diagram->items()) {
// --- CrossRefItem links ---
if (auto *xref = dynamic_cast<CrossRefItem*>(item)) {
for (auto it = xref->hoveredContactsMap().begin();
it != xref->hoveredContactsMap().end(); ++it)
{
Element *targetElmt = it.key();
if (!targetElmt || !targetElmt->diagram()) continue;
// it.value() est en coords LOCALES du CrossRefItem -> scene
injectLink(xref->mapRectToScene(it.value()), targetElmt);
}
continue;
}
// --- Folio report links (DynamicElementTextItem) ---
if (auto *deti = dynamic_cast<DynamicElementTextItem*>(item)) {
Element *parent = deti->parentElement();
if (!parent) continue;
// (a) Report element : label -> linked report on another folio
if (parent->linkType() & Element::AllReport) {
if (parent->linkedElements().isEmpty()) continue;
bool showsLabel =
(deti->textFrom() == DynamicElementTextItem::ElementInfo
&& deti->infoName() == QLatin1String("label")) ||
(deti->textFrom() == DynamicElementTextItem::CompositeText
&& deti->compositeText().contains(QStringLiteral("%{label}")));
if (!showsLabel) continue;
Element *targetElmt = parent->linkedElements().first();
if (!targetElmt || !targetElmt->diagram()) continue;
injectLink(deti->mapRectToScene(deti->boundingRect()), targetElmt);
continue;
}
// (b) Slave element : the "(folio-pos)" text -> master element
if (parent->linkType() == Element::Slave) {
QGraphicsTextItem *sx = deti->slaveXrefItem();
Element *master = deti->masterElement();
if (sx && master && master->diagram()) {
injectLink(sx->mapRectToScene(sx->boundingRect()), master);
}
continue;
}
continue;
}
// --- Slave cross-reference carried by a grouped text ---
if (auto *grp = dynamic_cast<ElementTextItemGroup*>(item)) {
Element *parent = grp->parentElement();
if (!parent || parent->linkType() != Element::Slave) continue;
if (parent->linkedElements().isEmpty()) continue;
QGraphicsTextItem *sx = grp->slaveXrefItem();
if (!sx) continue;
Element *master = parent->linkedElements().first();
if (!master || !master->diagram()) continue;
injectLink(sx->mapRectToScene(sx->boundingRect()), master);
continue;
}
}
}
}
////PDF links end////
////Print is finished, restore diagram and graphics item properties ////Print is finished, restore diagram and graphics item properties
for (auto view : diagram->views()) { for (auto view : diagram->views()) {
view->setInteractive(true); view->setInteractive(true);
@@ -772,9 +1218,29 @@ void ProjectPrintWindow::on_m_uncheck_all_clicked()
void ProjectPrintWindow::print() void ProjectPrintWindow::print()
{ {
m_preview->print(); const bool isPdf = (m_printer->outputFormat() == QPrinter::PdfFormat);
const QString pdfFile = isPdf ? m_printer->outputFileName() : QString();
m_preview->print(); // triggers requestPaint() synchronously; painter
// is created/destroyed inside that call
savePageSetupForCurrentPrinter(); savePageSetupForCurrentPrinter();
if (isPdf && !pdfFile.isEmpty()) {
// Defer post-processing and window close to the next event-loop
// iteration. This lets the macOS arm64 Metal compositor finish
// compositing the backing store before the window is destroyed,
// which prevents the black screen observed on Apple Silicon under
// macOS Sequoia (QPrintPreviewWidget + CALayer timing issue).
QTimer::singleShot(0, this, [this, pdfFile]() {
// Convert URI link annotations into native internal GoTo/FitR
// actions so cross-references jump inside the document.
pdfConvertUriToGoTo(pdfFile);
this->close(); this->close();
});
} else {
this->close();
}
} }
void ProjectPrintWindow::on_m_date_cb_userDateChanged(const QDate &date) void ProjectPrintWindow::on_m_date_cb_userDateChanged(const QDate &date)
+2 -1
View File
@@ -21,6 +21,7 @@
#include "../exportproperties.h" #include "../exportproperties.h"
#include <QMainWindow> #include <QMainWindow>
#include <QMap>
#include <QPrinter> #include <QPrinter>
namespace Ui { namespace Ui {
@@ -79,7 +80,7 @@ class ProjectPrintWindow : public QMainWindow
private: private:
void requestPaint(); void requestPaint();
void printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer); void printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap<Diagram*, int> &diagramPageMap = {});
QRect diagramRect(Diagram *diagram, const ExportProperties &option) const; QRect diagramRect(Diagram *diagram, const ExportProperties &option) const;
int horizontalPagesCount(Diagram *diagram, const ExportProperties &option, bool full_page) const; int horizontalPagesCount(Diagram *diagram, const ExportProperties &option, bool full_page) const;
int verticalPagesCount(Diagram *diagram, const ExportProperties &option, bool full_page) const; int verticalPagesCount(Diagram *diagram, const ExportProperties &option, bool full_page) const;
+17 -1
View File
@@ -49,7 +49,19 @@ bool ElementData::fromXml(const QDomElement &xml_element)
return false; return false;
} }
m_type = typeFromString(xml_element.attribute(QStringLiteral("link_type"), QStringLiteral("simple"))); // --- HIER STARTET UNSER DEBUG-BLOCK ---
// Wir holen den String aus der XML und speichern ihn kurz zwischen
QString raw_type_string = xml_element.attribute(QStringLiteral("link_type"), QStringLiteral("simple"));
qDebug() << "\n=== NEUES BAUTEIL WIRD GELADEN ===";
qDebug() << "[XML Parser] Roher 'link_type' String aus der .elmt Datei:" << raw_type_string;
// Jetzt übergeben wir ihn an deine Übersetzungs-Funktion
m_type = typeFromString(raw_type_string);
qDebug() << "[XML Parser] Übersetzter ElementData-Typ:" << typeToString(m_type);
// --- HIER ENDET UNSER DEBUG-BLOCK ---
kindInfoFromXml(xml_element); kindInfoFromXml(xml_element);
m_informations.fromXml(xml_element.firstChildElement(QStringLiteral("elementInformations")), m_informations.fromXml(xml_element.firstChildElement(QStringLiteral("elementInformations")),
QStringLiteral("elementInformation")); QStringLiteral("elementInformation"));
@@ -323,6 +335,8 @@ QString ElementData::typeToString(ElementData::Type type)
return QStringLiteral("terminal"); return QStringLiteral("terminal");
case ElementData::Thumbnail: case ElementData::Thumbnail:
return QStringLiteral("thumbnail"); return QStringLiteral("thumbnail");
case ElementData::ConductorDefinition:
return QStringLiteral("conductor_definition");
default: default:
qDebug() << "ElementData::typeToString : type don't exist" qDebug() << "ElementData::typeToString : type don't exist"
<< "return failsafe value 'simple'"; << "return failsafe value 'simple'";
@@ -346,6 +360,8 @@ ElementData::Type ElementData::typeFromString(const QString &string)
return ElementData::Terminal; return ElementData::Terminal;
} else if (string == QLatin1String("thumbnail")) { } else if (string == QLatin1String("thumbnail")) {
return ElementData::Thumbnail; return ElementData::Thumbnail;
} else if (string == QLatin1String("conductor_definition")) {
return ElementData::ConductorDefinition;
} }
//Return simple if nothing match //Return simple if nothing match
+2 -1
View File
@@ -41,7 +41,8 @@ class ElementData : public PropertiesInterface
Master = 8, Master = 8,
Slave = 16, Slave = 16,
Terminal = 32, Terminal = 32,
Thumbnail = 64}; Thumbnail = 64,
ConductorDefinition = 128};
Q_ENUM(Type) Q_ENUM(Type)
Q_DECLARE_FLAGS(Types, Type) Q_DECLARE_FLAGS(Types, Type)
+12
View File
@@ -174,6 +174,12 @@ QString TerminalData::typeToString(TerminalData::Type type)
return QString("Inner"); return QString("Inner");
case Outer : case Outer :
return QString("Outer"); return QString("Outer");
case No :
return QString("No");
case Nc :
return QString("Nc");
case Common :
return QString("Common");
} }
return QString("Generic"); return QString("Generic");
} }
@@ -193,6 +199,12 @@ TerminalData::Type TerminalData::typeFromString(const QString &string)
return TerminalData::Inner; return TerminalData::Inner;
} else if (string == "Outer") { } else if (string == "Outer") {
return TerminalData::Outer; return TerminalData::Outer;
} else if (string == "No") {
return TerminalData::No;
} else if (string == "Nc") {
return TerminalData::Nc;
} else if (string == "Common") {
return TerminalData::Common;
} else { } else {
qDebug() << "TerminalData::typeFromString, argument string is invalid" qDebug() << "TerminalData::typeFromString, argument string is invalid"
" failsafe type 'TerminalData::Generic' is returned"; " failsafe type 'TerminalData::Generic' is returned";
+4 -1
View File
@@ -41,7 +41,10 @@ class TerminalData : public PropertiesInterface
enum Type { enum Type {
Generic, Generic,
Inner, Inner,
Outer Outer,
No, ///< Normally Open terminal (for SW contacts)
Nc, ///< Normally Closed terminal (for SW contacts)
Common ///< Common terminal (for SW contacts)
}; };
Q_ENUM(Type) Q_ENUM(Type)
+6
View File
@@ -29,6 +29,7 @@
XRefProperties::XRefProperties() XRefProperties::XRefProperties()
{ {
m_show_power_ctc = true; m_show_power_ctc = true;
m_show_terminal_name = true;
m_display = Cross; m_display = Cross;
m_snap_to = Bottom; m_snap_to = Bottom;
m_prefix_keys << "power" << "delay" << "switch"; m_prefix_keys << "power" << "delay" << "switch";
@@ -48,6 +49,7 @@ void XRefProperties::toSettings(QSettings &settings,
const QString prefix) const const QString prefix) const
{ {
settings.setValue(prefix % "showpowerctc", m_show_power_ctc); settings.setValue(prefix % "showpowerctc", m_show_power_ctc);
settings.setValue(prefix % "showterminalname", m_show_terminal_name);
QString display = m_display == Cross? "cross" : "contacts"; QString display = m_display == Cross? "cross" : "contacts";
settings.setValue(prefix % "displayhas", display); settings.setValue(prefix % "displayhas", display);
QString snap = m_snap_to == Bottom? "bottom" : "label"; QString snap = m_snap_to == Bottom? "bottom" : "label";
@@ -78,6 +80,7 @@ void XRefProperties::fromSettings(const QSettings &settings,
const QString prefix) const QString prefix)
{ {
m_show_power_ctc = settings.value(prefix % "showpowerctc", true).toBool(); m_show_power_ctc = settings.value(prefix % "showpowerctc", true).toBool();
m_show_terminal_name = settings.value(prefix % "showterminalname", true).toBool();
QString display = settings.value(prefix % "displayhas", "cross").toString(); QString display = settings.value(prefix % "displayhas", "cross").toString();
display == "cross"? m_display = Cross : m_display = Contacts; display == "cross"? m_display = Cross : m_display = Contacts;
QString snap = settings.value(prefix % "snapto", "label").toString(); QString snap = settings.value(prefix % "snapto", "label").toString();
@@ -107,6 +110,7 @@ QDomElement XRefProperties::toXml(QDomDocument &xml_document) const
xml_element.setAttribute("type", m_key); xml_element.setAttribute("type", m_key);
xml_element.setAttribute("showpowerctc", m_show_power_ctc? "true" : "false"); xml_element.setAttribute("showpowerctc", m_show_power_ctc? "true" : "false");
xml_element.setAttribute("showterminalname", m_show_terminal_name? "true" : "false");
QString display = m_display == Cross? "cross" : "contacts"; QString display = m_display == Cross? "cross" : "contacts";
xml_element.setAttribute("displayhas", display); xml_element.setAttribute("displayhas", display);
QString snap = m_snap_to == Bottom? "bottom" : "label"; QString snap = m_snap_to == Bottom? "bottom" : "label";
@@ -137,6 +141,7 @@ QDomElement XRefProperties::toXml(QDomDocument &xml_document) const
*/ */
bool XRefProperties::fromXml(const QDomElement &xml_element) { bool XRefProperties::fromXml(const QDomElement &xml_element) {
m_show_power_ctc = xml_element.attribute("showpowerctc") == "true"; m_show_power_ctc = xml_element.attribute("showpowerctc") == "true";
m_show_terminal_name = xml_element.attribute("showterminalname", "true") == "true";
QString display = xml_element.attribute("displayhas", "cross"); QString display = xml_element.attribute("displayhas", "cross");
display == "cross"? m_display = Cross : m_display = Contacts; display == "cross"? m_display = Cross : m_display = Contacts;
QString snap = xml_element.attribute("snapto", "label"); QString snap = xml_element.attribute("snapto", "label");
@@ -188,6 +193,7 @@ QHash<QString, XRefProperties> XRefProperties::defaultProperties()
bool XRefProperties::operator ==(const XRefProperties &xrp) const{ bool XRefProperties::operator ==(const XRefProperties &xrp) const{
return (m_show_power_ctc == xrp.m_show_power_ctc return (m_show_power_ctc == xrp.m_show_power_ctc
&& m_show_terminal_name == xrp.m_show_terminal_name
&& m_display == xrp.m_display && m_display == xrp.m_display
&& m_snap_to == xrp.m_snap_to && m_snap_to == xrp.m_snap_to
&& m_prefix == xrp.m_prefix && m_prefix == xrp.m_prefix
+4
View File
@@ -57,6 +57,9 @@ class XRefProperties : public PropertiesInterface
void setShowPowerContac (const bool a) {m_show_power_ctc = a;} void setShowPowerContac (const bool a) {m_show_power_ctc = a;}
bool showPowerContact () const {return m_show_power_ctc;} bool showPowerContact () const {return m_show_power_ctc;}
void setShowTerminalName (const bool a) {m_show_terminal_name = a;}
bool showTerminalName () const {return m_show_terminal_name;}
void setDisplayHas (const DisplayHas dh) {m_display = dh;} void setDisplayHas (const DisplayHas dh) {m_display = dh;}
DisplayHas displayHas () const {return m_display;} DisplayHas displayHas () const {return m_display;}
@@ -81,6 +84,7 @@ class XRefProperties : public PropertiesInterface
private: private:
bool m_show_power_ctc; bool m_show_power_ctc;
bool m_show_terminal_name;
DisplayHas m_display; DisplayHas m_display;
SnapTo m_snap_to; SnapTo m_snap_to;
Qt::AlignmentFlag m_xref_pos; Qt::AlignmentFlag m_xref_pos;
+400 -95
View File
@@ -17,6 +17,8 @@
*/ */
#include "crossrefitem.h" #include "crossrefitem.h"
#include <QTimer>
#include "../autoNum/assignvariables.h" #include "../autoNum/assignvariables.h"
#include "../diagram.h" #include "../diagram.h"
#include "../diagramposition.h" #include "../diagramposition.h"
@@ -25,6 +27,7 @@
#include "element.h" #include "element.h"
#include "elementtextitemgroup.h" #include "elementtextitemgroup.h"
#include "qgraphicsitemutility.h" #include "qgraphicsitemutility.h"
#include "terminal.h"
//define the height of the header. //define the height of the header.
static int header = 5; static int header = 5;
@@ -221,9 +224,12 @@ void CrossRefItem::updateLabel()
prepareGeometryChange(); prepareGeometryChange();
m_bounding_rect = QRectF(); m_bounding_rect = QRectF();
//init the painter // Build geometry and m_hovered_contacts_map using a QImage-backed
QPainter qp; // painter so font metrics match the screen painter in paint().
qp.begin(&m_drawing); // m_update_map=true allows the draw functions to populate the map;
// paint() calls them with m_update_map=false so the map is stable.
QImage dummy(1, 1, QImage::Format_ARGB32_Premultiplied);
QPainter qp(&dummy);
QPen pen_; QPen pen_;
pen_.setWidthF(0.5); pen_.setWidthF(0.5);
qp.setPen(pen_); qp.setPen(pen_);
@@ -232,17 +238,21 @@ void CrossRefItem::updateLabel()
//Draw cross or contact, only if master element is linked. //Draw cross or contact, only if master element is linked.
if (! m_element->linkedElements().isEmpty()) if (! m_element->linkedElements().isEmpty())
{ {
m_update_map = true;
XRefProperties::DisplayHas dh = m_properties.displayHas(); XRefProperties::DisplayHas dh = m_properties.displayHas();
if (dh == XRefProperties::Cross) if (dh == XRefProperties::Cross)
drawAsCross(qp); drawAsCross(qp);
else if (dh == XRefProperties::Contacts) else if (dh == XRefProperties::Contacts)
drawAsContacts(qp); drawAsContacts(qp);
m_update_map = false;
} }
qp.end();
autoPos(); autoPos();
update(); update();
// Schedule a second update after the scene has finished laying out,
// so the initial render uses the correct bounding rect.
QTimer::singleShot(0, this, [this]{ update(); });
} }
/** /**
@@ -310,7 +320,26 @@ void CrossRefItem::paint(
{ {
Q_UNUSED(option) Q_UNUSED(option)
Q_UNUSED(widget) Q_UNUSED(widget)
m_drawing.play(painter); // Draw directly — no QPicture involved anywhere.
// QPicture::play() + nested drawPicture() (m_hdr_no_ctc/m_hdr_nc_ctc)
// caused a use-after-free crash (QRegion::begin, Qt5Gui+0x49af60)
// confirmed by analysis of 19+ coredumps.
// m_update_map=false: draw functions do not overwrite m_hovered_contacts_map.
if (m_element->linkedElements().isEmpty()) return;
QPen pen_;
pen_.setWidthF(0.5);
painter->save();
painter->setPen(pen_);
painter->setFont(QETApp::diagramTextsFont(5));
m_update_map = false;
XRefProperties::DisplayHas dh = m_properties.displayHas();
if (dh == XRefProperties::Cross)
drawAsCross(*painter);
else if (dh == XRefProperties::Contacts)
drawAsContacts(*painter);
painter->restore();
} }
/** /**
@@ -320,7 +349,24 @@ void CrossRefItem::paint(
void CrossRefItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) void CrossRefItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{ {
event->accept(); event->accept();
QetGraphicsItem::showItem(m_hovered_contact);
// Find the element under the click position directly from the map,
// rather than relying on m_hovered_contact which may have been reset
// by hoverMoveEvent between the two clicks of the double-click.
QPointF pos = event->pos();
Element *target = m_hovered_contact;
if (!target) {
for (auto it = m_hovered_contacts_map.begin();
it != m_hovered_contacts_map.end(); ++it) {
if (it.value().contains(pos)) {
target = it.key();
break;
}
}
}
QetGraphicsItem::showItem(target);
} }
/** /**
@@ -432,49 +478,41 @@ void CrossRefItem::linkedChanged()
/** /**
@brief CrossRefItem::buildHeaderContact @brief CrossRefItem::buildHeaderContact
Draw the QPicture of m_hdr_no_ctc and m_hdr_nc_ctc Draw NO and NC contact symbols directly onto painter.
Previously used QPicture (m_hdr_no_ctc/m_hdr_nc_ctc) which caused
use-after-free crashes via nested QPicture::play() calls.
*/ */
void CrossRefItem::buildHeaderContact() void CrossRefItem::buildHeaderContact(QPainter &painter, QPointF no_pos, QPointF nc_pos)
{ {
if (!m_hdr_no_ctc.isNull() && !m_hdr_nc_ctc.isNull()) return;
//init the painter
QPainter qp;
QPen pen_; QPen pen_;
pen_.setWidthF(0.2); pen_.setWidthF(0.2);
painter.save();
painter.setPen(pen_);
//draw the NO contact //draw the NO contact header symbol
if (m_hdr_no_ctc.isNull()) { painter.drawLine(no_pos.x()+0, no_pos.y()+3, no_pos.x()+5, no_pos.y()+3);
qp.begin(&m_hdr_no_ctc);
qp.setPen(pen_);
qp.drawLine(0, 3, 5, 3);
QPointF p1[3] = { QPointF p1[3] = {
QPointF(5, 0), QPointF(no_pos.x()+5, no_pos.y()+0),
QPointF(10, 3), QPointF(no_pos.x()+10, no_pos.y()+3),
QPointF(15, 3), QPointF(no_pos.x()+15, no_pos.y()+3),
}; };
qp.drawPolyline(p1,3); painter.drawPolyline(p1, 3);
qp.end();
}
//draw the NC contact //draw the NC contact header symbol
if (m_hdr_nc_ctc.isNull()) {
qp.begin(&m_hdr_nc_ctc);
qp.setPen(pen_);
QPointF p2[3] = { QPointF p2[3] = {
QPointF(0, 3), QPointF(nc_pos.x()+0, nc_pos.y()+3),
QPointF(5, 3), QPointF(nc_pos.x()+5, nc_pos.y()+3),
QPointF(5, 0) QPointF(nc_pos.x()+5, nc_pos.y()+0)
}; };
qp.drawPolyline(p2,3); painter.drawPolyline(p2, 3);
QPointF p3[3] = { QPointF p3[3] = {
QPointF(4, 0), QPointF(nc_pos.x()+4, nc_pos.y()+0),
QPointF(10, 3), QPointF(nc_pos.x()+10, nc_pos.y()+3),
QPointF(15, 3), QPointF(nc_pos.x()+15, nc_pos.y()+3),
}; };
qp.drawPolyline(p3,3); painter.drawPolyline(p3, 3);
qp.end();
} painter.restore();
} }
/** /**
@@ -492,11 +530,95 @@ void CrossRefItem::setUpCrossBoundingRect(QPainter &painter)
QStringList no_str, nc_str; QStringList no_str, nc_str;
// Helper lambda: build "[13-14] pos" string for an element.
// For power contacts (e.g. 3-pole contactor), all named terminals
// are collected (e.g. "[1-2-3-4-5-6] pos").
// For other contacts (NO/NC/SW), only the first 2 named terminals
// are used — users are expected to name and order their terminals
// in the element editor.
// Helper lambda: build "[13-14] pos" for an element.
// - Power: all terminals sorted numerically → "[1-2-3-4-5-6] pos"
// - SW with typed terminals: show relevant pair per column (handled below)
// - Others: first 2 named terminals
auto buildLabel = [this](Element *elmt, bool is_no_col) -> QString {
const bool is_power =
elmt->kindInformations()["type"].toString() == "power";
const bool is_sw =
elmt->kindInformations()["state"].toString() == "SW";
QStringList tnames;
if (is_sw) {
// Check if terminals have explicit No/Nc/Common types
bool has_typed = false;
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
if (t->terminalType() == TerminalData::No ||
t->terminalType() == TerminalData::Nc ||
t->terminalType() == TerminalData::Common) {
has_typed = true; break;
}
}
if (has_typed) {
QString no_name, nc_name, common_name;
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
if (!t->name().isEmpty()) {
if (t->terminalType() == TerminalData::No) no_name = t->name();
else if (t->terminalType() == TerminalData::Nc) nc_name = t->name();
else if (t->terminalType() == TerminalData::Common) common_name = t->name();
}
}
// NO column: show NO+Common pair; NC column: show NC+Common pair
if (is_no_col)
tnames << no_name << common_name;
else
tnames << nc_name << common_name;
} else {
// Fallback: first 2 named terminals
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
const QString tn = t->name();
if (!tn.isEmpty()) { tnames << tn; if (tnames.size() >= 2) break; }
}
}
} else {
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
const QString tn = t->name();
if (!tn.isEmpty()) {
tnames << tn;
if (!is_power && tnames.size() >= 2) break;
}
}
if (is_power) {
std::sort(tnames.begin(), tnames.end(),
[](const QString &a, const QString &b){
int i_a = a.size();
while (i_a > 0 && a[i_a-1].isDigit()) --i_a;
int i_b = b.size();
while (i_b > 0 && b[i_b-1].isDigit()) --i_b;
bool a_ok = false, b_ok = false;
int ai = a.mid(i_a).toInt(&a_ok);
int bi = b.mid(i_b).toInt(&b_ok);
if (a_ok && b_ok && a.left(i_a) == b.left(i_b))
return ai < bi;
return a < b;
});
}
}
QString pos = elementPositionText(elmt, true);
if (!tnames.isEmpty() && m_properties.showTerminalName())
return QStringLiteral("[") + tnames.join("-") + QStringLiteral("] ") + pos;
return pos;
};
for (auto elmt : NOElements()) { for (auto elmt : NOElements()) {
no_str.append(elementPositionText(elmt, true)); no_str.append(buildLabel(elmt, true));
} }
for (auto elmt : NCElements()) { for (auto elmt : NCElements()) {
nc_str.append(elementPositionText(elmt, true)); nc_str.append(buildLabel(elmt, false));
} }
//There is no string to display, we return now //There is no string to display, we return now
@@ -509,9 +631,10 @@ void CrossRefItem::setUpCrossBoundingRect(QPainter &painter)
QRectF no_bounding; QRectF no_bounding;
for (auto str : no_str) for (auto str : no_str)
{ {
QRectF bounding = painter.boundingRect(QRectF (), Qt::AlignCenter, str); QRectF bounding = painter.boundingRect(QRectF(0, 0, 500, 20), Qt::AlignLeft, str);
no_bounding = no_bounding.united(bounding);
no_bounding.setHeight(no_bounding.height() + bounding.height()); no_bounding.setHeight(no_bounding.height() + bounding.height());
if (bounding.width() > no_bounding.width())
no_bounding.setWidth(bounding.width());
} }
//Adjust according to the NO //Adjust according to the NO
if (no_bounding.height() > default_bounding.height() - header) if (no_bounding.height() > default_bounding.height() - header)
@@ -523,9 +646,10 @@ void CrossRefItem::setUpCrossBoundingRect(QPainter &painter)
QRectF nc_bounding; QRectF nc_bounding;
for (auto str : nc_str) for (auto str : nc_str)
{ {
QRectF bounding = painter.boundingRect(QRectF (), Qt::AlignCenter, str); QRectF bounding = painter.boundingRect(QRectF(0, 0, 500, 20), Qt::AlignLeft, str);
nc_bounding = nc_bounding.united(bounding);
nc_bounding.setHeight(nc_bounding.height() + bounding.height()); nc_bounding.setHeight(nc_bounding.height() + bounding.height());
if (bounding.width() > nc_bounding.width())
nc_bounding.setWidth(bounding.width());
} }
//Adjust according to the NC //Adjust according to the NC
if (nc_bounding.height() > default_bounding.height() - header) if (nc_bounding.height() > default_bounding.height() - header)
@@ -549,7 +673,8 @@ void CrossRefItem::drawAsCross(QPainter &painter)
{ {
//calculate the size of the cross //calculate the size of the cross
setUpCrossBoundingRect(painter); setUpCrossBoundingRect(painter);
m_hovered_contacts_map.clear(); m_drawed_contacts = 0;
if (m_update_map) m_hovered_contacts_map.clear();
//Bounding rect is empty that mean there's no contact to draw //Bounding rect is empty that mean there's no contact to draw
if (boundingRect().isEmpty()) return; if (boundingRect().isEmpty()) return;
@@ -559,12 +684,11 @@ void CrossRefItem::drawAsCross(QPainter &painter)
painter.drawLine(br.width()/2, 0, br.width()/2, br.height()); //vertical line painter.drawLine(br.width()/2, 0, br.width()/2, br.height()); //vertical line
painter.drawLine(0, header, br.width(), header); //horizontal line painter.drawLine(0, header, br.width(), header); //horizontal line
//Add the symbolic contacts //Add the symbolic contacts (drawn directly, no QPicture)
buildHeaderContact(); static const qreal hdr_symbol_width = 15.0;
QPointF p((m_bounding_rect.width()/4) - (m_hdr_no_ctc.width()/2), 0); QPointF no_pos((m_bounding_rect.width()/4) - (hdr_symbol_width/2), 0);
painter.drawPicture (p, m_hdr_no_ctc); QPointF nc_pos((m_bounding_rect.width() * 3/4) - (hdr_symbol_width/2), 0);
p.setX((m_bounding_rect.width() * 3/4) - (m_hdr_nc_ctc.width()/2)); buildHeaderContact(painter, no_pos, nc_pos);
painter.drawPicture (p, m_hdr_nc_ctc);
//and fill it //and fill it
fillCrossRef(painter); fillCrossRef(painter);
@@ -581,7 +705,7 @@ void CrossRefItem::drawAsContacts(QPainter &painter)
return; return;
m_drawed_contacts = 0; m_drawed_contacts = 0;
m_hovered_contacts_map.clear(); if (m_update_map) m_hovered_contacts_map.clear();
QRectF bounding_rect; QRectF bounding_rect;
//Draw each linked contact //Draw each linked contact
@@ -605,7 +729,7 @@ void CrossRefItem::drawAsContacts(QPainter &painter)
else if (type == "delayOff") option += DelayOff; else if (type == "delayOff") option += DelayOff;
else if (type == "delayOnOff") option += DelayOnOff; else if (type == "delayOnOff") option += DelayOnOff;
QRectF br = drawContact(painter, option, elmt); QRectF br = drawContact(painter, option, elmt, i);
bounding_rect = bounding_rect.united(br); bounding_rect = bounding_rect.united(br);
} }
} }
@@ -624,9 +748,81 @@ void CrossRefItem::drawAsContacts(QPainter &painter)
@param elmt : the element to display text (the position of the contact) @param elmt : the element to display text (the position of the contact)
@return The bounding rect of the draw (contact + text) @return The bounding rect of the draw (contact + text)
*/ */
QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt) QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, int pole_index)
{ {
QString str = elementPositionText(elmt); QString str = elementPositionText(elmt);
// Collect terminal names from the element definition (.elmt)
// e.g. name="13" and name="14" on each terminal
// For power contacts, sort numerically and pick the pair for pole_index.
// For SW contacts with typed terminals (No/Nc/Common), filter by role.
QStringList terminal_names;
const bool is_power_ctc =
elmt->kindInformations()["type"].toString() == "power";
const bool is_sw = (flags & SW) && !(flags & NOC);
// Check if SW terminals have explicit No/Nc/Common types
bool sw_has_typed_terminals = false;
if (is_sw) {
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
if (t->terminalType() == TerminalData::No ||
t->terminalType() == TerminalData::Nc ||
t->terminalType() == TerminalData::Common) {
sw_has_typed_terminals = true;
break;
}
}
}
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
const QString tname = t->name();
if (!tname.isEmpty())
terminal_names << tname;
}
if (is_power_ctc) {
// Sort terminals alphanumerically so names like "R1","R2"... or "1","2"...
// are ordered correctly. Extract trailing digits for numeric comparison;
// fall back to full string comparison when no digits are found.
std::sort(terminal_names.begin(), terminal_names.end(),
[](const QString &a, const QString &b){
// Extract trailing numeric part
int i_a = a.size();
while (i_a > 0 && a[i_a-1].isDigit()) --i_a;
int i_b = b.size();
while (i_b > 0 && b[i_b-1].isDigit()) --i_b;
bool a_ok = false, b_ok = false;
int ai = a.mid(i_a).toInt(&a_ok);
int bi = b.mid(i_b).toInt(&b_ok);
if (a_ok && b_ok && a.left(i_a) == b.left(i_b))
return ai < bi;
return a < b;
});
// Pick the pair for this pole: pole 0 → [0,1], pole 1 → [2,3], etc.
int idx = pole_index * 2;
if (idx + 1 < terminal_names.size())
terminal_names = QStringList() << terminal_names[idx] << terminal_names[idx+1];
else
terminal_names.clear();
} else if (is_sw && sw_has_typed_terminals) {
// Build [NO_name, Common_name, NC_name] from typed terminals
QString no_name, nc_name, common_name;
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
if (!t->name().isEmpty()) {
if (t->terminalType() == TerminalData::No) no_name = t->name();
else if (t->terminalType() == TerminalData::Nc) nc_name = t->name();
else if (t->terminalType() == TerminalData::Common) common_name = t->name();
}
}
// drawText expects: [0]=NC, [1]=NO, [2]=Common
// (drawText uses [1] for NO top-left, [0] for NC bottom-left, [2] for Common right)
terminal_names.clear();
terminal_names << nc_name << no_name << common_name;
}
int offset = m_drawed_contacts*10; int offset = m_drawed_contacts*10;
QRectF bounding_rect = QRectF(0, offset, 24, 10); QRectF bounding_rect = QRectF(0, offset, 24, 10);
@@ -643,15 +839,17 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt)
painter.drawLine(0, offset+6, 8, offset+6); painter.drawLine(0, offset+6, 8, offset+6);
painter.drawLine(16, offset+6, 24, offset+6); painter.drawLine(16, offset+6, 24, offset+6);
///take example of this code for display the terminal text // Draw terminal names on each side of the contact symbol
/*QFont font = QETApp::diagramTextsFont(4); // terminal_names[0] on the left, terminal_names[1] on the right
font.setBold(true); if (!terminal_names.isEmpty() && m_properties.showTerminalName()) {
painter.setFont(font); painter.setFont(QETApp::diagramTextsFont(4));
QRectF bt(0, offset, 24, 10); QRectF bt(0, offset, 24, 10);
int txt = 10 + m_drawed_contacts; if (terminal_names.size() >= 1)
painter.drawText(bt, Qt::AlignLeft|Qt::AlignTop, QString::number(txt)); painter.drawText(bt, Qt::AlignLeft|Qt::AlignTop, terminal_names[0]);
painter.drawText(bt, Qt::AlignRight|Qt::AlignTop, QString::number(txt)); if (terminal_names.size() >= 2)
painter.setFont(QETApp::diagramTextsFont(5));*/ painter.drawText(bt, Qt::AlignRight|Qt::AlignTop, terminal_names[1]);
painter.setFont(QETApp::diagramTextsFont(5));
}
//draw open contact //draw open contact
if (flags &NO) { if (flags &NO) {
@@ -729,14 +927,8 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt)
painter.drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, str); painter.drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, str);
bounding_rect = bounding_rect.united(text_rect); bounding_rect = bounding_rect.united(text_rect);
if (m_hovered_contacts_map.contains(elmt)) if (m_update_map)
{ m_hovered_contacts_map.insert(elmt, text_rect);
m_hovered_contacts_map.insert(elmt, bounding_rect);
}
else
{
m_hovered_contacts_map.insert(elmt, bounding_rect);
}
++m_drawed_contacts; ++m_drawed_contacts;
} }
@@ -768,6 +960,25 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt)
}; };
painter.drawPolyline(p2, 3); painter.drawPolyline(p2, 3);
// Draw terminal names for switch contact (3 terminals)
// terminal_names[0] = NC (bottom-left)
// terminal_names[1] = NO (top-left)
// terminal_names[2] = Common (right)
if (!terminal_names.isEmpty() && m_properties.showTerminalName()) {
painter.setFont(QETApp::diagramTextsFont(4));
// Storage order set above: [0]=NC, [1]=NO, [2]=Common
if (terminal_names.size() >= 2)
painter.drawText(QRectF(0, offset, 8, 8),
Qt::AlignLeft|Qt::AlignTop, terminal_names[1]); // NO top-left
if (terminal_names.size() >= 3)
painter.drawText(QRectF(16, offset+4, 8, 6),
Qt::AlignRight|Qt::AlignTop, terminal_names[2]); // Common right
if (terminal_names.size() >= 1)
painter.drawText(QRectF(0, offset+9, 8, 6),
Qt::AlignLeft|Qt::AlignTop, terminal_names[0]); // NC bottom-left
painter.setFont(QETApp::diagramTextsFont(5));
}
//Draw the half ellipse off delay //Draw the half ellipse off delay
if (flags &Delay) if (flags &Delay)
{ {
@@ -799,12 +1010,8 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt)
str); str);
bounding_rect = bounding_rect.united(text_rect); bounding_rect = bounding_rect.united(text_rect);
if (m_hovered_contacts_map.contains(elmt)) { if (m_update_map)
m_hovered_contacts_map.insert(elmt, bounding_rect); m_hovered_contacts_map.insert(elmt, text_rect);
}
else {
m_hovered_contacts_map.insert(elmt, bounding_rect);
}
//a switch contact take place of two normal contact //a switch contact take place of two normal contact
m_drawed_contacts += 2; m_drawed_contacts += 2;
@@ -835,12 +1042,8 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt)
str); str);
bounding_rect = bounding_rect.united(text_rect); bounding_rect = bounding_rect.united(text_rect);
if (m_hovered_contacts_map.contains(elmt)) { if (m_update_map)
m_hovered_contacts_map.insert(elmt, bounding_rect); m_hovered_contacts_map.insert(elmt, text_rect);
}
else {
m_hovered_contacts_map.insert(elmt, bounding_rect);
}
++m_drawed_contacts; ++m_drawed_contacts;
} }
return bounding_rect; return bounding_rect;
@@ -866,7 +1069,60 @@ void CrossRefItem::fillCrossRef(QPainter &painter)
m_hovered_contact == elmt ? pen.setColor(Qt::blue) :pen.setColor(Qt::black); m_hovered_contact == elmt ? pen.setColor(Qt::blue) :pen.setColor(Qt::black);
painter.setPen(pen); painter.setPen(pen);
// Collect terminal names for NO column.
// Power: all terminals sorted numerically.
// SW with typed terminals: NO+Common pair.
// Others: first 2 named terminals.
const bool is_power_no =
elmt->kindInformations()["type"].toString() == "power";
const bool is_sw_no =
elmt->kindInformations()["state"].toString() == "SW";
QStringList tnames;
if (is_sw_no) {
bool has_typed = false;
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
if (t->terminalType() == TerminalData::No ||
t->terminalType() == TerminalData::Nc ||
t->terminalType() == TerminalData::Common) {
has_typed = true; break;
}
}
if (has_typed) {
QString no_name, common_name;
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
if (!t->name().isEmpty()) {
if (t->terminalType() == TerminalData::No) no_name = t->name();
else if (t->terminalType() == TerminalData::Common) common_name = t->name();
}
}
tnames << no_name << common_name;
} else {
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
const QString tn = t->name();
if (!tn.isEmpty()) { tnames << tn; if (tnames.size() >= 2) break; }
}
}
} else {
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
const QString tn = t->name();
if (!tn.isEmpty()) {
tnames << tn;
if (!is_power_no && tnames.size() >= 2) break;
}
}
}
QString terminal_label;
if (!tnames.isEmpty() && m_properties.showTerminalName())
terminal_label = QStringLiteral("[") + tnames.join("-") + QStringLiteral("]");
QString str = elementPositionText(elmt, true); QString str = elementPositionText(elmt, true);
if (!terminal_label.isEmpty())
str = terminal_label + QStringLiteral(" ") + str;
QRectF bounding = painter.boundingRect( QRectF bounding = painter.boundingRect(
QRectF(no_top_left, QRectF(no_top_left,
QSize(middle_cross, 1)), QSize(middle_cross, 1)),
@@ -874,13 +1130,11 @@ void CrossRefItem::fillCrossRef(QPainter &painter)
str); str);
painter.drawText(bounding, Qt::AlignLeft, str); painter.drawText(bounding, Qt::AlignLeft, str);
if (m_hovered_contacts_map.contains(elmt)) if (m_update_map) {
{ QString pos_str = elementPositionText(elmt, true);
m_hovered_contacts_map.insert(elmt, bounding); QRectF pos_rect = painter.boundingRect(bounding, Qt::AlignRight, pos_str);
} pos_rect.adjust(-2, -1, 2, 1); // extend hit area slightly
else m_hovered_contacts_map.insert(elmt, pos_rect);
{
m_hovered_contacts_map.insert(elmt, bounding);
} }
no_top_left.ry() += bounding.height(); no_top_left.ry() += bounding.height();
@@ -895,7 +1149,60 @@ void CrossRefItem::fillCrossRef(QPainter &painter)
:pen.setColor(Qt::black); :pen.setColor(Qt::black);
painter.setPen(pen); painter.setPen(pen);
// Collect terminal names for NC column.
// Power: all terminals sorted numerically.
// SW with typed terminals: NC+Common pair.
// Others: first 2 named terminals.
const bool is_power_nc =
elmt->kindInformations()["type"].toString() == "power";
const bool is_sw_nc =
elmt->kindInformations()["state"].toString() == "SW";
QStringList tnames_nc;
if (is_sw_nc) {
bool has_typed = false;
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
if (t->terminalType() == TerminalData::No ||
t->terminalType() == TerminalData::Nc ||
t->terminalType() == TerminalData::Common) {
has_typed = true; break;
}
}
if (has_typed) {
QString nc_name, common_name;
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
if (!t->name().isEmpty()) {
if (t->terminalType() == TerminalData::Nc) nc_name = t->name();
else if (t->terminalType() == TerminalData::Common) common_name = t->name();
}
}
tnames_nc << nc_name << common_name;
} else {
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
const QString tn = t->name();
if (!tn.isEmpty()) { tnames_nc << tn; if (tnames_nc.size() >= 2) break; }
}
}
} else {
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
const QString tn = t->name();
if (!tn.isEmpty()) {
tnames_nc << tn;
if (!is_power_nc && tnames_nc.size() >= 2) break;
}
}
}
QString terminal_label;
if (!tnames_nc.isEmpty() && m_properties.showTerminalName())
terminal_label = QStringLiteral("[") + tnames_nc.join("-") + QStringLiteral("]");
QString str = elementPositionText(elmt, true); QString str = elementPositionText(elmt, true);
if (!terminal_label.isEmpty())
str = terminal_label + QStringLiteral(" ") + str;
QRectF bounding = painter.boundingRect( QRectF bounding = painter.boundingRect(
QRectF(nc_top_left, QRectF(nc_top_left,
QSize(middle_cross, 1)), QSize(middle_cross, 1)),
@@ -903,13 +1210,11 @@ void CrossRefItem::fillCrossRef(QPainter &painter)
str); str);
painter.drawText(bounding, Qt::AlignRight, str); painter.drawText(bounding, Qt::AlignRight, str);
if (m_hovered_contacts_map.contains(elmt)) if (m_update_map) {
{ QString pos_str = elementPositionText(elmt, true);
m_hovered_contacts_map.insert(elmt, bounding); QRectF pos_rect = painter.boundingRect(bounding, Qt::AlignRight, pos_str);
} pos_rect.adjust(-2, -1, 2, 1); // extend hit area slightly
else m_hovered_contacts_map.insert(elmt, pos_rect);
{
m_hovered_contacts_map.insert(elmt, bounding);
} }
nc_top_left.ry() += bounding.height(); nc_top_left.ry() += bounding.height();
+9 -4
View File
@@ -22,7 +22,6 @@
#include <QGraphicsObject> #include <QGraphicsObject>
#include <QMultiMap> #include <QMultiMap>
#include <QPicture>
class Element; class Element;
class DynamicElementTextItem; class DynamicElementTextItem;
@@ -103,11 +102,11 @@ class CrossRefItem : public QGraphicsObject
private: private:
void linkedChanged(); void linkedChanged();
void buildHeaderContact(); void buildHeaderContact(QPainter &painter, QPointF no_pos, QPointF nc_pos);
void setUpCrossBoundingRect(QPainter &painter); void setUpCrossBoundingRect(QPainter &painter);
void drawAsCross(QPainter &painter); void drawAsCross(QPainter &painter);
void drawAsContacts(QPainter &painter); void drawAsContacts(QPainter &painter);
QRectF drawContact(QPainter &painter, int flags, Element *elmt); QRectF drawContact(QPainter &painter, int flags, Element *elmt, int pole_index = 0);
void fillCrossRef(QPainter &painter); void fillCrossRef(QPainter &painter);
void AddExtraInfo(QPainter &painter, const QString&); void AddExtraInfo(QPainter &painter, const QString&);
QList<Element *> NOElements() const; QList<Element *> NOElements() const;
@@ -117,16 +116,22 @@ class CrossRefItem : public QGraphicsObject
private: private:
Element *m_element; //element to display the cross reference Element *m_element; //element to display the cross reference
QRectF m_bounding_rect; QRectF m_bounding_rect;
QPicture m_drawing, m_hdr_no_ctc, m_hdr_nc_ctc;
QPainterPath m_shape_path; QPainterPath m_shape_path;
XRefProperties m_properties; XRefProperties m_properties;
int m_drawed_contacts; int m_drawed_contacts;
bool m_update_map = false;
QMultiMap <Element *, QRectF> m_hovered_contacts_map; QMultiMap <Element *, QRectF> m_hovered_contacts_map;
Element *m_hovered_contact = nullptr; Element *m_hovered_contact = nullptr;
DynamicElementTextItem *m_text = nullptr; DynamicElementTextItem *m_text = nullptr;
ElementTextItemGroup *m_group = nullptr; ElementTextItemGroup *m_group = nullptr;
QList <QMetaObject::Connection> m_slave_connection; QList <QMetaObject::Connection> m_slave_connection;
QList <QMetaObject::Connection> m_update_connection; QList <QMetaObject::Connection> m_update_connection;
public:
/// Returns the map of linked elements and their clickable rects (local coords).
/// Used by the PDF export to inject hyperlink annotations.
const QMultiMap<Element *, QRectF> &hoveredContactsMap() const
{ return m_hovered_contacts_map; }
}; };
#endif // CROSSREFITEM_H #endif // CROSSREFITEM_H
@@ -26,7 +26,7 @@
#include "crossrefitem.h" #include "crossrefitem.h"
#include "element.h" #include "element.h"
#include "elementtextitemgroup.h" #include "elementtextitemgroup.h"
#include <QTimer>
#include <QDomDocument> #include <QDomDocument>
#include <QDomElement> #include <QDomElement>
#include <QGraphicsSceneMouseEvent> #include <QGraphicsSceneMouseEvent>
@@ -302,7 +302,8 @@ void DynamicElementTextItem::refreshLabelConnection()
if ((m_text_from == ElementInfo && m_info_name == "label") || if ((m_text_from == ElementInfo && m_info_name == "label") ||
(m_text_from == CompositeText && m_composite_text.contains("%{label}"))) (m_text_from == CompositeText && m_composite_text.contains("%{label}")))
{ {
if(m_parent_element.data()->linkType() & Element::AllReport) if((m_parent_element.data()->linkType() & Element::AllReport) ||
m_parent_element.data()->linkType() == Element::ConductorDefinition)
{ {
updateReportFormulaConnection(); updateReportFormulaConnection();
updateReportText(); updateReportText();
@@ -418,7 +419,8 @@ void DynamicElementTextItem::setInfoName(const QString &info_name)
updateXref(); updateXref();
} }
if (m_parent_element && (m_parent_element.data()->linkType() & Element::AllReport)) //special treatment for report if (m_parent_element && ((m_parent_element.data()->linkType() & Element::AllReport) ||
m_parent_element.data()->linkType() == Element::ConductorDefinition)) //special treatment for report
{ {
if(old_info_name != info_name) if(old_info_name != info_name)
{ {
@@ -472,7 +474,8 @@ void DynamicElementTextItem::setCompositeText(const QString &text)
updateXref(); updateXref();
} }
if (m_parent_element && (m_parent_element.data()->linkType() & Element::AllReport)) //special treatment for report if (m_parent_element && ((m_parent_element.data()->linkType() & Element::AllReport) ||
m_parent_element.data()->linkType() == Element::ConductorDefinition)) //special treatment for report
{ {
/* /*
* May be in some case the old and new composite text both have the var %{label}, * May be in some case the old and new composite text both have the var %{label},
@@ -726,7 +729,8 @@ QVariant DynamicElementTextItem::itemChange(QGraphicsItem::GraphicsItemChange ch
if(!m_parent_element.data()->linkedElements().isEmpty()) if(!m_parent_element.data()->linkedElements().isEmpty())
masterChanged(); masterChanged();
} }
else if(m_parent_element.data()->linkType() & Element::AllReport) else if((m_parent_element.data()->linkType() & Element::AllReport) ||
m_parent_element.data()->linkType() == Element::ConductorDefinition)
{ {
//Get the report formula, and add connection to keep up to date the formula. //Get the report formula, and add connection to keep up to date the formula.
if (m_parent_element.data()->diagram() && m_parent_element.data()->diagram()->project()) if (m_parent_element.data()->diagram() && m_parent_element.data()->diagram()->project())
@@ -1027,7 +1031,8 @@ void DynamicElementTextItem::clearFormulaConnection()
void DynamicElementTextItem::updateReportFormulaConnection() void DynamicElementTextItem::updateReportFormulaConnection()
{ {
if(!(m_parent_element.data()->linkType() & Element::AllReport)) if(!(m_parent_element.data()->linkType() & Element::AllReport) &&
m_parent_element.data()->linkType() != Element::ConductorDefinition)
return; return;
removeConnectionForReportFormula(m_report_formula); removeConnectionForReportFormula(m_report_formula);
@@ -1041,7 +1046,8 @@ void DynamicElementTextItem::updateReportFormulaConnection()
*/ */
void DynamicElementTextItem::updateReportText() void DynamicElementTextItem::updateReportText()
{ {
if(!(m_parent_element.data()->linkType() & Element::AllReport)) if(!(m_parent_element.data()->linkType() & Element::AllReport) &&
m_parent_element.data()->linkType() != Element::ConductorDefinition)
return; return;
if (m_text_from == ElementInfo && m_info_name == "label") if (m_text_from == ElementInfo && m_info_name == "label")
@@ -1098,7 +1104,10 @@ void DynamicElementTextItem::updateLabel()
void DynamicElementTextItem::conductorWasAdded(Conductor *conductor) void DynamicElementTextItem::conductorWasAdded(Conductor *conductor)
{ {
Q_UNUSED(conductor) Q_UNUSED(conductor)
QTimer::singleShot(100, this, [this]() {
setPotentialConductor(); setPotentialConductor();
conductorPropertiesChanged();
});
} }
/** /**
@@ -1123,7 +1132,8 @@ void DynamicElementTextItem::conductorWasRemoved(Conductor *conductor)
*/ */
void DynamicElementTextItem::setPotentialConductor() void DynamicElementTextItem::setPotentialConductor()
{ {
if(parentElement() && (parentElement()->linkType() & Element::AllReport)) if(parentElement() && ((parentElement()->linkType() & Element::AllReport) || (parentElement()->linkType() == Element::ConductorDefinition) ||
parentElement()->linkType() == Element::ConductorDefinition))
{ {
if(parentElement()->terminals().isEmpty()) if(parentElement()->terminals().isEmpty())
return; return;
@@ -1156,6 +1166,7 @@ void DynamicElementTextItem::setPotentialConductor()
connect(m_watched_conductor.data(), &Conductor::propertiesChange, this, &DynamicElementTextItem::conductorPropertiesChanged); connect(m_watched_conductor.data(), &Conductor::propertiesChange, this, &DynamicElementTextItem::conductorPropertiesChanged);
} }
} }
conductorPropertiesChanged();
} }
else //This text haven't got a parent element, then ther isn't a conductor in the potential else //This text haven't got a parent element, then ther isn't a conductor in the potential
{ {
@@ -1172,7 +1183,8 @@ void DynamicElementTextItem::setPotentialConductor()
*/ */
void DynamicElementTextItem::conductorPropertiesChanged() void DynamicElementTextItem::conductorPropertiesChanged()
{ {
if(m_parent_element && (m_parent_element.data()->linkType() & Element::AllReport)) if(m_parent_element && ((m_parent_element.data()->linkType() & Element::AllReport) || (m_parent_element.data()->linkType() == Element::ConductorDefinition) ||
m_parent_element.data()->linkType() == Element::ConductorDefinition))
{ {
if(m_text_from == ElementInfo) if(m_text_from == ElementInfo)
{ {
@@ -1201,7 +1213,8 @@ QString DynamicElementTextItem::reportReplacedCompositeText() const
{ {
QString string; QString string;
if(m_parent_element.data()->linkType() & Element::AllReport) if((m_parent_element.data()->linkType() & Element::AllReport) || (m_parent_element.data()->linkType() == Element::ConductorDefinition) ||
m_parent_element.data()->linkType() == Element::ConductorDefinition)
{ {
string = m_composite_text; string = m_composite_text;
@@ -86,6 +86,9 @@ class DynamicElementTextItem : public DiagramTextItem
void fromXml(const QDomElement &dom_elmt) override; void fromXml(const QDomElement &dom_elmt) override;
Element *parentElement() const; Element *parentElement() const;
/// PDF export: slave cross-reference text item ("(folio-pos)") and its master target.
QGraphicsTextItem *slaveXrefItem() const { return m_slave_Xref_item; }
Element *masterElement() const { return m_master_element.data(); }
ElementTextItemGroup *parentGroup() const; ElementTextItemGroup *parentGroup() const;
Element *elementUseForInfo() const; Element *elementUseForInfo() const;
void refreshLabelConnection(); void refreshLabelConnection();
+33
View File
@@ -36,6 +36,7 @@
#include "../qetxml.h" #include "../qetxml.h"
#include "../qetversion.h" #include "../qetversion.h"
#include "qgraphicsitemutility.h" #include "qgraphicsitemutility.h"
#include <QDebug>
#include <QDomElement> #include <QDomElement>
#include <utility> #include <utility>
@@ -431,6 +432,11 @@ bool Element::buildFromXml(const QDomElement &xml_def_elmt, int *state)
m_data.fromXml(xml_def_elmt); m_data.fromXml(xml_def_elmt);
setToolTip(name()); setToolTip(name());
QString my_type_str = xml_def_elmt.attribute(QStringLiteral("link_type"), QStringLiteral("simple"));
if (my_type_str == QLatin1String("conductor_definition")) {
m_link_type = Element::ConductorDefinition;
}
//load kind informations //load kind informations
m_kind_informations.fromXml( m_kind_informations.fromXml(
xml_def_elmt.firstChildElement(QStringLiteral("kindInformations")), xml_def_elmt.firstChildElement(QStringLiteral("kindInformations")),
@@ -627,6 +633,9 @@ Terminal *Element::parseTerminal(const QDomElement &dom_element)
Terminal *new_terminal = new Terminal(data, this); Terminal *new_terminal = new Terminal(data, this);
m_terminals << new_terminal; m_terminals << new_terminal;
connect(new_terminal, &Terminal::conductorWasAdded, this, &Element::updateConductorTexts);
connect(new_terminal, &Terminal::conductorWasRemoved, this, &Element::updateConductorTexts);
//Sort from top to bottom and left to right //Sort from top to bottom and left to right
std::sort(m_terminals.begin(), std::sort(m_terminals.begin(),
m_terminals.end(), m_terminals.end(),
@@ -1288,6 +1297,8 @@ QString Element::linkTypeToString() const
return QStringLiteral("Terminale"); return QStringLiteral("Terminale");
case Thumbnail: case Thumbnail:
return QStringLiteral("Thumbnail"); return QStringLiteral("Thumbnail");
case ConductorDefinition:
return QStringLiteral("ConductorDefinition");
default: default:
return QStringLiteral("Unknown"); return QStringLiteral("Unknown");
} }
@@ -1555,3 +1566,25 @@ ElementsLocation Element::location() const
{ {
return m_location; return m_location;
} }
/**
* @brief Element::updateConductorTexts
*Slot that is triggered when a cable is *
*connected to or disconnected from a terminal on this component.
*/
/**
* @brief Element::updateConductorTexts
*/
void Element::updateConductorTexts()
{
if (m_link_type != Element::ConductorDefinition) {
return;
}
for (DynamicElementTextItem *deti : m_dynamic_text_list) {
if (deti) {
deti->setPotentialConductor();
deti->updateLabel();
}
}
}
+4 -1
View File
@@ -59,7 +59,8 @@ class Element : public QetGraphicsItem
Master = 8, Master = 8,
Slave = 16, Slave = 16,
Terminale = 32, Terminale = 32,
Thumbnail = 64}; Thumbnail = 64,
ConductorDefinition = 128};
Element(const ElementsLocation &location, Element(const ElementsLocation &location,
QGraphicsItem * = nullptr, QGraphicsItem * = nullptr,
@@ -95,6 +96,8 @@ class Element : public QetGraphicsItem
DynamicElementTextItem *text, DynamicElementTextItem *text,
ElementTextItemGroup *group); ElementTextItemGroup *group);
public slots:
void updateConductorTexts();
public: public:
QList<Terminal *> terminals() const; QList<Terminal *> terminals() const;
@@ -76,6 +76,8 @@ class ElementTextItemGroup : public QObject, public QGraphicsItemGroup
QList<DynamicElementTextItem *> texts() const; QList<DynamicElementTextItem *> texts() const;
Diagram *diagram() const; Diagram *diagram() const;
Element *parentElement() const; Element *parentElement() const;
/// PDF export: slave cross-reference text item of the group, if any.
QGraphicsTextItem *slaveXrefItem() const { return m_slave_Xref_item; }
QDomElement toXml(QDomDocument &dom_document) const; QDomElement toXml(QDomDocument &dom_document) const;
void fromXml(QDomElement &dom_element); void fromXml(QDomElement &dom_element);
+15
View File
@@ -753,6 +753,15 @@ QString Terminal::name() const
return d->m_name; return d->m_name;
} }
/**
@brief Terminal::terminalType
@return the type of this terminal (Generic, Inner, Outer, No, Nc, Common)
*/
TerminalData::Type Terminal::terminalType() const
{
return d->m_type;
}
/** /**
@brief Conductor::relatedPotentialTerminal @brief Conductor::relatedPotentialTerminal
Return terminal at the same potential from the same Return terminal at the same potential from the same
@@ -779,6 +788,12 @@ QList<Terminal *> relatedPotentialTerminal (
// If terminal parent element is a Terminal element. // If terminal parent element is a Terminal element.
else if (terminal -> parentElement() -> linkType() & Element::Terminale) else if (terminal -> parentElement() -> linkType() & Element::Terminale)
{ {
// English: Check if the user activated the potential isolation checkbox for this terminal
if (terminal->parentElement()->elementInformations().value(QStringLiteral("potential_isolating")).toString() == QLatin1String("true")) {
// English: Potential is isolated. Return an empty list so it does not propagate to the other side.
return QList<Terminal *>();
}
QList <Terminal *> terminals = terminal->parentElement()->terminals(); QList <Terminal *> terminals = terminal->parentElement()->terminals();
terminals.removeAll(const_cast<Terminal *>(terminal)); terminals.removeAll(const_cast<Terminal *>(terminal));
return terminals; return terminals;
+3 -1
View File
@@ -18,13 +18,14 @@
#ifndef TERMINAL_H #ifndef TERMINAL_H
#define TERMINAL_H #define TERMINAL_H
#include "../qet.h" #include "../qet.h"
#include "../properties/terminaldata.h"
#include <QtWidgets> #include <QtWidgets>
#include <QtXml> #include <QtXml>
class Conductor; class Conductor;
class Diagram; class Diagram;
class Element; class Element;
class TerminalData;
/** /**
@brief The Terminal class @brief The Terminal class
@@ -75,6 +76,7 @@ class Terminal : public QGraphicsObject
Element *parentElement () const; Element *parentElement () const;
QUuid uuid () const; QUuid uuid () const;
QString name () const; QString name () const;
TerminalData::Type terminalType() const;
QList<Conductor *> conductors() const; QList<Conductor *> conductors() const;
Qet::Orientation orientation() const; Qet::Orientation orientation() const;
+2 -1
View File
@@ -189,7 +189,8 @@ QStringList QETInformation::elementInfoKeys()
ELMT_MACHINE_MANUFACTURER_REF_AUX4, ELMT_MACHINE_MANUFACTURER_REF_AUX4,
ELMT_SUPPLIER_AUX4, ELMT_SUPPLIER_AUX4,
ELMT_QUANTITY_AUX4, ELMT_QUANTITY_AUX4,
ELMT_UNITY_AUX4, }; ELMT_UNITY_AUX4,
"exclude_from_bom" };
return list; return list;
} }
+54 -5
View File
@@ -16,7 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>. along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "elementinfowidget.h" #include "elementinfowidget.h"
#include <QCheckBox>
#include "../diagram.h" #include "../diagram.h"
#include "../qetapp.h" #include "../qetapp.h"
#include "../qetgraphicsitem/element.h" #include "../qetgraphicsitem/element.h"
@@ -161,6 +161,13 @@ void ElementInfoWidget::enableLiveEdit()
for (ElementInfoPartWidget *eipw : m_eipw_list) for (ElementInfoPartWidget *eipw : m_eipw_list)
connect(eipw, &ElementInfoPartWidget::textChanged, this, &ElementInfoWidget::apply); connect(eipw, &ElementInfoPartWidget::textChanged, this, &ElementInfoWidget::apply);
connect(ui->m_auto_num_locked_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply); connect(ui->m_auto_num_locked_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
if (m_potential_isolating_cb) {
connect(m_potential_isolating_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
}
if (m_exclude_from_bom_cb) {
connect(m_exclude_from_bom_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
}
} }
/** /**
@@ -172,6 +179,13 @@ void ElementInfoWidget::disableLiveEdit()
for (ElementInfoPartWidget *eipw : m_eipw_list) for (ElementInfoPartWidget *eipw : m_eipw_list)
disconnect(eipw, &ElementInfoPartWidget::textChanged, this, &ElementInfoWidget::apply); disconnect(eipw, &ElementInfoPartWidget::textChanged, this, &ElementInfoWidget::apply);
disconnect(ui->m_auto_num_locked_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply); disconnect(ui->m_auto_num_locked_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
if (m_potential_isolating_cb) {
disconnect(m_potential_isolating_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
}
if (m_exclude_from_bom_cb) {
disconnect(m_exclude_from_bom_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
}
} }
/** /**
@@ -193,16 +207,34 @@ void ElementInfoWidget::buildInterface()
ui->scroll_vlayout->addWidget(eipw); ui->scroll_vlayout->addWidget(eipw);
m_eipw_list << eipw; m_eipw_list << eipw;
} }
ui->scroll_vlayout->addStretch(); ui->scroll_vlayout->addStretch();
// Existing potential isolating checkbox
m_potential_isolating_cb = new QCheckBox(tr("Séparation de potentiel"), this);
m_potential_isolating_cb->setStyleSheet(QStringLiteral("margin: 5px; font-weight: bold;"));
// English: Initialize and style the BOM exclusion checkbox
m_exclude_from_bom_cb = new QCheckBox(tr("Exclure de la nomenclature"), this);
m_exclude_from_bom_cb->setStyleSheet(QStringLiteral("margin: 5px; font-weight: bold;"));
if (QVBoxLayout *mainLayout = qobject_cast<QVBoxLayout*>(this->layout())) {
mainLayout->insertWidget(1, m_potential_isolating_cb);
// English: Insert the new checkbox into the main vertical layout
mainLayout->insertWidget(2, m_exclude_from_bom_cb);
}
// English: BOM exclusion applies to all elements, so it's always visible
m_exclude_from_bom_cb->setVisible(true);
// Show checkbox only if the element is a terminal // Show checkbox only if the element is a terminal
if (m_element.data()->elementData().m_type == ElementData::Terminal) { if (m_element.data()->elementData().m_type == ElementData::Terminal) {
ui->m_auto_num_locked_cb->setVisible(true); ui->m_auto_num_locked_cb->setVisible(true);
m_potential_isolating_cb->setVisible(true);
} else { } else {
ui->m_auto_num_locked_cb->setVisible(false); ui->m_auto_num_locked_cb->setVisible(false);
m_potential_isolating_cb->setVisible(false);
} }
} }
/** /**
@brief ElementInfoWidget::infoPartWidgetForKey @brief ElementInfoWidget::infoPartWidgetForKey
@param key @param key
@@ -243,6 +275,17 @@ void ElementInfoWidget::updateUi()
if (m_element->elementData().m_type == ElementData::Terminal) { if (m_element->elementData().m_type == ElementData::Terminal) {
QString lock_value = element_info.value(QStringLiteral("auto_num_locked")).toString(); QString lock_value = element_info.value(QStringLiteral("auto_num_locked")).toString();
ui->m_auto_num_locked_cb->setChecked(lock_value == QLatin1String("true")); ui->m_auto_num_locked_cb->setChecked(lock_value == QLatin1String("true"));
// English: Load the potential isolating status from the element information mapping
if (m_potential_isolating_cb) {
QString isolating_value = element_info.value(QStringLiteral("potential_isolating")).toString();
m_potential_isolating_cb->setChecked(isolating_value == QLatin1String("true"));
}
}
// English: Load the BOM exclusion status from the element information mapping
if (m_exclude_from_bom_cb) {
QString exclude_bom_value = element_info.value(QStringLiteral("exclude_from_bom")).toString();
m_exclude_from_bom_cb->setChecked(exclude_bom_value == QLatin1String("true"));
} }
if (m_live_edit) { if (m_live_edit) {
@@ -260,7 +303,6 @@ DiagramContext ElementInfoWidget::currentInfo() const
for (const auto &eipw : qAsConst(m_eipw_list)) for (const auto &eipw : qAsConst(m_eipw_list))
{ {
//add value only if they're something to store //add value only if they're something to store
if (!eipw->text().isEmpty()) if (!eipw->text().isEmpty())
{ {
@@ -275,10 +317,17 @@ DiagramContext ElementInfoWidget::currentInfo() const
// Save the auto numbering lock status // Save the auto numbering lock status
if (m_element->elementData().m_type == ElementData::Terminal) { if (m_element->elementData().m_type == ElementData::Terminal) {
info_.addValue(QStringLiteral("auto_num_locked"), ui->m_auto_num_locked_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false")); info_.addValue(QStringLiteral("auto_num_locked"), ui->m_auto_num_locked_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
if (m_potential_isolating_cb) {
info_.addValue(QStringLiteral("potential_isolating"), m_potential_isolating_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
}
}
if (m_exclude_from_bom_cb) {
info_.addValue(QStringLiteral("exclude_from_bom"), m_exclude_from_bom_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
} }
return info_; return info_;
} }
/** /**
@brief ElementInfoWidget::firstActivated @brief ElementInfoWidget::firstActivated
Slot activated when this widget is show. Slot activated when this widget is show.
+3
View File
@@ -27,6 +27,7 @@ class Element;
class QUndoCommand; class QUndoCommand;
class ElementInfoPartWidget; class ElementInfoPartWidget;
class ChangeElementInformationCommand; class ChangeElementInformationCommand;
class QCheckBox;
namespace Ui { namespace Ui {
class ElementInfoWidget; class ElementInfoWidget;
@@ -71,6 +72,8 @@ class ElementInfoWidget : public AbstractElementPropertiesEditorWidget
private: private:
Ui::ElementInfoWidget *ui; Ui::ElementInfoWidget *ui;
QList <ElementInfoPartWidget *> m_eipw_list; QList <ElementInfoPartWidget *> m_eipw_list;
QCheckBox *m_potential_isolating_cb = nullptr;
QCheckBox *m_exclude_from_bom_cb = nullptr;
bool m_first_activation; bool m_first_activation;
bool m_ui_builded = false; bool m_ui_builded = false;
}; };
+3
View File
@@ -308,6 +308,9 @@ void ElementPropertiesWidget::updateUi()
case Element::Terminale: case Element::Terminale:
m_list_editor << new ElementInfoWidget(m_element, this); m_list_editor << new ElementInfoWidget(m_element, this);
break; break;
case Element::ConductorDefinition:
break;
default: default:
break; break;
} }
+2
View File
@@ -139,6 +139,7 @@ void XRefPropertiesWidget::saveProperties(int index) {
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "right") xrp.setXrefPos(Qt::AlignRight); else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "right") xrp.setXrefPos(Qt::AlignRight);
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "alignment") xrp.setXrefPos(Qt::AlignBaseline); else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "alignment") xrp.setXrefPos(Qt::AlignBaseline);
xrp.setShowPowerContac(ui->m_show_power_cb->isChecked()); xrp.setShowPowerContac(ui->m_show_power_cb->isChecked());
xrp.setShowTerminalName(ui->m_show_terminal_name_cb->isChecked());
xrp.setPrefix("power", ui->m_power_prefix_le->text()); xrp.setPrefix("power", ui->m_power_prefix_le->text());
xrp.setPrefix("delay", ui->m_delay_prefix_le->text()); xrp.setPrefix("delay", ui->m_delay_prefix_le->text());
xrp.setPrefix("switch", ui->m_switch_prefix_le->text()); xrp.setPrefix("switch", ui->m_switch_prefix_le->text());
@@ -190,6 +191,7 @@ void XRefPropertiesWidget::updateDisplay()
else if(xrp.getXrefPos() == Qt::AlignBaseline) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("alignment")); else if(xrp.getXrefPos() == Qt::AlignBaseline) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("alignment"));
else if(xrp.getXrefPos() == Qt::AlignBottom) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("bottom")); else if(xrp.getXrefPos() == Qt::AlignBottom) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("bottom"));
ui->m_show_power_cb->setChecked(xrp.showPowerContact()); ui->m_show_power_cb->setChecked(xrp.showPowerContact());
ui->m_show_terminal_name_cb->setChecked(xrp.showTerminalName());
ui->m_power_prefix_le-> setText(xrp.prefix("power")); ui->m_power_prefix_le-> setText(xrp.prefix("power"));
ui->m_delay_prefix_le-> setText(xrp.prefix("delay")); ui->m_delay_prefix_le-> setText(xrp.prefix("delay"));
ui->m_switch_prefix_le->setText(xrp.prefix("switch")); ui->m_switch_prefix_le->setText(xrp.prefix("switch"));
+8
View File
@@ -108,6 +108,13 @@
<layout class="QHBoxLayout" name="horizontalLayout_3"> <layout class="QHBoxLayout" name="horizontalLayout_3">
<item> <item>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="m_show_terminal_name_cb">
<property name="text">
<string>Afficher les numéros de bornes dans les Xrefs</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QRadioButton" name="m_display_has_contacts_rb"> <widget class="QRadioButton" name="m_display_has_contacts_rb">
<property name="text"> <property name="text">
@@ -283,6 +290,7 @@
<tabstop>m_master_le</tabstop> <tabstop>m_master_le</tabstop>
<tabstop>m_slave_le</tabstop> <tabstop>m_slave_le</tabstop>
<tabstop>m_show_power_cb</tabstop> <tabstop>m_show_power_cb</tabstop>
<tabstop>m_show_terminal_name_cb</tabstop>
<tabstop>m_power_prefix_le</tabstop> <tabstop>m_power_prefix_le</tabstop>
<tabstop>m_delay_prefix_le</tabstop> <tabstop>m_delay_prefix_le</tabstop>
<tabstop>m_switch_prefix_le</tabstop> <tabstop>m_switch_prefix_le</tabstop>
+60 -6
View File
@@ -151,9 +151,8 @@ void WiringListExport::toCsv()
{ {
if (!m_project) return; if (!m_project) return;
QDomDocument doc = m_project->toXml(); const QString csv = toCsvString();
if (csv.isEmpty()) {
if (doc.isNull()) {
QMessageBox::warning(m_parent, tr("Erreur"), tr("Impossible de lire la structure en mémoire du projet.")); QMessageBox::warning(m_parent, tr("Erreur"), tr("Impossible de lire la structure en mémoire du projet."));
return; return;
} }
@@ -172,6 +171,57 @@ void WiringListExport::toCsv()
QMessageBox::warning(m_parent, tr("Erreur"), tr("Impossible d'ouvrir le fichier pour l'écriture.")); QMessageBox::warning(m_parent, tr("Erreur"), tr("Impossible d'ouvrir le fichier pour l'écriture."));
return; return;
} }
QTextStream out(&file);
out << csv;
file.close();
QMessageBox::information(m_parent, tr("Export réussi"), tr("Le plan de câblage a été exporté avec succès !"));
}
QString WiringListExport::toCsvString() const
{
if (!m_project) return QString();
QDomDocument doc = m_project->toXml();
if (doc.isNull()) return QString();
QSet<QString> conductorDefinitionTypes;
QDomElement rootElem = doc.documentElement();
QDomElement collection = rootElem.firstChildElement("collection");
if (!collection.isNull()) {
QDomNodeList defs = collection.elementsByTagName("definition");
for (int i = 0; i < defs.size(); ++i) {
QDomElement def = defs.at(i).toElement();
if (def.attribute("link_type") == "conductor_definition") {
QDomElement parentEl = def.parentNode().toElement();
if (parentEl.tagName().toLower() == "element") {
QString name = parentEl.attribute("name");
if (!name.isEmpty()) {
conductorDefinitionTypes.insert(name);
}
}
}
}
}
QSet<QString> conductorDefinitionUuids;
QDomNodeList projectElements = rootElem.elementsByTagName("element");
for (int i = 0; i < projectElements.size(); ++i) {
QDomElement el = projectElements.at(i).toElement();
QString typeVal = el.attribute("type");
bool isCondDef = false;
for (const QString &cType : conductorDefinitionTypes) {
if (typeVal.endsWith(cType)) {
isCondDef = true;
break;
}
}
if (isCondDef) {
QString uuid = normalizeUuid(el.attribute("uuid", el.attribute("id", "")));
if (!uuid.isEmpty()) {
conductorDefinitionUuids.insert(uuid);
}
}
}
QMap<QString, ElementInfo> elementsInfo = collectElementsInfo(doc.documentElement()); QMap<QString, ElementInfo> elementsInfo = collectElementsInfo(doc.documentElement());
QList<ConductorData> conductors = collectConductors(doc.documentElement()); QList<ConductorData> conductors = collectConductors(doc.documentElement());
@@ -200,6 +250,10 @@ void WiringListExport::toCsv()
for (int i = 0; i < conductors.size(); ++i) { for (int i = 0; i < conductors.size(); ++i) {
ConductorData c = conductors[i]; ConductorData c = conductors[i];
if (conductorDefinitionUuids.contains(c.el1_uuid) || conductorDefinitionUuids.contains(c.el2_uuid)) {
continue;
}
if (c.element1_label.isEmpty() && elementsInfo.contains(c.el1_uuid)) { if (c.element1_label.isEmpty() && elementsInfo.contains(c.el1_uuid)) {
c.element1_label = elementsInfo[c.el1_uuid].label; c.element1_label = elementsInfo[c.el1_uuid].label;
if (c.element1_label.isEmpty()) c.element1_label = elementsInfo[c.el1_uuid].name; if (c.element1_label.isEmpty()) c.element1_label = elementsInfo[c.el1_uuid].name;
@@ -310,7 +364,8 @@ void WiringListExport::toCsv()
return a.terminalname2 < b.terminalname2; return a.terminalname2 < b.terminalname2;
}); });
QTextStream out(&file); QString csv;
QTextStream out(&csv);
out << tr("Page", "Wiring list CSV header") << ";" out << tr("Page", "Wiring list CSV header") << ";"
<< tr("Composant 1", "Wiring list CSV header") << ";" << tr("Composant 1", "Wiring list CSV header") << ";"
<< tr("Borne 1", "Wiring list CSV header") << ";" << tr("Borne 1", "Wiring list CSV header") << ";"
@@ -333,6 +388,5 @@ void WiringListExport::toCsv()
<< c.function << "\n"; << c.function << "\n";
} }
file.close(); return csv;
QMessageBox::information(m_parent, tr("Export réussi"), tr("Le plan de câblage a été exporté avec succès !"));
} }
+5
View File
@@ -45,6 +45,11 @@ class WiringListExport : public QObject
public: public:
explicit WiringListExport(QETProject *project, QWidget *parent = nullptr); explicit WiringListExport(QETProject *project, QWidget *parent = nullptr);
void toCsv(); void toCsv();
/**
Build the wiring-list CSV and return it as a string (no GUI).
Used by toCsv() and by the headless command-line export.
*/
QString toCsvString() const;
private: private:
QETProject *m_project; QETProject *m_project;