Compare commits

...

59 Commits

Author SHA1 Message Date
Laurent Trinques 998c5e8a0d Update translations files 2026-06-12 13:28:08 +02:00
Laurent Trinques d4061d17fa Merge pull request #497 from ispyisail/fix-readonly-after-saveas
Clear read-only state when a project is saved to a writable file
2026-06-12 11:48:05 +02:00
Laurent Trinques fb8c86ec12 Merge pull request #495 from ispyisail/feature-folio-template-variables
Folio properties: auto-add a title block's custom variables
2026-06-12 11:47:33 +02:00
Laurent Trinques d62479e6e4 Merge pull request #496 from ispyisail/fix-system-regional-locale
Fix regional system locale loading the wrong translation (pt_BR/nl_BE/nl_NL)
2026-06-12 11:46:56 +02:00
Laurent Trinques 1a4bb91285 Merge pull request #494 from ispyisail/fix-elementdialog-filename-label
Element editor Save As: label the field as a file name, not 'element name'
2026-06-12 11:46:12 +02:00
Laurent Trinques aa96d5e7df Merge pull request #493 from ispyisail/cli-set-titleblock
CLI: add --set-titleblock, and fix headless backup crash
2026-06-12 11:42:04 +02:00
Shane Ringrose a8e408ad39 Clear read-only state when a project is saved to a writable file (#217)
Saving a read-only project to a writable location (e.g. Save As to /tmp)
left it marked read-only, so it stayed uneditable until closed and
reopened. Two issues in QETProject::write():

- The guard refused to write whenever QFileInfo(path).isWritable() was
  false. For a Save As to a *new* file that test is always false (the file
  doesn't exist yet), so it could wrongly block saving a read-only project
  elsewhere. Now it checks the directory's writability for a new file.
- After a successful write the read-only flag was never cleared. Since the
  file was just written, it is writable, so clear it (setReadOnly(false)
  emits readOnlyChanged, re-enabling editing live).

Fixes #217.
2026-06-12 06:09:48 +12:00
Shane Ringrose 0eb3e1e436 Fix regional system locale loading the wrong translation (#421)
langFromSetting() truncated the system locale to two letters
(QLocale::system().name().left(2)), so a user on the default 'Système'
language whose locale is regional got the base-language translation
instead of their regional one. QET ships qet_pt_BR, qet_nl_BE and
qet_nl_NL, so e.g. a Brazilian user saw European Portuguese (and
untranslated strings fell back to the French source).

Keep the full locale name and, in setLanguage(), try the exact
translation, then the base language, then English (French stays the
native source). Brazilian/Belgian/Dutch users on 'system' now get their
regional translation; everyone else is unaffected.

Refs #421.
2026-06-12 05:58:04 +12:00
Shane Ringrose ba6320bff8 Folio properties: auto-add a title block's custom variables (#271)
When a title block template uses custom variables (e.g. %{department},
%{owner}), the user previously had to declare each one by hand in the
folio properties 'Custom' tab before a value could be entered. Now the
template's undefined custom variables are added automatically, so the
user only fills in the values.

- listOfVariables() now extracts %{name} placeholders with a regex
  (deduplicated) instead of a crude '%' strip that returned '{name}'.
- The folio properties widget merges the template's custom variables into
  the Custom tab both on open (setProperties) and when the template is
  changed, preserving any values already entered and skipping the
  standard fields (title, author, date, ...) which have their own inputs.

Fixes #271 (variable auto-population; the revision-history request in the
thread is a separate feature).
2026-06-12 05:45:08 +12:00
Shane Ringrose 86b5d7ac95 Element editor Save As: label the field as a file name, not element name (#469)
The Save As 'location' dialog used a QFileNameEdit (accepts only
[0-9a-z_-.]) but labelled it 'New element name', which is confusing —
QET also has a separate, translatable display name shown in the
collection. Users reasonably tried to type a display name (spaces,
capitals) and it was rejected.

Rename the placeholder to 'Element file name' and add a tooltip noting
the accepted characters and that the display name is edited separately
in the element properties. Updates the English translation; other
languages fall back until re-translated.

Fixes #469.
2026-06-12 05:34:20 +12:00
Shane Ringrose 19e99aab02 CLI: disable async crash-recovery backup in headless mode (fixes segfault)
QETProject schedules an asynchronous crash-recovery backup on construction
(writeBackup() -> QtConcurrent::run(QET::writeToFile, ..., &m_backup_file)).
In one-shot CLI mode the QETProject is destroyed as soon as the command
returns, while that background write still references its m_backup_file
member — an intermittent use-after-free segfault during teardown (~1 in 6
runs; observed on --resave and --set-titleblock).

A crash-recovery backup is meaningless for a short-lived headless command,
so add QETProject::setBackupEnabled(false), called from the CLI entry in
main(). writeBackup() then early-returns, so no background write is ever
launched. Fixes the crash for all CLI commands. See #492.
2026-06-12 05:25:44 +12:00
Shane Ringrose 44f0abbb56 CLI: add --set-titleblock to stamp title-block fields headlessly
The first write-to-project CLI command, aimed at CI / revision workflows:
stamp title-block metadata onto every folio (and the project default),
then save. Each argument is key=value:

  qelectrotech --set-titleblock in.qet out.qet revision=B date=today

Standard keys map to the documented title-block fields (title, author,
date, plant, location, revision, version, filename); date=today uses the
current date and an explicit date forces UseDateValue mode; any other key
is stored as a custom title-block field. Assignments are parsed up front
so a malformed one fails before writing.

Addresses the 'saving' side of the CLI-for-scripts request (#162).
2026-06-12 05:25:44 +12:00
Laurent Trinques e7787daa2c Update qet_zh.qm 2026-06-11 16:26:31 +02:00
Laurent Trinques c7fd3416f6 Merge pull request #491 from zi-mozhuang/master
Update qet_zh.ts
2026-06-11 16:25:42 +02:00
子墨庄 b782a1612a Update qet_zh.ts
Checked the format and conducted a pre-test using the linguist.
2026-06-11 22:16:49 +08:00
Laurent Trinques 2fdbc3c243 Merge pull request #490 from ispyisail/cli-pdf-links
CLI: clickable cross-reference hyperlinks in PDF export
2026-06-11 14:39:53 +02:00
Shane Ringrose a219c3f587 CLI: clickable cross-reference hyperlinks in PDF export
Wire the shared PdfLinks helper into the headless --export-pdf path so
CLI-exported PDFs get the same internal cross-reference / folio-report
navigation as the GUI print export.

For each page, after rendering, the scene-to-page geometry is rebuilt
from the QPdfWriter (96 dpi, zero margins, page sized to the diagram so
the scale is ~1 with no centering) — deliberately NOT reusing the
QPrinter-based mapping — and passed to PdfLinks::injectCrossRefLinks().
After the painter closes, PdfLinks::convertUriToGoTo() rewrites the URI
annotations into native GoTo/FitR actions.

Builds on the helper extracted in the previous commit; no change to the
other CLI tools.
2026-06-11 23:48:45 +12:00
Shane Ringrose 6b3b55b0e1 PDF links: extract pdf_links.{cpp,h} shared helper
Move the PDF cross-reference hyperlink logic out of ProjectPrintWindow
into a standalone translation unit so it can be reused (the CLI PDF
export will call it next):

- injectCrossRefLinks(): emits the URI link annotations for a diagram's
  cross-references and folio reports. The scene-to-page mapping is passed
  in as a PageGeometry (transform + devToPdf + source-rect lookup) so each
  caller supplies its own correct geometry, rather than the helper assuming
  a QPrinter.
- convertUriToGoTo(): the PDF post-processor, moved verbatim.

ProjectPrintWindow stays a pure caller: it builds its PageGeometry from the
printer page layout exactly as before and calls the helper. No behavioural
change to GUI PDF export; no class-structure changes.

Per review guidance on #483.
2026-06-11 23:48:14 +12:00
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
69 changed files with 9859 additions and 7345 deletions
+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})
+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()
+9
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,10 @@ 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/pdf_links.cpp
${QET_DIR}/sources/pdf_links.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 +504,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 +720,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
+217 -194
View File
File diff suppressed because it is too large Load Diff
+217 -194
View File
File diff suppressed because it is too large Load Diff
+217 -194
View File
File diff suppressed because it is too large Load Diff
+217 -194
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+228 -205
View File
File diff suppressed because it is too large Load Diff
+217 -194
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+216 -192
View File
File diff suppressed because it is too large Load Diff
+217 -194
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+214 -191
View File
File diff suppressed because it is too large Load Diff
+217 -194
View File
File diff suppressed because it is too large Load Diff
+217 -194
View File
File diff suppressed because it is too large Load Diff
+217 -194
View File
File diff suppressed because it is too large Load Diff
+215 -192
View File
File diff suppressed because it is too large Load Diff
+217 -194
View File
File diff suppressed because it is too large Load Diff
+215 -192
View File
File diff suppressed because it is too large Load Diff
+215 -192
View File
File diff suppressed because it is too large Load Diff
+217 -194
View File
File diff suppressed because it is too large Load Diff
+214 -191
View File
File diff suppressed because it is too large Load Diff
+217 -194
View File
File diff suppressed because it is too large Load Diff
+217 -194
View File
File diff suppressed because it is too large Load Diff
+217 -194
View File
File diff suppressed because it is too large Load Diff
+217 -194
View File
File diff suppressed because it is too large Load Diff
+214 -191
View File
File diff suppressed because it is too large Load Diff
+217 -194
View File
File diff suppressed because it is too large Load Diff
+214 -191
View File
File diff suppressed because it is too large Load Diff
+214 -191
View File
File diff suppressed because it is too large Load Diff
+214 -191
View File
File diff suppressed because it is too large Load Diff
+215 -192
View File
File diff suppressed because it is too large Load Diff
+215 -192
View File
File diff suppressed because it is too large Load Diff
+215 -192
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1495 -1444
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) \
+825
View File
@@ -0,0 +1,825 @@
/*
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 "pdf_links.h"
#include "qetgraphicsitem/conductor.h"
#include "qetgraphicsitem/element.h"
#include "qetgraphicsitem/terminal.h"
#include "qetproject.h"
#include "titleblockproperties.h"
#include "wiringlistexport.h"
// Private Qt PDF engine for drawHyperlink() — see pdf_links / projectprintwindow.
#include <private/qpdf_p.h>
#include <QDir>
#include <QDirIterator>
#include <QDomDocument>
#include <QDate>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMap>
#include <QPageLayout>
#include <QPair>
#include <QPainter>
#include <QPdfWriter>
#include <QSet>
#include <QSqlError>
#include <QSqlQuery>
#include <QSvgGenerator>
#include <QTextStream>
#include <QTransform>
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"},
{"--set-titleblock", "settb"},
};
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;
}
// Page numbers (1-based) for cross-reference hyperlink targets: each
// diagram is exactly one page in the CLI export (no tiling).
QMap<Diagram *, int> pageMap;
for (int i = 0; i < diagrams.size(); ++i)
pageMap.insert(diagrams.at(i), i + 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);
// Inject clickable cross-reference / folio-report hyperlinks for this
// page. The geometry is rebuilt from the QPdfWriter (not a QPrinter):
// render() anchors the diagram top-left with KeepAspectRatio, and the
// page is sized to the diagram so the scale is ~1.
if (auto *engine = dynamic_cast<QPdfEngine *>(painter.paintEngine())) {
const QRectF source(r);
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());
// Device pixels -> PDF points, replicating the engine's page matrix
// (72/resolution scale + Y flip; zero margins -> no paint offset).
const qreal pt_scale = 72.0 / writer.resolution();
const qreal fullH_pt = writer.pageLayout().fullRectPoints().height();
const bool fullPageMode =
(writer.pageLayout().mode() == QPageLayout::FullPageMode);
const QRect paintPx =
writer.pageLayout().paintRectPixels(writer.resolution());
PdfLinks::PageGeometry geom;
geom.sceneToDevice = fit;
geom.target = target;
geom.pageBounds = QRectF(0, 0, target.width(), target.height());
geom.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);
};
geom.sourceRectOf = [](Diagram *dg) {
return QRectF(diagramRect(dg));
};
PdfLinks::injectCrossRefLinks(engine, diagram, geom, pageMap, output);
}
}
painter.end();
// Rewrite the URI link annotations into native internal GoTo actions, so
// the cross-references jump inside the document in any PDF viewer.
PdfLinks::convertUriToGoTo(output);
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;
}
/// Stamp title-block fields onto every folio (and the project default), then
/// save. Each assignment is "key=value". Standard keys map to the documented
/// title-block fields; "date=today" uses the current date; any other key is
/// stored as a custom title-block field. Aimed at CI/revision workflows
/// (e.g. set revision + date before exporting a new revision).
int setTitleBlock(QETProject &project, const QString &output,
const QStringList &assignments)
{
if (assignments.isEmpty()) {
err << "No field assignments given (expected key=value).\n";
return 2;
}
// Parse "key=value" assignments up front so a bad one fails before writing.
QList<QPair<QString, QString>> fields;
for (const QString &a : assignments) {
const int eq = a.indexOf('=');
if (eq <= 0) {
err << "Bad assignment '" << a << "' (expected key=value).\n";
return 2;
}
const QString key = a.left(eq);
const QString val = a.mid(eq + 1);
if (key.compare("date", Qt::CaseInsensitive) == 0
&& val.compare("today", Qt::CaseInsensitive) != 0
&& !QDate::fromString(val, Qt::ISODate).isValid()) {
err << "Bad date '" << val << "' (expected YYYY-MM-DD or 'today').\n";
return 2;
}
fields << qMakePair(key, val);
}
auto apply = [&](TitleBlockProperties &p) {
for (const auto &f : fields) {
const QString k = f.first.toLower();
const QString &v = f.second;
if (k == "title") p.title = v;
else if (k == "author") p.author = v;
else if (k == "filename") p.filename = v;
else if (k == "plant") p.plant = v;
else if (k == "location") p.locmach = v;
else if (k == "revision") p.indexrev = v;
else if (k == "version") p.version = v;
else if (k == "date") {
p.date = (v.compare("today", Qt::CaseInsensitive) == 0)
? QDate::currentDate()
: QDate::fromString(v, Qt::ISODate);
// An explicit date is only honoured when the folio is in
// "use the date value" mode (not "now"/"null").
p.useDate = TitleBlockProperties::UseDateValue;
}
else // unknown key -> custom title-block field
p.context.addValue(f.first, v);
}
};
// Project default (the template applied to new folios).
TitleBlockProperties def = project.defaultTitleBlockProperties();
apply(def);
project.setDefaultTitleBlockProperties(def);
// Every existing folio's own title block.
int folios = 0;
const QList<Diagram *> diagrams = project.diagrams();
for (Diagram *diagram : diagrams) {
TitleBlockProperties p =
diagram->border_and_titleblock.exportTitleBlock();
apply(p);
diagram->border_and_titleblock.importTitleBlock(p);
++folios;
}
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 << "Stamped " << fields.size() << " field(s) on "
<< folios << " folio(s) -> " << 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);
if (format == "settb")
return setTitleBlock(project, output, rest.mid(2));
return exportImages(project, format, output);
}
} // namespace CLIExport
+79
View File
@@ -0,0 +1,79 @@
/*
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>
qelectrotech --set-titleblock <project.qet> <output.qet> key=value...
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).
set-titleblock: stamp title-block fields onto every folio, then save.
Keys: title, author, date (or date=today), plant, location,
revision, version, filename; any other key becomes a custom
field. E.g. --set-titleblock in.qet out.qet revision=B date=today
*/
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();
+11 -1
View File
@@ -124,7 +124,17 @@ void ElementDialog::setUpWidget()
} else if (m_mode == SaveTemplate) { } else if (m_mode == SaveTemplate) {
m_text_field->setPlaceholderText(tr("Nom du nouveau template")); m_text_field->setPlaceholderText(tr("Nom du nouveau template"));
} else { } else {
m_text_field->setPlaceholderText(tr("Nom du nouvel élément")); // This is the element's file name, not its display name: the field
// only accepts file-name characters (QFileNameEdit). The visible
// element name is edited separately in the element properties.
m_text_field->setPlaceholderText(
tr("Nom de fichier de l'élément",
"placeholder: the element's file name, not its display name"));
m_text_field->setToolTip(
tr("Nom de fichier de l'élément : chiffres, minuscules, « - », "
"« _ » et « . » uniquement.\nLe nom affiché de l'élément se "
"modifie séparément dans les propriétés de l'élément.",
"tooltip for the element file-name field"));
} }
layout->addWidget(m_text_field); layout->addWidget(m_text_field);
+60 -3
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
@@ -59,7 +59,8 @@ ElementsPanelWidget::ElementsPanelWidget(QWidget *parent) : QWidget(parent) {
prj_close = new QAction(QET::Icons::DocumentClose, tr("Fermer ce projet"), this); prj_close = new QAction(QET::Icons::DocumentClose, tr("Fermer ce projet"), this);
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,7 +449,8 @@ void ElementsPanelWidget::updateButtons()
} }
prj_del_diagram -> setEnabled(is_writable); prj_del_diagram -> setEnabled(is_writable);
prj_move_diagram_up -> setEnabled(is_writable && min_position > 0); prj_duplicate_diagram -> setEnabled(is_writable);
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();
+21
View File
@@ -15,13 +15,17 @@
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"
#include "qetproject.h"
#include "singleapplication.h" #include "singleapplication.h"
#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 +198,23 @@ 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);
// No crash-recovery backups in one-shot CLI mode: the backup write
// runs on a background thread referencing the project and races the
// process exit (intermittent segfault in QET::writeToFile).
QETProject::setBackupEnabled(false);
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
+382
View File
@@ -0,0 +1,382 @@
/*
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 "pdf_links.h"
#include "diagram.h"
#include "qetgraphicsitem/crossrefitem.h"
#include "qetgraphicsitem/dynamicelementtextitem.h"
#include "qetgraphicsitem/element.h"
#include "qetgraphicsitem/elementtextitemgroup.h"
// Private Qt PDF engine for drawHyperlink() — not public API, stable since Qt4.
// Requires QT += gui-private in qelectrotech.pro / gui-private in CMake.
#include <private/qpdf_p.h>
#include <QByteArray>
#include <QFile>
#include <QGraphicsTextItem>
#include <QList>
#include <QRegularExpression>
#include <QUrl>
#include <QVector>
namespace PdfLinks {
void injectCrossRefLinks(QPdfEngine *engine, Diagram *diagram,
const PageGeometry &geom,
const QMap<Diagram *, int> &pageMap,
const QString &outputFileName)
{
if (!engine || !diagram)
return;
const QTransform &fit = geom.sceneToDevice;
const QRectF &target = geom.target;
const QRectF &pageBounds = geom.pageBounds;
// 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 = geom.sourceRectOf(dg);
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 = geom.devToPdf(devT.topLeft());
const QPointF b = geom.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 = pageMap.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(outputFileName);
url.setFragment(frag);
engine->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() is in the CrossRefItem's LOCAL coords -> 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;
}
}
}
void convertUriToGoTo(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();
}
} // namespace PdfLinks
+79
View File
@@ -0,0 +1,79 @@
/*
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 PDF_LINKS_H
#define PDF_LINKS_H
#include <QMap>
#include <QPointF>
#include <QRectF>
#include <QString>
#include <QTransform>
#include <functional>
class QPdfEngine;
class Diagram;
/**
Shared helper that turns a project's cross-references and folio reports
into clickable internal hyperlinks in a Qt-generated PDF. Used by both the
GUI print path (ProjectPrintWindow) and the headless CLI export, each of
which builds its own page geometry and passes it in — this code never
computes the scene-to-page mapping itself.
*/
namespace PdfLinks {
/**
Geometry mapping for one rendered PDF page. Each caller builds this
from its OWN page setup (printer page layout vs QPdfWriter), since the
device-pixel and point conversions differ between them.
*/
struct PageGeometry {
/// scene coordinates -> device pixels (the same "fit" render() applied)
QTransform sceneToDevice;
/// device paint rectangle, in pixels (the page area)
QRectF target;
/// links whose rectangle falls outside this are dropped
QRectF pageBounds;
/// device pixels -> PDF points (replicates the engine's page matrix)
std::function<QPointF(const QPointF &)> devToPdf;
/// a diagram -> its source rectangle in scene pixels (for /FitR framing)
std::function<QRectF(Diagram *)> sourceRectOf;
};
/**
Inject clickable cross-reference / folio-report hyperlinks for @p diagram
into the current page of @p engine. Each link is emitted as a URI
annotation encoding the target page and a /FitR rectangle;
convertUriToGoTo() then rewrites those into native internal GoTo actions.
*/
void injectCrossRefLinks(QPdfEngine *engine, Diagram *diagram,
const PageGeometry &geom,
const QMap<Diagram *, int> &pageMap,
const QString &outputFileName);
/**
Post-process a Qt-generated PDF file: rewrite every "/S /URI" link
annotation into a native internal "/S /GoTo" action (page + /FitR or
/Fit destination) and rebuild the xref table. No-op if the file has no
such annotations.
*/
void convertUriToGoTo(const QString &pdfPath);
}
#endif // PDF_LINKS_H
+130 -4
View File
@@ -18,12 +18,20 @@
#include "projectprintwindow.h" #include "projectprintwindow.h"
#include "../diagram.h" #include "../diagram.h"
#include "../pdf_links.h"
#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 +45,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
@@ -214,13 +227,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 +277,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 +364,65 @@ 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);
};
PdfLinks::PageGeometry geom;
geom.sceneToDevice = fit;
geom.target = target;
geom.pageBounds = pageBounds;
geom.devToPdf = devToPdf;
geom.sourceRectOf = [this](Diagram *dg) {
return QRectF(diagramRect(dg, exportProperties()));
};
PdfLinks::injectCrossRefLinks(
pdfEngine, diagram, geom, diagramPageMap,
printer->outputFileName());
}
}
////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 +878,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();
this->close();
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.
PdfLinks::convertUriToGoTo(pdfFile);
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;
+19 -15
View File
@@ -226,20 +226,20 @@ void QETApp::setLanguage(const QString &desired_language) {
// load translations for the QET application // load translations for the QET application
// charge les traductions pour l'application QET // charge les traductions pour l'application QET
if (!qetTranslator.load("qet_" + desired_language, languages_path)) { // desired_language may be a full locale such as "pt_BR": try that exact
/* in case of failure, // translation, then the base language ("pt"), then fall back to English.
* we fall back on the native channels for French // French is the application's source language and needs no translation.
* en cas d'echec, const QString base_language = desired_language.section('_', 0, 0);
* on retombe sur les chaines natives pour le francais bool loaded = qetTranslator.load("qet_" + desired_language, languages_path);
*/ if (!loaded && base_language != desired_language)
if (desired_language != "fr") { loaded = qetTranslator.load("qet_" + base_language, languages_path);
// use of the English version by default if (!loaded && base_language != "fr") {
// utilisation de la version anglaise par defaut // use of the English version by default
if(!qetTranslator.load("qet_en", languages_path)) // utilisation de la version anglaise par defaut
qWarning() << "failed to load" if(!qetTranslator.load("qet_en", languages_path))
<< "qet_en" << languages_path << "(" << __FILE__ qWarning() << "failed to load"
<< __LINE__ << __FUNCTION__ << ")"; << "qet_en" << languages_path << "(" << __FILE__
} << __LINE__ << __FUNCTION__ << ")";
} }
qApp->installTranslator(&qetTranslator); qApp->installTranslator(&qetTranslator);
@@ -263,7 +263,11 @@ QString QETApp::langFromSetting()
QSettings settings; QSettings settings;
system_language = settings.value("lang", "system").toString(); system_language = settings.value("lang", "system").toString();
if(system_language == "system") { if(system_language == "system") {
system_language = QLocale::system().name().left(2); // Keep the full locale (e.g. "pt_BR"), not just the base language
// ("pt"): QET ships regional translations (pt_BR, nl_BE, nl_NL) and
// truncating here loaded the wrong one. setLanguage() falls back to
// the base language when no regional translation exists.
system_language = QLocale::system().name();
} }
lang_is_set = true; lang_is_set = true;
} }
+13 -14
View File
@@ -928,7 +928,7 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in
bounding_rect = bounding_rect.united(text_rect); bounding_rect = bounding_rect.united(text_rect);
if (m_update_map) if (m_update_map)
m_hovered_contacts_map.insert(elmt, bounding_rect); m_hovered_contacts_map.insert(elmt, text_rect);
++m_drawed_contacts; ++m_drawed_contacts;
} }
@@ -961,22 +961,21 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in
painter.drawPolyline(p2, 3); painter.drawPolyline(p2, 3);
// Draw terminal names for switch contact (3 terminals) // Draw terminal names for switch contact (3 terminals)
// terminal_names[0] = NO side (top left) // terminal_names[0] = NC (bottom-left)
// terminal_names[1] = NC side (bottom left) // terminal_names[1] = NO (top-left)
// terminal_names[2] = common side (right) // terminal_names[2] = Common (right)
if (!terminal_names.isEmpty() && m_properties.showTerminalName()) { if (!terminal_names.isEmpty() && m_properties.showTerminalName()) {
painter.setFont(QETApp::diagramTextsFont(4)); painter.setFont(QETApp::diagramTextsFont(4));
// Sort order from parseTerminal (top->bottom, left->right): // Storage order set above: [0]=NC, [1]=NO, [2]=Common
// [0]=12 (NO, top-left), [1]=14 (common, top-center), [2]=13 (NC, bottom-center)
if (terminal_names.size() >= 1)
painter.drawText(QRectF(0, offset, 8, 8),
Qt::AlignLeft|Qt::AlignTop, terminal_names[1]); // 12 NO left
if (terminal_names.size() >= 2) if (terminal_names.size() >= 2)
painter.drawText(QRectF(16, offset+4, 8, 6), painter.drawText(QRectF(0, offset, 8, 8),
Qt::AlignRight|Qt::AlignTop, terminal_names[2]); // 14 common right Qt::AlignLeft|Qt::AlignTop, terminal_names[1]); // NO top-left
if (terminal_names.size() >= 3) 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), painter.drawText(QRectF(0, offset+9, 8, 6),
Qt::AlignLeft|Qt::AlignTop, terminal_names[0]); // 13 NC left-bottom Qt::AlignLeft|Qt::AlignTop, terminal_names[0]); // NC bottom-left
painter.setFont(QETApp::diagramTextsFont(5)); painter.setFont(QETApp::diagramTextsFont(5));
} }
@@ -1012,7 +1011,7 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in
bounding_rect = bounding_rect.united(text_rect); bounding_rect = bounding_rect.united(text_rect);
if (m_update_map) if (m_update_map)
m_hovered_contacts_map.insert(elmt, bounding_rect); m_hovered_contacts_map.insert(elmt, text_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;
@@ -1044,7 +1043,7 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in
bounding_rect = bounding_rect.united(text_rect); bounding_rect = bounding_rect.united(text_rect);
if (m_update_map) if (m_update_map)
m_hovered_contacts_map.insert(elmt, bounding_rect); m_hovered_contacts_map.insert(elmt, text_rect);
++m_drawed_contacts; ++m_drawed_contacts;
} }
return bounding_rect; return bounding_rect;
+6
View File
@@ -126,6 +126,12 @@ class CrossRefItem : public QGraphicsObject
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
@@ -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();
@@ -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);
+6
View File
@@ -788,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;
+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;
} }
+27 -4
View File
@@ -43,6 +43,13 @@
static int BACKUP_INTERVAL = 1200000; //interval in ms of backup = 20min static int BACKUP_INTERVAL = 1200000; //interval in ms of backup = 20min
bool QETProject::m_backup_enabled = true;
void QETProject::setBackupEnabled(bool enabled)
{
m_backup_enabled = enabled;
}
/** /**
@brief QETProject::QETProject @brief QETProject::QETProject
Create a empty project Create a empty project
@@ -1006,16 +1013,30 @@ QETResult QETProject::write()
if (m_file_path.isEmpty()) if (m_file_path.isEmpty())
return(QString("unable to save project to file: no filepath was specified")); return(QString("unable to save project to file: no filepath was specified"));
// if the project was opened read-only // If the project was opened read-only, only refuse when the target
// and the file is still non-writable, do not save the project // really can't be written: an existing file that is not writable, or a
if (isReadOnly() && !QFileInfo(m_file_path).isWritable()) // new file (e.g. "Save As" to another location) whose directory is not
return(QString("the file %1 was opened read-only and thus will not be written").arg(m_file_path)); // writable. A non-existent file reports isWritable() == false, so the
// old check wrongly blocked saving a read-only project elsewhere.
if (isReadOnly()) {
const QFileInfo file_info(m_file_path);
const bool can_write = file_info.exists()
? file_info.isWritable()
: QFileInfo(file_info.absolutePath()).isWritable();
if (!can_write)
return(QString("the file %1 was opened read-only and thus will not be written").arg(m_file_path));
}
QDomDocument xml_project(toXml()); QDomDocument xml_project(toXml());
QString error_message; QString error_message;
if (!QET::writeXmlFile(xml_project, m_file_path, &error_message)) if (!QET::writeXmlFile(xml_project, m_file_path, &error_message))
return(error_message); return(error_message);
// The project has just been written to a writable file (e.g. saved to
// a new location with "Save As"), so it is no longer read-only.
if (isReadOnly())
setReadOnly(false);
//title block variables should be updated after file save dialog is confirmed, before file is saved. //title block variables should be updated after file save dialog is confirmed, before file is saved.
m_project_properties.addValue("saveddate", QLocale::system().toString(QDate::currentDate(), QLocale::ShortFormat)); m_project_properties.addValue("saveddate", QLocale::system().toString(QDate::currentDate(), QLocale::ShortFormat));
m_project_properties.addValue("saveddate-us", QDate::currentDate().toString("yyyy-MM-dd")); m_project_properties.addValue("saveddate-us", QDate::currentDate().toString("yyyy-MM-dd"));
@@ -1783,6 +1804,8 @@ void QETProject::addDiagram(Diagram *diagram, int pos)
*/ */
void QETProject::writeBackup() void QETProject::writeBackup()
{ {
if (!m_backup_enabled)
return;
#ifdef BUILD_WITHOUT_KF5 #ifdef BUILD_WITHOUT_KF5
#else #else
# if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove # if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
+8
View File
@@ -105,6 +105,12 @@ class QETProject : public QObject
QVersionNumber declaredQElectroTechVersion(); QVersionNumber declaredQElectroTechVersion();
void setTitle(const QString &); void setTitle(const QString &);
/// Enable/disable the asynchronous crash-recovery backup for all
/// projects. Disabled by the headless CLI: the backup write runs on a
/// background thread referencing the project, and a short-lived CLI
/// process can destroy the project before the write finishes (crash).
static void setBackupEnabled(bool enabled);
///DEFAULT PROPERTIES ///DEFAULT PROPERTIES
BorderProperties defaultBorderProperties() const; BorderProperties defaultBorderProperties() const;
void setDefaultBorderProperties(const BorderProperties &); void setDefaultBorderProperties(const BorderProperties &);
@@ -241,6 +247,8 @@ class QETProject : public QObject
// attributes // attributes
private: private:
/// When false, writeBackup() is a no-op (set by the headless CLI)
static bool m_backup_enabled;
/// File path this project is saved to /// File path this project is saved to
QString m_file_path; QString m_file_path;
/// Current state of the project /// Current state of the project
+11 -6
View File
@@ -1756,6 +1756,10 @@ QString TitleBlockTemplate::interpreteVariables(
QStringList TitleBlockTemplate::listOfVariables() QStringList TitleBlockTemplate::listOfVariables()
{ {
QStringList list; QStringList list;
// Match every "%{name}" placeholder. The bare "%name" form can't be
// extracted reliably without the variable list, and templates use the
// braced form, so only that is collected here.
static const QRegularExpression rx(QStringLiteral("%\\{([^}]+)\\}"));
// run through each individual cell // run through each individual cell
for (int j = 0 ; j < rows_heights_.count() ; ++ j) { for (int j = 0 ; j < rows_heights_.count() ; ++ j) {
for (int i = 0 ; i < columns_width_.count() ; ++ i) { for (int i = 0 ; i < columns_width_.count() ; ++ i) {
@@ -1763,14 +1767,15 @@ QStringList TitleBlockTemplate::listOfVariables()
|| cells_[i][j] -> cell_type || cells_[i][j] -> cell_type
== TitleBlockCell::EmptyCell) == TitleBlockCell::EmptyCell)
continue; continue;
#if TODO_LIST const QString cell_value = cells_[i][j] -> value.name();
#pragma message("@TODO not works on all cases...") auto it = rx.globalMatch(cell_value);
#endif while (it.hasNext()) {
// TODO: not works on all cases... const QString name = it.next().captured(1);
list << cells_[i][j] -> value.name().replace("%",""); if (!name.isEmpty() && !list.contains(name))
list << name;
}
} }
} }
qDebug() << list;
return list; return list;
} }
+56 -7
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,12 +303,11 @@ 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())
{ {
QString txt{eipw->text()}; QString txt{eipw->text()};
//remove line feed and carriage return //remove line feed and carriage return
txt.remove(QStringLiteral("\r")); txt.remove(QStringLiteral("\r"));
txt.remove(QStringLiteral("\n")); txt.remove(QStringLiteral("\n"));
info_.addValue(eipw->key(), txt); info_.addValue(eipw->key(), txt);
@@ -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;
}; };
+58 -16
View File
@@ -24,6 +24,7 @@
#include "ui_titleblockpropertieswidget.h" #include "ui_titleblockpropertieswidget.h"
#include <QMenu> #include <QMenu>
#include <QSet>
#include <utility> #include <utility>
/** /**
@@ -162,7 +163,11 @@ void TitleBlockPropertiesWidget::setProperties(
} }
ui -> m_tbt_cb -> setCurrentIndex(index); ui -> m_tbt_cb -> setCurrentIndex(index);
m_dcw -> setContext(properties.context); // Show the saved custom values, plus any of the template's custom variables
// that aren't defined yet, so the user only fills in the missing ones (#271).
DiagramContext context = properties.context;
addTemplateVariables(context, index);
m_dcw -> setContext(context);
} }
/** /**
@@ -435,12 +440,15 @@ void TitleBlockPropertiesWidget::updateTemplateList()
} }
/** /**
@brief TitleBlockPropertiesWidget::changeCurrentTitleBlockTemplate @brief TitleBlockPropertiesWidget::templateForIndex
Load the additional field of title block "text" @param index : index in the collection-type map (= the template combo index)
@return the TitleBlockTemplate currently selected for that collection, or
nullptr.
*/ */
void TitleBlockPropertiesWidget::changeCurrentTitleBlockTemplate(int index) TitleBlockTemplate *TitleBlockPropertiesWidget::templateForIndex(int index) const
{ {
m_dcw -> clear(); if (index < 0 || index >= m_map_index_to_collection_type.count())
return nullptr;
QET::QetCollection qc = m_map_index_to_collection_type.at(index); QET::QetCollection qc = m_map_index_to_collection_type.at(index);
TitleBlockTemplatesCollection *collection = nullptr; TitleBlockTemplatesCollection *collection = nullptr;
@@ -448,21 +456,55 @@ void TitleBlockPropertiesWidget::changeCurrentTitleBlockTemplate(int index)
if (c -> collection() == qc) if (c -> collection() == qc)
collection = c; collection = c;
if (!collection) return; if (!collection) return nullptr;
return collection -> getTemplate(ui -> m_tbt_cb -> currentText());
}
// get template /**
TitleBlockTemplate *tpl = collection -> getTemplate(ui -> m_tbt_cb -> currentText()); @brief TitleBlockPropertiesWidget::addTemplateVariables
if(tpl != nullptr) { Add to @p context every CUSTOM variable used by the currently selected
// get all template fields template that is not already present, with an empty value — so the user
QStringList fields = tpl -> listOfVariables(); only has to fill in the values instead of declaring the variables (#271).
// set fields to additional_fields_ widget The standard fields (title, author, date, …) are handled by their own
DiagramContext templateContext; widgets and are skipped. Existing values in @p context are preserved.
for(int i =0; i<fields.count(); i++) */
templateContext.addValue(fields.at(i), ""); void TitleBlockPropertiesWidget::addTemplateVariables(
m_dcw -> setContext(templateContext); DiagramContext &context, int index) const
{
TitleBlockTemplate *tpl = templateForIndex(index);
if (!tpl) return;
// Variables rendered from the dedicated standard-field widgets; they must
// not appear in the "Custom" tab.
static const QSet<QString> reserved {
QStringLiteral("author"), QStringLiteral("date"),
QStringLiteral("title"), QStringLiteral("filename"),
QStringLiteral("plant"), QStringLiteral("locmach"),
QStringLiteral("indexrev"), QStringLiteral("version"),
QStringLiteral("folio"), QStringLiteral("folio-id"),
QStringLiteral("folio-total"), QStringLiteral("auto_page_num"),
QStringLiteral("previous-folio-num"), QStringLiteral("next-folio-num")
};
const QStringList variables = tpl -> listOfVariables();
for (const QString &name : variables) {
if (name.isEmpty() || reserved.contains(name)) continue;
if (!context.contains(name)) context.addValue(name, "");
} }
} }
/**
@brief TitleBlockPropertiesWidget::changeCurrentTitleBlockTemplate
When the user picks a template, append its missing custom variables to the
"Custom" tab while keeping the values already entered (#271).
*/
void TitleBlockPropertiesWidget::changeCurrentTitleBlockTemplate(int index)
{
DiagramContext context = m_dcw -> context();
addTemplateVariables(context, index);
m_dcw -> setContext(context);
}
/** /**
@brief TitleBlockPropertiesWidget::on_m_date_now_pb_clicked @brief TitleBlockPropertiesWidget::on_m_date_now_pb_clicked
Set the date to current date Set the date to current date
+3
View File
@@ -30,6 +30,7 @@ class NumerotationContext;
class QETProject; class QETProject;
class QMenu; class QMenu;
class TitleBlockTemplatesCollection; class TitleBlockTemplatesCollection;
class TitleBlockTemplate;
namespace Ui { namespace Ui {
class TitleBlockPropertiesWidget; class TitleBlockPropertiesWidget;
@@ -77,6 +78,8 @@ class TitleBlockPropertiesWidget : public QWidget
void initDialog(const bool &current_date, QETProject *project); void initDialog(const bool &current_date, QETProject *project);
int getIndexFor (const QString &tbt_name, int getIndexFor (const QString &tbt_name,
const QET::QetCollection collection) const; const QET::QetCollection collection) const;
TitleBlockTemplate *templateForIndex (int index) const;
void addTemplateVariables (DiagramContext &context, int index) const;
private slots: private slots:
void editCurrentTitleBlockTemplate(); void editCurrentTitleBlockTemplate();
+32 -21
View File
@@ -151,13 +151,39 @@ 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;
} }
QFileDialog dialog(m_parent);
dialog.setAcceptMode(QFileDialog::AcceptSave);
dialog.setWindowTitle(tr("Exporter le plan de câblage"));
dialog.setDefaultSuffix("csv");
dialog.setNameFilter(tr("Fichiers CSV (*.csv)"));
if (dialog.exec() != QDialog::Accepted) return;
QString fileName = dialog.selectedFiles().first();
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::warning(m_parent, tr("Erreur"), tr("Impossible d'ouvrir le fichier pour l'écriture."));
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; QSet<QString> conductorDefinitionTypes;
QDomElement rootElem = doc.documentElement(); QDomElement rootElem = doc.documentElement();
QDomElement collection = rootElem.firstChildElement("collection"); QDomElement collection = rootElem.firstChildElement("collection");
@@ -197,21 +223,6 @@ void WiringListExport::toCsv()
} }
} }
QFileDialog dialog(m_parent);
dialog.setAcceptMode(QFileDialog::AcceptSave);
dialog.setWindowTitle(tr("Exporter le plan de câblage"));
dialog.setDefaultSuffix("csv");
dialog.setNameFilter(tr("Fichiers CSV (*.csv)"));
if (dialog.exec() != QDialog::Accepted) return;
QString fileName = dialog.selectedFiles().first();
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::warning(m_parent, tr("Erreur"), tr("Impossible d'ouvrir le fichier pour l'écriture."));
return;
}
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());
@@ -353,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") << ";"
@@ -376,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;