Compare commits

..

70 Commits

Author SHA1 Message Date
Laurent Trinques 918db632ed Update windows-msi.yml 2026-06-13 10:56:07 +02:00
Laurent Trinques f465b33e2b Update windows-msi.yml 2026-06-13 10:45:41 +02:00
Laurent Trinques 492528136e Update windows-msi.yml 2026-06-13 10:40:37 +02:00
Laurent Trinques 564a0e64a4 Update windows-msi.yml 2026-06-13 10:31:58 +02:00
Laurent Trinques 7f5a42a055 Update windows-msi.yml 2026-06-13 10:14:05 +02:00
Laurent Trinques 14d4aa772b Update windows-msi.yml 2026-06-13 09:59:17 +02:00
Laurent Trinques 81419bd27d Merge pull request #498 from ispyisail/fix-lang-path-fallback
Find translations when lang/ is beside bin/, not inside it (fixes #86)
2026-06-13 04:49:14 +02:00
Laurent Trinques ebefc269af Update Changelog file 2026-06-12 19:43:57 +02:00
Laurent Trinques c6bfd46981 Merge pull request #501 from ispyisail/fix-parttext-position-158
PartText: keep text position stable across save/reopen on font-size change (#158)
2026-06-12 16:49:26 +02:00
Laurent Trinques 998c5e8a0d Update translations files 2026-06-12 13:28:08 +02:00
Shane Ringrose ae382f6b12 parttext: re-anchor on font change so text position is stable across save/reopen (#158) 2026-06-12 23:05:15 +12: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 e63c303078 Find translations when lang/ is beside the bin/ folder, not inside it (#86)
QETApp::languagesPath() defaulted to applicationDirPath() + "/lang/".
The Windows installer puts the executable in a bin/ subfolder while the
lang/ folder sits next to it (../lang), so that default points at a
non-existent bin/lang/ — qetTranslator.load() fails and setLanguage()
silently falls back to the French source language. This is the root
cause behind the long-standing 'language won't change / resets to
French' reports, and why launching via 'Lancer QET.bat' (which passes
--lang-dir=lang/) works around it.

When the folder next to the binary doesn't exist, fall back to the
sibling ../lang folder if present. Behaviour is unchanged for builds
that already ship lang/ next to the binary, and for the QET_LANG_PATH
and --lang-dir paths.

Fixes #86.
2026-06-12 09:09:39 +12: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
129 changed files with 10697 additions and 7758 deletions
+130 -74
View File
@@ -18,7 +18,6 @@ jobs:
build-msi: build-msi:
name: Build MSI with WiX v7 name: Build MSI with WiX v7
runs-on: windows-latest runs-on: windows-latest
# Only runs if Windows Build succeeded (or triggered manually) # Only runs if Windows Build succeeded (or triggered manually)
if: > if: >
github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_dispatch' ||
@@ -27,10 +26,9 @@ jobs:
permissions: permissions:
contents: write contents: write
pages: write pages: write
id-token: write id-token: write # Required by SignPath
steps: steps:
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 1. Checkout (to retrieve QElectroTech.wxs and sources) # 1. Checkout (to retrieve QElectroTech.wxs and sources)
# ---------------------------------------------------------------- # ----------------------------------------------------------------
@@ -41,12 +39,16 @@ jobs:
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 2. Download the portable artifact from the main build # 2. Download the portable artifact from the main build
# Requires windows-build.yml to upload an artifact named
# "qelectrotech-windows-portable" (fixed name)
# ---------------------------------------------------------------- # ----------------------------------------------------------------
- name: Download portable artifact - name: Download portable artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: qelectrotech-windows-portable name: qelectrotech-windows-portable
path: artifact\files path: artifact\files
# workflow_run => use the triggering run's ID
# workflow_dispatch => use input run_id if provided, otherwise current run
run-id: ${{ github.event.workflow_run.id || github.event.inputs.run_id || github.run_id }} run-id: ${{ github.event.workflow_run.id || github.event.inputs.run_id || github.run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }} repository: ${{ github.repository }}
@@ -58,10 +60,12 @@ jobs:
id: version id: version
shell: pwsh shell: pwsh
run: | run: |
# Version from qetversion.cpp (same logic as windows-build.yml)
$src = Get-Content "sources\qetversion.cpp" -Raw -ErrorAction SilentlyContinue $src = Get-Content "sources\qetversion.cpp" -Raw -ErrorAction SilentlyContinue
if ($src -match 'return QVersionNumber\{([^}]+)\}') { if ($src -match 'return QVersionNumber\{([^}]+)\}') {
$ver = $Matches[1] -replace '\s','' -replace ',','.' $ver = $Matches[1] -replace '\s','' -replace ',','.'
} else { } else {
# Fallback: CMakeLists.txt
$cmake = Get-Content "CMakeLists.txt" -Raw $cmake = Get-Content "CMakeLists.txt" -Raw
if ($cmake -match 'project\s*\([^)]*VERSION\s+([\d]+\.[\d]+\.[\d]+)') { if ($cmake -match 'project\s*\([^)]*VERSION\s+([\d]+\.[\d]+\.[\d]+)') {
$ver = $Matches[1] $ver = $Matches[1]
@@ -69,42 +73,52 @@ jobs:
$ver = "0.0.0" $ver = "0.0.0"
} }
} }
# Numeric MSI version: 4 digits required (e.g. 0.100.1.0)
$verMsi = "$ver.0" $verMsi = "$ver.0"
# Short SHA for the display version
$sha = git rev-parse --short HEAD 2>$null $sha = git rev-parse --short HEAD 2>$null
if (-not $sha) { $sha = "unknown" } if (-not $sha) { $sha = "unknown" }
# Cumulative revision number (same calculation as windows-build.yml)
$count = git rev-list HEAD --count 2>$null $count = git rev-list HEAD --count 2>$null
$rev = [int]$count + 473 $rev = [int]$count + 473
$verDisplay = "${ver}-r${rev}-${sha}_x86_64-win64"
# Generate a unique ProductCode GUID from the commit SHA
# This ensures MajorUpgrade always triggers, even for same-version builds
$fullSha = git rev-parse HEAD 2>$null
if (-not $fullSha) { $fullSha = [System.Guid]::NewGuid().ToString() }
$bytes = [System.Text.Encoding]::UTF8.GetBytes($fullSha)
$md5 = [System.Security.Cryptography.MD5]::Create().ComputeHash($bytes)
$guidBytes = [byte[]]$md5[0..15]
$productGuid = [System.Guid]::new($guidBytes).ToString().ToUpper()
echo "VERSION_MSI=$verMsi" >> $env:GITHUB_OUTPUT $verDisplay = "${ver}-r${rev}-${sha}_x86_64-win64"
echo "VERSION_MSI=$verMsi" >> $env:GITHUB_OUTPUT
echo "VERSION_DISPLAY=$verDisplay" >> $env:GITHUB_OUTPUT echo "VERSION_DISPLAY=$verDisplay" >> $env:GITHUB_OUTPUT
echo "PRODUCT_GUID=$productGuid" >> $env:GITHUB_OUTPUT
Write-Host "Version MSI : $verMsi" Write-Host "Version MSI : $verMsi"
Write-Host "Version display : $verDisplay" Write-Host "Version display : $verDisplay"
Write-Host "Product GUID : $productGuid"
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 4. Install WiX v7, accept EULA and install WixUI extension # 4. Install WiX v7, accept EULA and install WixUI extension
# All done in one step: PATH is updated within the same step
# so wix is immediately available for eula and extension commands
# ---------------------------------------------------------------- # ----------------------------------------------------------------
- name: Install WiX v7 - name: Install WiX v7
shell: pwsh shell: pwsh
run: | run: |
dotnet tool install --global wix --version 7.0.0 dotnet tool install --global wix --version 7.0.0
# Update PATH immediately for the rest of this step
$toolsPath = [System.IO.Path]::Combine($env:USERPROFILE, '.dotnet', 'tools') $toolsPath = [System.IO.Path]::Combine($env:USERPROFILE, '.dotnet', 'tools')
$env:PATH = "$toolsPath;$env:PATH" $env:PATH = "$toolsPath;$env:PATH"
# Also export for subsequent steps
echo $toolsPath >> $env:GITHUB_PATH echo $toolsPath >> $env:GITHUB_PATH
# Accept OSMF EULA (official CI/CD method: writes a sentinel file)
wix eula accept wix7 wix eula accept wix7
# Install WixUI extension
wix extension add WixToolset.UI.wixext/7.0.0 wix extension add WixToolset.UI.wixext/7.0.0
# Install WixUtil extension (required for custom actions: Binary:Wix4UtilCA_*)
wix extension add WixToolset.Util.wixext/7.0.0 wix extension add WixToolset.Util.wixext/7.0.0
Write-Host "WiX v7 installed, EULA accepted, UI extension added."
Write-Host "WiX v7 installed, EULA accepted, UI + Util extensions added."
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 5. Check that the WXS file exists in the repository # 5. Check that the WXS file exists in the repository
@@ -115,6 +129,8 @@ jobs:
$wxs = "build-aux\windows\QElectroTech.wxs" $wxs = "build-aux\windows\QElectroTech.wxs"
if (-not (Test-Path $wxs)) { if (-not (Test-Path $wxs)) {
Write-Error "WXS file not found: $wxs" Write-Error "WXS file not found: $wxs"
Write-Host "Contents of build-aux\windows\ :"
Get-ChildItem "build-aux\windows\" -ErrorAction SilentlyContinue
exit 1 exit 1
} }
Write-Host "WXS found: $wxs" Write-Host "WXS found: $wxs"
@@ -129,8 +145,10 @@ jobs:
Get-ChildItem -Path "artifact\files" -Depth 2 | Get-ChildItem -Path "artifact\files" -Depth 2 |
Select-Object FullName | Format-Table -AutoSize Select-Object FullName | Format-Table -AutoSize
# Search for qelectrotech.exe in the artifact
$exe = Get-ChildItem -Path "artifact\files" -Filter "qelectrotech.exe" -Recurse | Select-Object -First 1 $exe = Get-ChildItem -Path "artifact\files" -Filter "qelectrotech.exe" -Recurse | Select-Object -First 1
if (-not $exe) { if (-not $exe) {
# Also try QElectroTech.exe (capital Q)
$exe = Get-ChildItem -Path "artifact\files" -Filter "QElectroTech.exe" -Recurse | Select-Object -First 1 $exe = Get-ChildItem -Path "artifact\files" -Filter "QElectroTech.exe" -Recurse | Select-Object -First 1
} }
if (-not $exe) { if (-not $exe) {
@@ -138,30 +156,40 @@ jobs:
exit 1 exit 1
} }
Write-Host "Executable: $($exe.FullName) ($([math]::Round($exe.Length/1MB,1)) MB)" Write-Host "Executable: $($exe.FullName) ($([math]::Round($exe.Length/1MB,1)) MB)"
$binDir = $exe.Directory.FullName
$filesDir = Split-Path $binDir -Parent # FilesDir = folder containing bin\
$binDir = $exe.Directory.FullName
$filesDir = Split-Path $binDir -Parent
echo "FILES_DIR=$filesDir" >> $env:GITHUB_ENV echo "FILES_DIR=$filesDir" >> $env:GITHUB_ENV
Write-Host "FILES_DIR: $filesDir" Write-Host "FILES_DIR: $filesDir"
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 7. Convert LICENSE (GPL-2) to RTF for the WixUI licence screen # 7. Convert LICENSE (GPL-2) to RTF for the WixUI licence screen
# RTF is the only format accepted by Windows Installer.
# The conversion wraps plain text lines in basic RTF markup.
# ---------------------------------------------------------------- # ----------------------------------------------------------------
- name: Convert LICENSE to RTF - name: Convert LICENSE to RTF
shell: pwsh shell: pwsh
run: | run: |
$licSrc = "LICENSE" $licSrc = "LICENSE"
$licRtf = "$env:TEMP\License.rtf" $licRtf = "$env:TEMP\License.rtf"
if (-not (Test-Path $licSrc)) { if (-not (Test-Path $licSrc)) {
Write-Error "LICENSE file not found in repository root" Write-Error "LICENSE file not found in repository root"
exit 1 exit 1
} }
$lines = Get-Content $licSrc -Encoding UTF8 $lines = Get-Content $licSrc -Encoding UTF8
# RTF header — Courier New, 9pt, black
$rtf = New-Object System.Text.StringBuilder $rtf = New-Object System.Text.StringBuilder
[void]$rtf.AppendLine('{\rtf1\ansi\ansicpg1252\deff0') [void]$rtf.AppendLine('{\rtf1\ansi\ansicpg1252\deff0')
[void]$rtf.AppendLine('{\fonttbl{\f0\fmodern\fprq1\fcharset0 Courier New;}}') [void]$rtf.AppendLine('{\fonttbl{\f0\fmodern\fprq1\fcharset0 Courier New;}}')
[void]$rtf.AppendLine('{\colortbl;\red0\green0\blue0;}') [void]$rtf.AppendLine('{\colortbl;\red0\green0\blue0;}')
[void]$rtf.AppendLine('\f0\fs18\cf1') [void]$rtf.AppendLine('\f0\fs18\cf1')
foreach ($line in $lines) { foreach ($line in $lines) {
# Escape RTF special characters
$escaped = $line ` $escaped = $line `
-replace '\\', '\\\\' ` -replace '\\', '\\\\' `
-replace '\{', '\{' ` -replace '\{', '\{' `
@@ -169,30 +197,29 @@ jobs:
[void]$rtf.AppendLine("$escaped\par") [void]$rtf.AppendLine("$escaped\par")
} }
[void]$rtf.AppendLine('}') [void]$rtf.AppendLine('}')
[System.IO.File]::WriteAllText($licRtf, $rtf.ToString(), [System.Text.Encoding]::ASCII) [System.IO.File]::WriteAllText($licRtf, $rtf.ToString(), [System.Text.Encoding]::ASCII)
echo "LICENSE_RTF=$licRtf" >> $env:GITHUB_ENV echo "LICENSE_RTF=$licRtf" >> $env:GITHUB_ENV
Write-Host "License.rtf generated: $licRtf ($([math]::Round((Get-Item $licRtf).Length/1KB,1)) KB)" Write-Host "License.rtf generated: $licRtf ($([math]::Round((Get-Item $licRtf).Length/1KB,1)) KB)"
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 8. Remove Lancer QET.bat from the artifact # 8. Replace Lancer QET.bat with the MSI-specific version
# The MSI does not use the .bat: shortcuts point directly to # The portable version uses relative paths suited for the zip.
# qelectrotech.exe, and elements\ is set read-only via a # The MSI version uses %~dp0 to resolve paths relative to
# CustomAction in QElectroTech.wxs. # the installation directory in Program Files.
# The .bat is kept as-is in the ZIP portable build.
# ---------------------------------------------------------------- # ----------------------------------------------------------------
- name: Remove Lancer QET.bat from artifact - name: Replace Lancer QET.bat for MSI
shell: pwsh shell: pwsh
run: | run: |
$bat = "$env:FILES_DIR\Lancer QET.bat" $bat = "$env:FILES_DIR\Lancer QET.bat"
if (Test-Path $bat) { $content = "@echo off`r`nstart `"`" `"%~dp0bin\qelectrotech.exe`" --common-elements-dir=`"%~dp0elements/`" --common-tbt-dir=`"%~dp0titleblocks/`" --lang-dir=`"%~dp0lang/`" -style windowsvista`r`n"
Remove-Item $bat -Force [System.IO.File]::WriteAllText($bat, $content, [System.Text.Encoding]::ASCII)
Write-Host "Lancer QET.bat removed from artifact (MSI uses direct exe shortcut)." Write-Host "Lancer QET.bat replaced for MSI installation."
} else { Write-Host "=== Content of new Lancer QET.bat ==="
Write-Host "Lancer QET.bat not found in artifact (already absent)." Get-Content $bat
}
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 9. Build the MSI # 9. Build the MSI (unsigned)
# ---------------------------------------------------------------- # ----------------------------------------------------------------
- name: Build MSI - name: Build MSI
shell: pwsh shell: pwsh
@@ -204,22 +231,31 @@ jobs:
$wxs = "build-aux\windows\QElectroTech.wxs" $wxs = "build-aux\windows\QElectroTech.wxs"
$outputName = "QElectroTech-${verDisplay}.msi" $outputName = "QElectroTech-${verDisplay}.msi"
# Deterministic ProductCode GUID (UUID v5) based on version.
# Same version => same GUID (required for MSI repair/uninstall).
# Different version => different GUID (triggers a proper upgrade).
$seed = "qelectrotech-msi-$version"
$sha1 = [System.Security.Cryptography.SHA1]::Create()
$hash = $sha1.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($seed))
$hash[6] = ($hash[6] -band 0x0F) -bor 0x50 # UUID version 5
$hash[8] = ($hash[8] -band 0x3F) -bor 0x80 # RFC4122 variant
$productCode = "{$([System.Guid]::new([byte[]]$hash[0..15]).ToString().ToUpper())}"
New-Item -ItemType Directory -Force -Path "dist" | Out-Null New-Item -ItemType Directory -Force -Path "dist" | Out-Null
Write-Host "=== wix build ===" Write-Host "=== wix build ==="
Write-Host " WXS : $wxs" Write-Host " WXS : $wxs"
Write-Host " Version : $version" Write-Host " Version : $version"
Write-Host " FilesDir : $filesDir" Write-Host " ProductCode : $productCode"
Write-Host " LicenseRtf : $licRtf" Write-Host " FilesDir : $filesDir"
Write-Host " Output : dist\$outputName" Write-Host " LicenseRtf : $licRtf"
Write-Host " Output : dist\$outputName"
$productGuid = "${{ steps.version.outputs.PRODUCT_GUID }}"
wix build $wxs ` wix build $wxs `
-arch x64 ` -arch x64 `
-d "Version=$version" ` -d "Version=$version" `
-d "ProductVersion=$verDisplay" ` -d "ProductVersion=$verDisplay" `
-d "ProductCode=$productGuid" ` -d "ProductCode=$productCode" `
-d "FilesDir=$filesDir" ` -d "FilesDir=$filesDir" `
-d "LicenseRtf=$licRtf" ` -d "LicenseRtf=$licRtf" `
-ext WixToolset.UI.wixext ` -ext WixToolset.UI.wixext `
@@ -230,40 +266,50 @@ jobs:
Write-Error "MSI not generated: dist\$outputName" Write-Error "MSI not generated: dist\$outputName"
exit 1 exit 1
} }
$size = [math]::Round((Get-Item "dist\$outputName").Length / 1MB, 1) $size = [math]::Round((Get-Item "dist\$outputName").Length / 1MB, 1)
Write-Host "MSI generated: dist\$outputName ($size MB) ✓" Write-Host "MSI generated: dist\$outputName ($size MB) ✓"
echo "MSI_NAME=$outputName" >> $env:GITHUB_ENV echo "MSI_NAME=$outputName" >> $env:GITHUB_ENV
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 10. Sign the MSI with SignPath # 9b. Upload unsigned MSI as artifact (required by SignPath)
# SignPath fetches artifacts directly from GitHub Actions,
# so the MSI must be uploaded before the signing request.
# We capture the artifact-id output for use by SignPath.
# ---------------------------------------------------------------- # ----------------------------------------------------------------
- name: Install SignPath PowerShell module - name: Upload unsigned MSI artifact (pre-signing)
shell: pwsh id: upload_unsigned
run: | uses: actions/upload-artifact@v4
Install-Module -Name SignPath -Force -Scope CurrentUser with:
name: qelectrotech-windows-msi-unsigned
- name: Sign MSI with SignPath path: dist\*.msi
shell: pwsh retention-days: 1
run: | if-no-files-found: error
$msi = Get-ChildItem "$env:GITHUB_WORKSPACE\dist\*.msi" | Select-Object -First 1
if (-not $msi) {
Write-Error "No .msi found in dist/"
exit 1
}
Write-Host "Signing: $($msi.FullName)"
Submit-SigningRequest `
-InputArtifactPath $msi.FullName `
-ApiToken "${{ secrets.SIGNPATH_API_TOKEN }}" `
-OrganizationId "${{ secrets.SIGNPATH_ORGANIZATION_ID }}" `
-ProjectSlug "MSI" `
-SigningPolicySlug "test-signing" `
-OutputArtifactPath $msi.FullName `
-Force `
-WaitForCompletion
Write-Host "Signing complete: $($msi.Name)"
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 11. Upload the MSI artifact # 9c. Submit signing request to SignPath (OSS organization)
# Prerequisites:
# - SIGNPATH_API_TOKEN : CI user token from the OSS org
# - SIGNPATH_ORGANIZATION_ID : Organization ID of the OSS org
# (visible in app.signpath.io → Settings after accepting
# the OSS invitation)
# The action downloads the signed MSI and places it in
# dist/ (overwriting the unsigned one).
# ----------------------------------------------------------------
- name: Sign MSI via SignPath
uses: signpath/github-action-submit-signing-request@v1
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
project-slug: 'qelectrotech-source-mirror'
signing-policy-slug: 'test-signing'
artifact-configuration-slug: 'MSI'
github-artifact-id: ${{ steps.upload_unsigned.outputs.artifact-id }}
wait-for-completion: true
output-artifact-directory: 'dist\'
# ----------------------------------------------------------------
# 10. Upload the signed MSI artifact
# ---------------------------------------------------------------- # ----------------------------------------------------------------
- name: Upload MSI artifact - name: Upload MSI artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -284,9 +330,9 @@ jobs:
gh release view nightly --repo "$REPO" --json assets \ gh release view nightly --repo "$REPO" --json assets \
--jq '.assets[] | select(.name | test("\\.msi$")) | .name' \ --jq '.assets[] | select(.name | test("\\.msi$")) | .name' \
| while read -r name; do | while read -r name; do
echo "Deleting old asset: $name" echo "Deleting old asset: $name"
gh release delete-asset nightly "$name" --repo "$REPO" --yes gh release delete-asset nightly "$name" --repo "$REPO" --yes
done done
echo "Old .msi assets deleted." echo "Old .msi assets deleted."
shell: bash shell: bash
@@ -307,18 +353,20 @@ jobs:
shell: pwsh shell: pwsh
run: | run: |
Write-Host "=== MSI build summary ===" Write-Host "=== MSI build summary ==="
Write-Host "Version : ${{ steps.version.outputs.VERSION_DISPLAY }}" Write-Host "Version : ${{ steps.version.outputs.VERSION_DISPLAY }}"
Write-Host "WiX : v7.0.0" Write-Host "WiX : v7.0.0"
Write-Host "Runner image : ${{ runner.os }} / ${{ runner.arch }}" Write-Host "Signing : SignPath OSS"
Write-Host "Runner : ${{ runner.os }} / ${{ runner.arch }}"
if (Test-Path "dist\$env:MSI_NAME") { if (Test-Path "dist\$env:MSI_NAME") {
$size = [math]::Round((Get-Item "dist\$env:MSI_NAME").Length / 1MB, 1) $size = [math]::Round((Get-Item "dist\$env:MSI_NAME").Length / 1MB, 1)
Write-Host "MSI : $env:MSI_NAME ($size MB) ✓" Write-Host "MSI : $env:MSI_NAME ($size MB) ✓"
} else { } else {
Write-Host "MSI : FAILED ✗" Write-Host "MSI : FAILED ✗"
} }
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 13. Generate and deploy the GitHub Pages download page # 13. Generate and deploy the GitHub Pages download page
# Toutes les URLs sont connues ici (exe, zip, msi).
# ---------------------------------------------------------------- # ----------------------------------------------------------------
- name: Checkout (for generate-page.py) - name: Checkout (for generate-page.py)
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -334,23 +382,31 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
set -euo pipefail set -euo pipefail
REPO="${{ github.repository }}" REPO="${{ github.repository }}"
# Fetch asset names from the nightly release (source of truth)
ASSETS=$(gh release view nightly --repo "$REPO" --json assets --jq '.assets[].name') ASSETS=$(gh release view nightly --repo "$REPO" --json assets --jq '.assets[].name')
EXE_NAME=$(echo "$ASSETS" | grep '\.exe$' | head -1) EXE_NAME=$(echo "$ASSETS" | grep '\.exe$' | head -1)
ZIP_NAME=$(echo "$ASSETS" | grep '\.zip$' | head -1) ZIP_NAME=$(echo "$ASSETS" | grep '\.zip$' | head -1)
MSI_NAME=$(echo "$ASSETS" | grep '\.msi$' | head -1 || echo "") MSI_NAME=$(echo "$ASSETS" | grep '\.msi$' | head -1 || echo "")
BASE="https://github.com/$REPO/releases/download/nightly" BASE="https://github.com/$REPO/releases/download/nightly"
INSTALLER_URL="$BASE/$EXE_NAME" INSTALLER_URL="$BASE/$EXE_NAME"
PORTABLE_URL="$BASE/$ZIP_NAME" PORTABLE_URL="$BASE/$ZIP_NAME"
MSI_URL="" MSI_URL=""
[ -n "$MSI_NAME" ] && MSI_URL="$BASE/$MSI_NAME" [ -n "$MSI_NAME" ] && MSI_URL="$BASE/$MSI_NAME"
SHA="${{ github.event.workflow_run.head_sha || github.sha }}" SHA="${{ github.event.workflow_run.head_sha || github.sha }}"
SHORT="${SHA:0:7}" SHORT="${SHA:0:7}"
DATE=$(date -u '+%Y-%m-%d %H:%M UTC') DATE=$(date -u '+%Y-%m-%d %H:%M UTC')
RUN_URL="https://github.com/$REPO/actions/runs/${{ github.run_id }}" RUN_URL="https://github.com/$REPO/actions/runs/${{ github.run_id }}"
RUN_NUMBER="${{ github.run_number }}" RUN_NUMBER="${{ github.run_number }}"
export DATE SHORT REPO SHA RUN_URL RUN_NUMBER export DATE SHORT REPO SHA RUN_URL RUN_NUMBER
export INSTALLER_URL PORTABLE_URL MSI_URL export INSTALLER_URL PORTABLE_URL MSI_URL
python3 source/build-aux/generate-page.py python3 source/build-aux/generate-page.py
- name: Add .nojekyll - name: Add .nojekyll
+118 -94
View File
@@ -14,30 +14,25 @@
# 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(cmake/hoto_update_cmake_message.cmake)
cmake_minimum_required(VERSION 3.5...4.2) cmake_minimum_required(VERSION 3.5...4.2)
project(qelectrotech project(qelectrotech
VERSION 0.100.0 VERSION 0.100.1
DESCRIPTION "QET is a CAD/CAE editor focusing on schematics drawing features." DESCRIPTION "QET is a CAD/CAE editor focusing on schematics drawing features."
HOMEPAGE_URL "https://qelectrotech.org/" HOMEPAGE_URL "https://qelectrotech.org/"
LANGUAGES CXX) LANGUAGES CXX)
include(cmake/copyright_message.cmake) include(cmake/copyright_message.cmake)
set(QET_DIR ${PROJECT_SOURCE_DIR}) set(QET_DIR ${PROJECT_SOURCE_DIR})
include(cmake/qet_compilation_vars.cmake)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS ${QET_COMPONENTS})
qt_standard_project_setup()
# Add sub directories # Add sub directories
option(PACKAGE_TESTS "Build the tests" NO) option(PACKAGE_TESTS "Build the tests" ON)
if(PACKAGE_TESTS) if(PACKAGE_TESTS)
message("Add sub directory tests") message("Add sub directory tests")
add_subdirectory(tests) add_subdirectory(tests)
endif() endif()
include(cmake/paths_compilation_installation.cmake) include(cmake/paths_compilation_installation.cmake)
@@ -48,98 +43,127 @@ include(cmake/git_last_commit_sha.cmake)
include(cmake/fetch_kdeaddons.cmake) include(cmake/fetch_kdeaddons.cmake)
include(cmake/fetch_singleapplication.cmake) include(cmake/fetch_singleapplication.cmake)
include(cmake/fetch_pugixml.cmake) include(cmake/fetch_pugixml.cmake)
include(cmake/qet_compilation_vars.cmake)
set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
SET(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOUIC_SEARCH_PATHS ${QET_DIR}/sources/ui) find_package(
QT
NAMES
Qt5
COMPONENTS
${QET_COMPONENTS}
REQUIRED
)
include(cmake/define_definitions.cmake) find_package(
Qt${QT_VERSION_MAJOR}
qt_add_executable( COMPONENTS
${PROJECT_NAME} ${QET_COMPONENTS}
${QET_RES_FILES} REQUIRED)
${QET_SRC_FILES}
${QM_FILES}
${QET_DIR}/qelectrotech.qrc
)
if(QMFILES_AS_RESOURCE)
qt_add_translations(${PROJECT_NAME} TS_FILES ${TS_FILES} RESOURCE_PREFIX "/lang")
else()
qt_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES})
set_source_files_properties(${TS_FILES} PROPERTIES OUTPUT_LOCATION "${QET_DIR}/lang")
qt_add_translation(QM_FILES ${TS_FILES})
endif()
find_package(SQLite3 REQUIRED) find_package(SQLite3 REQUIRED)
set(CMAKE_AUTOUIC_SEARCH_PATHS ${QET_DIR}/sources/ui)
qt5_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES})
set_source_files_properties(${TS_FILES} PROPERTIES OUTPUT_LOCATION "${QET_DIR}/lang")
qt5_add_translation(QM_FILES ${TS_FILES})
# als laatse
include(cmake/define_definitions.cmake)
# On Windows, WIN32 sets /SUBSYSTEM:WINDOWS to suppress the console window.
# Qt automatically links qtmain.lib which provides the WinMain entry point,
# so no source code change is needed.
if(WIN32)
add_executable(
${PROJECT_NAME}
WIN32
${QET_RES_FILES}
${QET_SRC_FILES}
${QM_FILES}
${QET_DIR}/qelectrotech.qrc
)
else()
add_executable(
${PROJECT_NAME}
${QET_RES_FILES}
${QET_SRC_FILES}
${QM_FILES}
${QET_DIR}/qelectrotech.qrc
)
endif()
target_link_libraries( target_link_libraries(
${PROJECT_NAME} ${PROJECT_NAME}
PUBLIC PUBLIC
PRIVATE PRIVATE
pugixml::pugixml pugixml::pugixml
SingleApplication::SingleApplication SingleApplication::SingleApplication
SQLite::SQLite3 SQLite::SQLite3
${KF6_PRIVATE_LIBRARIES} ${KF5_PRIVATE_LIBRARIES}
${QET_PRIVATE_LIBRARIES} ${QET_PRIVATE_LIBRARIES}
) )
target_include_directories( target_include_directories(
${PROJECT_NAME} ${PROJECT_NAME}
PRIVATE PRIVATE
${QET_DIR}/sources/titleblock ${QET_DIR}/sources/titleblock
${QET_DIR}/sources/ui ${QET_DIR}/sources/ui
${QET_DIR}/sources/qetgraphicsitem ${QET_DIR}/sources/qetgraphicsitem
${QET_DIR}/sources/qetgraphicsitem/ViewItem ${QET_DIR}/sources/qetgraphicsitem/ViewItem
${QET_DIR}/sources/qetgraphicsitem/ViewItem/ui ${QET_DIR}/sources/qetgraphicsitem/ViewItem/ui
${QET_DIR}/sources/richtext ${QET_DIR}/sources/richtext
${QET_DIR}/sources/factory ${QET_DIR}/sources/factory
${QET_DIR}/sources/properties ${QET_DIR}/sources/properties
${QET_DIR}/sources/dvevent ${QET_DIR}/sources/dvevent
${QET_DIR}/sources/editor ${QET_DIR}/sources/editor
${QET_DIR}/sources/editor/esevent ${QET_DIR}/sources/editor/esevent
${QET_DIR}/sources/editor/graphicspart ${QET_DIR}/sources/editor/graphicspart
${QET_DIR}/sources/editor/ui ${QET_DIR}/sources/editor/ui
${QET_DIR}/sources/editor/UndoCommand ${QET_DIR}/sources/editor/UndoCommand
${QET_DIR}/sources/undocommand ${QET_DIR}/sources/undocommand
${QET_DIR}/sources/diagramevent ${QET_DIR}/sources/diagramevent
${QET_DIR}/sources/ElementsCollection ${QET_DIR}/sources/ElementsCollection
${QET_DIR}/sources/ElementsCollection/ui ${QET_DIR}/sources/ElementsCollection/ui
${QET_DIR}/sources/autoNum ${QET_DIR}/sources/autoNum
${QET_DIR}/sources/autoNum/ui ${QET_DIR}/sources/autoNum/ui
${QET_DIR}/sources/ui/configpage ${QET_DIR}/sources/ui/configpage
${QET_DIR}/sources/SearchAndReplace ${QET_DIR}/sources/SearchAndReplace
${QET_DIR}/sources/SearchAndReplace/ui ${QET_DIR}/sources/SearchAndReplace/ui
${QET_DIR}/sources/NameList ${QET_DIR}/sources/NameList
${QET_DIR}/sources/NameList/ui ${QET_DIR}/sources/NameList/ui
${QET_DIR}/sources/utils ${QET_DIR}/sources/utils
${QET_DIR}/sources/dataBase ${QET_DIR}/pugixml/src
${QET_DIR}/sources/dataBase/ui ${QET_DIR}/sources/dataBase
${QET_DIR}/sources/factory/ui ${QET_DIR}/sources/dataBase/ui
${QET_DIR}/sources/print ${QET_DIR}/sources/factory/ui
${QET_DIR}/sources/svg ${QET_DIR}/sources/print
) ${QET_DIR}/sources/svg
)
install(TARGETS ${PROJECT_NAME}) install(TARGETS ${PROJECT_NAME})
if (NOT MINGW)
install(DIRECTORY ico/breeze-icons/16x16 DESTINATION ${QET_ICONS_PATH}) if (NOT MINGW)
install(DIRECTORY ico/breeze-icons/22x22 DESTINATION ${QET_ICONS_PATH}) install(DIRECTORY ico/breeze-icons/16x16 DESTINATION ${QET_ICONS_PATH})
install(DIRECTORY ico/breeze-icons/32x32 DESTINATION ${QET_ICONS_PATH}) install(DIRECTORY ico/breeze-icons/22x22 DESTINATION ${QET_ICONS_PATH})
install(DIRECTORY ico/breeze-icons/48x48 DESTINATION ${QET_ICONS_PATH}) install(DIRECTORY ico/breeze-icons/32x32 DESTINATION ${QET_ICONS_PATH})
install(DIRECTORY ico/breeze-icons/64x64 DESTINATION ${QET_ICONS_PATH}) install(DIRECTORY ico/breeze-icons/48x48 DESTINATION ${QET_ICONS_PATH})
install(DIRECTORY ico/breeze-icons/128x128 DESTINATION ${QET_ICONS_PATH}) install(DIRECTORY ico/breeze-icons/64x64 DESTINATION ${QET_ICONS_PATH})
install(DIRECTORY ico/breeze-icons/256x256 DESTINATION ${QET_ICONS_PATH}) install(DIRECTORY ico/breeze-icons/128x128 DESTINATION ${QET_ICONS_PATH})
install(DIRECTORY elements DESTINATION share/qelectrotech) install(DIRECTORY ico/breeze-icons/256x256 DESTINATION ${QET_ICONS_PATH})
install(DIRECTORY examples DESTINATION share/qelectrotech) install(DIRECTORY elements DESTINATION share/qelectrotech)
install(DIRECTORY titleblocks DESTINATION share/qelectrotech) install(DIRECTORY examples DESTINATION share/qelectrotech)
install(FILES LICENSE ELEMENTS.LICENSE CREDIT README ChangeLog DESTINATION share/doc/qelectrotech) install(DIRECTORY titleblocks DESTINATION share/qelectrotech)
install(FILES misc/org.qelectrotech.qelectrotech.desktop DESTINATION share/applications) install(FILES LICENSE ELEMENTS.LICENSE CREDIT README ChangeLog DESTINATION share/doc/qelectrotech)
install(FILES misc/qelectrotech.xml DESTINATION share/mime/packages) install(FILES misc/org.qelectrotech.qelectrotech.desktop DESTINATION share/applications)
install(FILES misc/qelectrotech.appdata.xml DESTINATION ${QET_APPDATA_PATH}) install(FILES misc/qelectrotech.xml DESTINATION share/mime/packages)
if(NOT QMFILES_AS_RESOURCE) install(FILES misc/qelectrotech.appdata.xml DESTINATION ${QET_APPDATA_PATH})
install(FILES ${QM_FILES} DESTINATION ${QET_LANG_PATH}) install(FILES ${QM_FILES} DESTINATION ${QET_LANG_PATH})
endif()
endif() endif()
+91 -2
View File
@@ -2,13 +2,102 @@
## [Unreleased](https://github.com/qelectrotech/qelectrotech-source-mirror/tree/HEAD) ## [Unreleased](https://github.com/qelectrotech/qelectrotech-source-mirror/tree/HEAD)
[Full Changelog](https://github.com/qelectrotech/qelectrotech-source-mirror/compare/0.9...HEAD) [Full Changelog](https://github.com/qelectrotech/qelectrotech-source-mirror/compare/nightly...HEAD)
**Closed issues:**
- “Exclude from auto-numbering” and “Potential isolation” tick boxes fault [\#482](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/482)
- Add ability to change appearance of multiple lines at once [\#476](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/476)
- Save As dialog asks for "element name" but actually requires a file name [\#469](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/469)
- Some icons are illegible with dark theme [\#466](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/466)
- \[BUG\] Properties to all conductors [\#460](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/460)
- Internal links in PDF export [\#417](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/417)
- Apple silicon download is not working [\#400](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/400)
- Add missing title block variables to 'folio properties' window [\#271](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/271)
- Bug: Saving a read-only project doesn't clear the read-only state [\#217](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/217)
**Merged pull requests:**
- PartText: keep text position stable across save/reopen on font-size change \(\#158\) [\#501](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/501) ([ispyisail](https://github.com/ispyisail))
- Clear read-only state when a project is saved to a writable file [\#497](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/497) ([ispyisail](https://github.com/ispyisail))
- Fix regional system locale loading the wrong translation \(pt\_BR/nl\_BE/nl\_NL\) [\#496](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/496) ([ispyisail](https://github.com/ispyisail))
- Folio properties: auto-add a title block's custom variables [\#495](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/495) ([ispyisail](https://github.com/ispyisail))
- Element editor Save As: label the field as a file name, not 'element name' [\#494](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/494) ([ispyisail](https://github.com/ispyisail))
- CLI: add --set-titleblock, and fix headless backup crash [\#493](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/493) ([ispyisail](https://github.com/ispyisail))
- Update qet\_zh.ts [\#491](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/491) ([zi-mozhuang](https://github.com/zi-mozhuang))
- CLI: clickable cross-reference hyperlinks in PDF export [\#490](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/490) ([ispyisail](https://github.com/ispyisail))
- CLI: add verification & data-export tools \(info, BOM, nets, links, check-elements, resave\) [\#489](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/489) ([ispyisail](https://github.com/ispyisail))
- Issues 482 [\#488](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/488) ([Kellermorph](https://github.com/Kellermorph))
- Revert "Update-UI-Chinese-translation" [\#486](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/486) ([scorpio810](https://github.com/scorpio810))
- CLI export: don't draw the editor grid in PDF/PNG/SVG output [\#485](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/485) ([ispyisail](https://github.com/ispyisail))
- Update-UI-Chinese-translation [\#484](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/484) ([zi-mozhuang](https://github.com/zi-mozhuang))
- Add headless command-line export \(PDF / PNG / SVG / cable-list / wire-list\) [\#483](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/483) ([ispyisail](https://github.com/ispyisail))
- fix possible crashes in crossrefitem [\#479](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/479) ([ChuckNr11](https://github.com/ChuckNr11))
- Fix: Dynamic element text shifting/jumping when duplicating diagrams [\#477](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/477) ([Kellermorph](https://github.com/Kellermorph))
- Update German translations for duplicate diagram feature [\#475](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/475) ([Kellermorph](https://github.com/Kellermorph))
- Feat: Add ability to duplicate diagrams/folios with all metadata and … [\#473](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/473) ([Kellermorph](https://github.com/Kellermorph))
- Feature: Allow excluding specific elements from BOM \(Nomenclature\) [\#472](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/472) ([Kellermorph](https://github.com/Kellermorph))
- Potential Isolation option for terminals [\#471](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/471) ([Kellermorph](https://github.com/Kellermorph))
- Fix: Wiring list filter and dynamic text timing [\#470](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/470) ([Kellermorph](https://github.com/Kellermorph))
- New element: Line definition [\#464](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/464) ([Kellermorph](https://github.com/Kellermorph))
- Turkish Lang Update [\#463](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/463) ([scorpio810](https://github.com/scorpio810))
- follow up: wiring list [\#462](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/462) ([Kellermorph](https://github.com/Kellermorph))
- Fix and Improve Multi-selection for Diagram Operations [\#459](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/459) ([Kellermorph](https://github.com/Kellermorph))
## [nightly](https://github.com/qelectrotech/qelectrotech-source-mirror/tree/nightly) (2026-05-10)
[Full Changelog](https://github.com/qelectrotech/qelectrotech-source-mirror/compare/0.100...nightly)
**Closed issues:**
- Flatpak runtimes outdated [\#446](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/446)
- Move Flemish man pages from man/be/ to man/nl\_BE/ [\#439](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/439)
- you could share an PR to fix it? [\#438](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/438)
- Diacritics in some filenames can possibly lead to the problems during packaging [\#437](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/437)
- שרטוט חשמל [\#435](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/435)
- Feature request: add circuit simulation [\#432](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/432)
- No usable sources archive for version 0.100 [\#418](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/418)
- New release ? [\#411](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/411)
- options moving when opening "file", "edition" menus [\#299](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/299)
**Merged pull requests:**
- Try to add Windows build CI workflow [\#457](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/457) ([scorpio810](https://github.com/scorpio810))
- Fixed: Prevented the selection in the project tree from jumping to the last page when saving. [\#456](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/456) ([Kellermorph](https://github.com/Kellermorph))
- Fix Thumbnail in Makrotree [\#455](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/455) ([Kellermorph](https://github.com/Kellermorph))
- Update German translation for macro feature [\#454](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/454) ([Kellermorph](https://github.com/Kellermorph))
- Fix losing Focus on moving diagram position with keyboard [\#452](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/452) ([ChuckNr11](https://github.com/ChuckNr11))
- Draft: Feature - Introduce User Templates Collection and Dedicated UI Tab [\#451](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/451) ([Kellermorph](https://github.com/Kellermorph))
- Update translation [\#450](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/450) ([Kellermorph](https://github.com/Kellermorph))
- Automatic Terminal Numbering Tool [\#449](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/449) ([Kellermorph](https://github.com/Kellermorph))
- Supplement to pull request \#444 by Kellermorph [\#448](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/448) ([ChuckNr11](https://github.com/ChuckNr11))
- Add RAM-based wiring list export [\#447](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/447) ([Kellermorph](https://github.com/Kellermorph))
- Follow-up: Address review comments for slave limit feature [\#444](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/444) ([Kellermorph](https://github.com/Kellermorph))
- Feature: Auto-select active diagram in the elements panel tree [\#443](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/443) ([Kellermorph](https://github.com/Kellermorph))
- Revert "Feature: Implement max\_slaves limit for Master elements" [\#442](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/442) ([scorpio810](https://github.com/scorpio810))
- Feature: Implement max\_slaves limit for Master elements [\#441](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/441) ([Kellermorph](https://github.com/Kellermorph))
- Update qet\_cs.ts [\#434](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/434) ([pafri](https://github.com/pafri))
- Update QCH Help file [\#433](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/433) ([Int-Circuit](https://github.com/Int-Circuit))
- Create Korean man page for QElectroTech [\#431](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/431) ([Kyle-Code-CA](https://github.com/Kyle-Code-CA))
- Update ELEMENTS.LICENSE [\#430](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/430) ([Kyle-Code-CA](https://github.com/Kyle-Code-CA))
- Update CREDIT [\#429](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/429) ([Kyle-Code-CA](https://github.com/Kyle-Code-CA))
- Add Korean comments to QElectroTech XML file [\#428](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/428) ([Kyle-Code-CA](https://github.com/Kyle-Code-CA))
- Add Spanish and Korean summaries to appdata [\#427](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/427) ([Kyle-Code-CA](https://github.com/Kyle-Code-CA))
- Add Korean translations for comments and generic names [\#426](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/426) ([Kyle-Code-CA](https://github.com/Kyle-Code-CA))
- Restore copyright and license information in QET64.nsi [\#425](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/425) ([Kyle-Code-CA](https://github.com/Kyle-Code-CA))
- Add Korean language strings to lang\_extra.nsh [\#424](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/424) ([Kyle-Code-CA](https://github.com/Kyle-Code-CA))
- Add Korean translation author to aboutqetdialog [\#423](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/423) ([Kyle-Code-CA](https://github.com/Kyle-Code-CA))
- Add Korean language support in xml element collection [\#422](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/422) ([Kyle-Code-CA](https://github.com/Kyle-Code-CA))
- Add Korean translation \(ko\) translated by jkh [\#419](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/419) ([Kyle-Code-CA](https://github.com/Kyle-Code-CA))
## [0.100](https://github.com/qelectrotech/qelectrotech-source-mirror/tree/0.100) (2026-01-25)
[Full Changelog](https://github.com/qelectrotech/qelectrotech-source-mirror/compare/0.9...0.100)
**Closed issues:** **Closed issues:**
- error in doxygen action code [\#414](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/414) - error in doxygen action code [\#414](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/414)
- "NoName" is automatically inserted into empty text cells in title block [\#407](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/407) - "NoName" is automatically inserted into empty text cells in title block [\#407](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/407)
- Apple silicon download is not working [\#400](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/400)
- Apple silicon download is not working [\#394](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/394) - Apple silicon download is not working [\#394](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/394)
- Differenciating connector for proper labeling [\#390](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/390) - Differenciating connector for proper labeling [\#390](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/390)
- Non-perpendicular connections [\#368](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/368) - Non-perpendicular connections [\#368](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/368)
-3
View File
@@ -59,9 +59,6 @@ Here are the technical choices made for the software development:
If you wish to be informed of the latest developments, browse the [archive](https://listengine.tuxfamily.org/lists.tuxfamily.org/qet/) of the project mailing list where all commits (changes) are registered. This archive is publicly available, you don't need any account to access it. If you wish to be informed of the latest developments, browse the [archive](https://listengine.tuxfamily.org/lists.tuxfamily.org/qet/) of the project mailing list where all commits (changes) are registered. This archive is publicly available, you don't need any account to access it.
### Build QElectroTech under Windows
To build QElectroTech under microsoft Windows, please follow [these instructions (french)](md/fr/fr_window_build_summary.md)
# Features # Features
+1
Submodule SingleApplication added at cbe496bc01
+5 -10
View File
@@ -62,15 +62,10 @@ message("PROJECT_SOURCE_DIR :" ${PROJECT_SOURCE_DIR})
message("QET_DIR :" ${QET_DIR}) message("QET_DIR :" ${QET_DIR})
message("GIT_COMMIT_SHA :" ${GIT_COMMIT_SHA}) message("GIT_COMMIT_SHA :" ${GIT_COMMIT_SHA})
if(BUILD_WITH_KF6 AND BUILD_KF6) if(BUILD_WITH_KF5)
message("KF6_GIT_TAG :" ${KF6_GIT_TAG}) message("KF5_GIT_TAG :" ${KF5_GIT_TAG})
endif() else()
if(NOT BUILD_WITH_KF6) add_definitions(-DBUILD_WITHOUT_KF5)
add_definitions(-DBUILD_WITHOUT_KF6)
endif() endif()
message("QET_COMPONENTS :" ${QET_COMPONENTS}) message("QET_COMPONENTS :" ${QET_COMPONENTS})
message("Qt version :" ${Qt6_VERSION}) message("QT_VERSION_MAJOR :" ${QT_VERSION_MAJOR})
if(QMFILES_AS_RESOURCE)
add_definitions(-DQMFILES_AS_RESOURCE)
endif()
+2 -5
View File
@@ -31,8 +31,5 @@ add_definitions(-DQT_MESSAGELOGCONTEXT)
# In order to do so, uncomment the following line. # In order to do so, uncomment the following line.
#add_definitions(-DTODO_LIST) #add_definitions(-DTODO_LIST)
# Build with KF6 # Build with KF5
option(BUILD_WITH_KF6 "Build with KF6" ON) option(BUILD_WITH_KF5 "Build with KF5" ON)
# Use translations as a Qt resource
option(QMFILES_AS_RESOURCE "Use .qm files as Qt resource" ON)
+38 -26
View File
@@ -1,4 +1,4 @@
# Copyright 2006-2026 The QElectroTech Team # Copyright 2006 The QElectroTech Team
# This file is part of QElectroTech. # This file is part of QElectroTech.
# #
# QElectroTech is free software: you can redistribute it and/or modify # QElectroTech is free software: you can redistribute it and/or modify
@@ -14,42 +14,54 @@
# 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/>.
option(BUILD_KF6 "Build KF6 libraries, use system ones otherwise" OFF) message(" - fetch_kdeaddons")
if(BUILD_KF6)
block(PROPAGATE KF6_GIT_TAG)
message(STATUS " - fetch_kdeaddons")
set(KDE_SKIP_TEST_SETTINGS ON)
set(KCOREADDONS_USE_QML OFF)
set(KWIDGETSADDONS_USE_QML OFF)
set(BUILD_TESTING OFF)
set(BUILD_DESIGNERPLUGIN OFF)
set(BUILD_QCH OFF)
set(BUILD_SHARED_LIBS OFF)
Include(FetchContent) if(DEFINED BUILD_WITH_KF5)
Include(FetchContent)
if(NOT DEFINED KF6_GIT_TAG) option(BUILD_KF5 "Build KF5 libraries, use system ones otherwise" YES)
set(KF6_GIT_TAG v6.22.0)
if(BUILD_KF5)
if(NOT DEFINED KF5_GIT_TAG)
#https://qelectrotech.org/forum/viewtopic.php?pid=13924#p13924
set(KF5_GIT_TAG v5.77.0)
endif() endif()
# Fix stop the run autotests of kcoreaddons
# see
# https://invent.kde.org/frameworks/kcoreaddons/-/blob/master/CMakeLists.txt#L98
# issue:
# CMake Error at /usr/share/ECM/modules/ECMAddTests.cmake:89 (add_executable):
# Cannot find source file:
# see
# https://qelectrotech.org/forum/viewtopic.php?pid=13929#p13929
set(KDE_SKIP_TEST_SETTINGS "TRUE")
set(BUILD_TESTING "0")
FetchContent_Declare(
ecm
GIT_REPOSITORY https://invent.kde.org/frameworks/extra-cmake-modules.git
GIT_TAG ${KF5_GIT_TAG})
FetchContent_MakeAvailable(ecm)
FetchContent_Declare( FetchContent_Declare(
kcoreaddons kcoreaddons
GIT_REPOSITORY https://invent.kde.org/frameworks/kcoreaddons.git GIT_REPOSITORY https://invent.kde.org/frameworks/kcoreaddons.git
GIT_TAG ${KF6_GIT_TAG}) GIT_TAG ${KF5_GIT_TAG})
FetchContent_MakeAvailable(kcoreaddons) FetchContent_MakeAvailable(kcoreaddons)
FetchContent_Declare( FetchContent_Declare(
kwidgetsaddons kwidgetsaddons
GIT_REPOSITORY https://invent.kde.org/frameworks/kwidgetsaddons.git GIT_REPOSITORY https://invent.kde.org/frameworks/kwidgetsaddons.git
GIT_TAG ${KF6_GIT_TAG}) GIT_TAG ${KF5_GIT_TAG})
FetchContent_MakeAvailable(kwidgetsaddons) FetchContent_MakeAvailable(kwidgetsaddons)
endblock() else()
else() find_package(KF5CoreAddons REQUIRED)
find_package(KF6CoreAddons REQUIRED) find_package(KF5WidgetsAddons REQUIRED)
find_package(KF6WidgetsAddons REQUIRED) endif()
endif()
set(KF6_PRIVATE_LIBRARIES set(KF5_PRIVATE_LIBRARIES
KF6::CoreAddons KF5::WidgetsAddons
KF6::WidgetsAddons KF5::CoreAddons
) )
endif()
+8 -4
View File
@@ -1,4 +1,4 @@
# Copyright 2006-2026 The QElectroTech Team # Copyright 2006 The QElectroTech Team
# This file is part of QElectroTech. # This file is part of QElectroTech.
# #
# QElectroTech is free software: you can redistribute it and/or modify # QElectroTech is free software: you can redistribute it and/or modify
@@ -14,10 +14,14 @@
# 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/>.
option(BUILD_PUGIXML "Build pugixml library, use system one otherwise" OFF) message(" - fetch_pugixml")
Include(FetchContent)
option(BUILD_PUGIXML "Build pugixml library, use system one otherwise" YES)
if(BUILD_PUGIXML) if(BUILD_PUGIXML)
Include(FetchContent)
message(" - fetch pugixml")
FetchContent_Declare( FetchContent_Declare(
pugixml pugixml
GIT_REPOSITORY https://github.com/zeux/pugixml.git GIT_REPOSITORY https://github.com/zeux/pugixml.git
+5 -2
View File
@@ -1,4 +1,4 @@
# Copyright 2006-2026 The QElectroTech Team # Copyright 2006 The QElectroTech Team
# This file is part of QElectroTech. # This file is part of QElectroTech.
# #
# QElectroTech is free software: you can redistribute it and/or modify # QElectroTech is free software: you can redistribute it and/or modify
@@ -16,6 +16,9 @@
message(" - fetch_singleapplication") message(" - fetch_singleapplication")
# https://github.com/itay-grudev/SingleApplication/issues/18
#qmake
#DEFINES += QAPPLICATION_CLASS=QGuiApplication
set(QAPPLICATION_CLASS QApplication) set(QAPPLICATION_CLASS QApplication)
Include(FetchContent) Include(FetchContent)
@@ -23,6 +26,6 @@ Include(FetchContent)
FetchContent_Declare( FetchContent_Declare(
SingleApplication SingleApplication
GIT_REPOSITORY https://github.com/itay-grudev/SingleApplication.git GIT_REPOSITORY https://github.com/itay-grudev/SingleApplication.git
GIT_TAG v3.5.4) GIT_TAG v3.2.0)
FetchContent_MakeAvailable(SingleApplication) FetchContent_MakeAvailable(SingleApplication)
+25
View File
@@ -0,0 +1,25 @@
# Copyright 2006 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/>.
if(${CMAKE_VERSION} VERSION_LESS 3.14)
message(
"_____________________________________________________________________")
message("to update Cmake on linux:")
message("https://github.com/Kitware/CMake/")
message("linux => cmake-3.19.1-Linux-x86_64.sh")
message(" sudo ./cmake.sh --prefix=/usr/local/ --exclude-subdir")
message("windows good luck :)")
endif()
+12 -10
View File
@@ -17,8 +17,6 @@
message(" - qet_compilation_vars") message(" - qet_compilation_vars")
set(QET_COMPONENTS set(QET_COMPONENTS
Core
Gui
LinguistTools LinguistTools
PrintSupport PrintSupport
Xml Xml
@@ -31,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
@@ -108,16 +107,14 @@ 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
${QET_DIR}/sources/bordertitleblock.h ${QET_DIR}/sources/bordertitleblock.h
# ${QET_DIR}/sources/colorbutton.cpp
# ${QET_DIR}/sources/colorbutton.h
# ${QET_DIR}/sources/colorcombobox.cpp
# ${QET_DIR}/sources/colorcombobox.h
# ${QET_DIR}/sources/colorcomboboxdelegate.cpp
# ${QET_DIR}/sources/colorcomboboxdelegate.h
${QET_DIR}/sources/conductorautonumerotation.cpp ${QET_DIR}/sources/conductorautonumerotation.cpp
${QET_DIR}/sources/conductorautonumerotation.h ${QET_DIR}/sources/conductorautonumerotation.h
${QET_DIR}/sources/conductornumexport.cpp ${QET_DIR}/sources/conductornumexport.cpp
@@ -426,6 +423,10 @@ set(QET_SRC_FILES
${QET_DIR}/sources/PropertiesEditor/propertieseditorwidget.cpp ${QET_DIR}/sources/PropertiesEditor/propertieseditorwidget.cpp
${QET_DIR}/sources/PropertiesEditor/propertieseditorwidget.h ${QET_DIR}/sources/PropertiesEditor/propertieseditorwidget.h
${QET_DIR}/pugixml/src/pugiconfig.hpp
${QET_DIR}/pugixml/src/pugixml.cpp
${QET_DIR}/pugixml/src/pugixml.hpp
${QET_DIR}/sources/qetgraphicsitem/conductor.cpp ${QET_DIR}/sources/qetgraphicsitem/conductor.cpp
${QET_DIR}/sources/qetgraphicsitem/conductor.h ${QET_DIR}/sources/qetgraphicsitem/conductor.h
${QET_DIR}/sources/qetgraphicsitem/conductortextitem.cpp ${QET_DIR}/sources/qetgraphicsitem/conductortextitem.cpp
@@ -503,7 +504,6 @@ 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.cpp
${QET_DIR}/sources/svg/qetsvg.h ${QET_DIR}/sources/svg/qetsvg.h
@@ -742,17 +742,19 @@ set(TS_FILES
${QET_DIR}/lang/qet_mn.ts ${QET_DIR}/lang/qet_mn.ts
${QET_DIR}/lang/qet_nb.ts ${QET_DIR}/lang/qet_nb.ts
${QET_DIR}/lang/qet_nl.ts ${QET_DIR}/lang/qet_nl.ts
${QET_DIR}/lang/qet_nl_BE.ts ${QET_DIR}/lang/qet_nl_BE.ts
${QET_DIR}/lang/qet_no.ts ${QET_DIR}/lang/qet_no.ts
${QET_DIR}/lang/qet_pl.ts ${QET_DIR}/lang/qet_pl.ts
${QET_DIR}/lang/qet_pt.ts ${QET_DIR}/lang/qet_pt.ts
${QET_DIR}/lang/qet_pt_BR.ts ${QET_DIR}/lang/qet_pt_BR.ts
${QET_DIR}/lang/qet_ro.ts ${QET_DIR}/lang/qet_ro.ts
${QET_DIR}/lang/qet_rs.ts
${QET_DIR}/lang/qet_ru.ts ${QET_DIR}/lang/qet_ru.ts
${QET_DIR}/lang/qet_sk.ts ${QET_DIR}/lang/qet_sk.ts
${QET_DIR}/lang/qet_sl.ts ${QET_DIR}/lang/qet_sl.ts
${QET_DIR}/lang/qet_sr.ts ${QET_DIR}/lang/qet_sr.ts
${QET_DIR}/lang/qet_sv.ts ${QET_DIR}/lang/qet_sv.ts
${QET_DIR}/lang/qet_tr.ts ${QET_DIR}/lang/qet_tr.ts
${QET_DIR}/lang/qet_uk.ts
${QET_DIR}/lang/qet_zh.ts ${QET_DIR}/lang/qet_zh.ts
) )
+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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

-101
View File
@@ -1,101 +0,0 @@
Compiler QElectroTech sous microsoft Windows 10 et 11 avec MSYS2
================================
Ce document décrit les étapes nécessaire afin de compilé QElectroTech sous Windows avec Qt6 et cmake en utilisant MSYS2.
# MSYS2
L'ensemble des outils nécessaire au développement et à la compilation de QElectroTech sous Windows sera installé par lintermédiaire de [MSYS2](https://www.msys2.org/). Cela comprend entre autre le framework [Qt6](https://www.qt.io/development/qt-framework/qt6), les outils cmake, les dépendances ([kde framework](https://develop.kde.org/docs/), [sqlite](https://sqlite.org/), [pugixml](https://pugixml.org/)), les outils de compilation [minGW](https://www.mingw-w64.org/)...
>Il sera nécessaire d'utiliser [winget](https://learn.microsoft.com/fr-fr/windows/package-manager/winget/), celui-ci est présent par défaut sous Windows 11, dans le cas de Windows 10, winget peut necessité d'être activé manuellement
# Installer GIT et MSYS2 avec winget
Avec power shell.
```
winget install Git.Git
```
puis
```
winget install MSYS2.MSYS2
```
## Mise à jour de MSYS2
Lors de la première utilisation de MSYS2 il est nécessaire de mettre celui-ci à jour.
Lancer "MSYS2 MSYS" depuis le menu démarré de Windows.
Une fenêtre avec un shell s'ouvre, dans celui-ci lancer la commande :
```
pacman -Syu
```
A la fin de la mise à jour MSYS2 MSYS se fermera automatiquement. Ouvrez le à nouveau et relancé la commande
```
pacman -Syu
```
## Installation des outils de devellopement
Toujours dans le shell MSYS2 MSYS lancer la commande suivante.
```
pacman -S mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-qt6-svg mingw-w64-ucrt-x86_64-qt6-base mingw-w64-ucrt-x86_64-sqlite3 mingw-w64-ucrt-x86_64-pugixml mingw-w64-ucrt-x86_64-kcoreaddons mingw-w64-ucrt-x86_64-kwidgetsaddons mingw-w64-ucrt-x86_64-extra-cmake-modules mingw-w64-ucrt-x86_64-gdb mingw-w64-ucrt-x86_64-qt6-translations mingw-w64-ucrt-x86_64-qt6-tools
```
> La quantité de paquets à installer est conséquent, en fonction de votre connexion internet cela peut prendre plusieurs dizaine de minute
L'ensemble des outils est mantenant installé 😀
# Installer Qt creator
Télécharger [l'installateur online de Qt](https://www.qt.io/development/download-qt-installer-oss) et lancer l'installation en suivant les indications de ce dernier.
>Dans le cas où vous comptez utilisé Qt Creator uniquement pour développez QElectroTech, lors de l'installation choisissez l'option "installation personnalisée" puis dans la page suivante sélectionné uniquement Qt Creator.
## Configurer Qt creator
Ouvrir Qt creator puis rendez vous dans "édition -> préférence -> kit"
### Versions de Qt
- Cliquer sur _ajouter_
- Renseigner _Chemin de qmake_ (exemple C:\\msys64\\ucrt64\\bin\\qmake.exe).
- Dans le champ _Nom :_ ajouter (msys2).
![](assets/windows_msys2_setup/qt_version.png)
### Compilateurs
- Cliquer sur _ajouter_ puis choisir _MinGW_.
- Renseigner _Emplacement du compilateur C_ (exemple C:\\msys64\\ucrt64\\bin\\g++.exe).
- Dans le champ _Nom :_ ajouter (msys2).
![](assets/windows_msys2_setup/compiler.png)
### Débogueurs
- Cliquer sur _ajouter_
- Renseigner _Chemin :_ (exemple C:\\msys64\\ucrt64\\bin\\gdb.exe).
- Dans le champ _Nom :_ ajouter (msys2).
![](assets/windows_msys2_setup/debugger.png)
### cmake
- Outils -> _Ajouter_
- Renseigner _Chemin :_ (exemple C:\\msys64\\ucrt64\\bin\\cmake.exe).
- Dans le champ _Nom :_ ajouter (msys2).
![](assets/windows_msys2_setup/cmake.png)
### KIT
Maintenant que tous les prérequis sont fait nous allons crée un kit utilisant les outils fournis par MSYS2. Cliquer sur _Ajouter_, un nouveau kit _manuel_ apparaît, nommer celui-ci par exemple _Qt6 msys2_ puis renseigner le compilateur, le débogueur, la version de Qt et Outils CMake en choisissant à chaque fois ceux que nous venons de créer.
puis cliquer sur _appliquer_.
![](assets/windows_msys2_setup/kit.png)
Bravo 🥳🥳 vous avez terminé l'installation de la totalité des outils de développement.
# Clonez le dépôts de QElectrotech
Clonez le dépôt de QElectroTech comme vous le faite habituellement, sinon utilisez les commandes suivante dans power shell.
Crée et/ou se rendre dans le dossier dans lequel vous voulez clonez le dépôt (dans l'exemple nous allons crée un dossier QElectroTech dans C:)
```
mkdir C:\QElectroTech
cd C:\QElectroTech
git clone --recursive https://github.com/qelectrotech/qelectrotech-source-mirror.git
```
Une fois le dépôt cloné lancer Qt creator puis choisir d'ouvrir un projet existant, en choisissant le _CMakeLists.txt_ se trouvant à la racine du projet QElectroTech, enfin dans l'assistant de création de projet choisir comme kit le kit que nous avons créer précédemment.
-13
View File
@@ -1,13 +0,0 @@
Compiler QElectroTech sous microsoft Windows 10 et 11
================================
Compiler QElectroTech pour et/ou sous Windows peut être effectué avec plusieurs méthode différente.
Ce document énumère uniquement les différentes méthode possible
N'est mentionné que les étapes nécessaire afin de compilé QElectroTech sous Windows avec Qt6 et cmake. Ce document ne traite pas la compilation avec Qt5 et qmake.
>QElectroTech 0.100 est la dernière version à utiliser Qt5. Les version suivante sont développé avec Qt6 et utilise cmake au lieu de qmake.
Il existe deux méthodes pour cela :
1. [Utiliser msys2 (méthode recommandé)](fr_window_build_msys2.md)
2. Télécharger et compiler l'ensemble des dépendances (non rédigé)
Submodule
+1
Submodule pugixml added at 5a1892b321
+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) \
@@ -26,7 +26,12 @@
#include "xmlprojectelementcollectionitem.h" #include "xmlprojectelementcollectionitem.h"
#include <QFutureWatcher> #include <QFutureWatcher>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
#include <QtConcurrentMap> #include <QtConcurrentMap>
#else
#include <QtConcurrentRun>
#endif
/** /**
@brief ElementsCollectionModel::ElementsCollectionModel @brief ElementsCollectionModel::ElementsCollectionModel
Constructor Constructor
@@ -289,14 +294,14 @@ void ElementsCollectionModel::loadCollections(bool common_collection,
connect(watcher, &QFutureWatcher<void>::progressRangeChanged, connect(watcher, &QFutureWatcher<void>::progressRangeChanged,
this, &ElementsCollectionModel::loadingProgressRangeChanged); this, &ElementsCollectionModel::loadingProgressRangeChanged);
connect(watcher, &QFutureWatcher<void>::finished, connect(watcher, &QFutureWatcher<void>::finished,
this, &ElementsCollectionModel::loadingFinished); this, &ElementsCollectionModel::loadingFinished);
connect( connect(watcher, &QFutureWatcher<void>::finished, watcher, &QFutureWatcher<void>::deleteLater);
watcher,
&QFutureWatcher<void>::finished,
watcher,
&QFutureWatcher<void>::deleteLater);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
m_future = QtConcurrent::map(m_items_list_to_setUp, setUpData); m_future = QtConcurrent::map(m_items_list_to_setUp, setUpData);
#else
qDebug() << "Help code for QT 6 or later";
#endif
watcher->setFuture(m_future); watcher->setFuture(m_future);
} }
@@ -835,8 +835,14 @@ void ElementsCollectionWidget::search()
} }
hideCollection(true); hideCollection(true);
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
const QStringList text_list = text.split("+", QString::SkipEmptyParts);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.14 or later")
#endif
const QStringList text_list = text.split("+", Qt::SkipEmptyParts); const QStringList text_list = text.split("+", Qt::SkipEmptyParts);
#endif
QModelIndexList match_index; QModelIndexList match_index;
for (QString txt : text_list) { for (QString txt : text_list) {
match_index << m_model->match(m_showed_index.isValid() match_index << m_model->match(m_showed_index.isValid()
@@ -803,13 +803,13 @@ bool ElementsLocation::setXml(const QDomDocument &xml_document) const
QString path_ = collectionPath(false); QString path_ = collectionPath(false);
QRegularExpression rx("^(.*)/(.*\\.elmt)$"); QRegularExpression rx("^(.*)/(.*\\.elmt)$");
if (auto regex_match = rx.match(path_); regex_match.hasMatch()) if (rx.exactMatch(path_))
{ {
return project() return project()
->embeddedElementCollection() ->embeddedElementCollection()
->addElementDefinition( ->addElementDefinition(
regex_match.captured(1), rx.cap(1),
regex_match.captured(2), rx.cap(2),
xml_document.documentElement()); xml_document.documentElement());
} }
else else
@@ -20,7 +20,7 @@
#include "../NameList/nameslist.h" #include "../NameList/nameslist.h"
#include "../diagramcontext.h" #include "../diagramcontext.h"
#include "pugixml.hpp" #include "pugixml/src/pugixml.hpp"
#include <QIcon> #include <QIcon>
#include <QString> #include <QString>
@@ -87,7 +87,11 @@ void ElementsTreeView::startElementDrag(const ElementsLocation &location)
{ {
if (! location.exist()) return; if (! location.exist()) return;
auto drag = new QDrag{this}; #if QT_VERSION < QT_VERSION_CHECK(6, 2, 0)
QDrag* drag = new QDrag(this);
#else
QScopedPointer<QDrag> drag(new QDrag(this));
#endif
QString location_str = location.toString(); QString location_str = location.toString();
QMimeData *mime_data = new QMimeData(); QMimeData *mime_data = new QMimeData();
@@ -361,7 +361,7 @@ void FileElementCollectionItem::setUpIcon()
setIcon(QET::Icons::Folder); setIcon(QET::Icons::Folder);
} else { } else {
if (m_path.endsWith(".qetmak")) { if (m_path.endsWith(".qetmak")) {
setIcon(QET::Icons::PartRectangle); setIcon(QIcon());
} else { } else {
ElementsLocation loc(collectionPath()); ElementsLocation loc(collectionPath());
setIcon(loc.icon()); setIcon(loc.icon());
+1 -1
View File
@@ -17,7 +17,7 @@
*/ */
#ifndef NAMES_LIST_H #ifndef NAMES_LIST_H
#define NAMES_LIST_H #define NAMES_LIST_H
#include "pugixml.hpp" #include "pugixml/src/pugixml.hpp"
#include <QtXml> #include <QtXml>
/** /**
@@ -18,7 +18,6 @@
#include "terminalstripdrawer.h" #include "terminalstripdrawer.h"
#include <QPainter> #include <QPainter>
#include <QHash>
namespace TerminalStripDrawer { namespace TerminalStripDrawer {
+6 -1
View File
@@ -130,7 +130,12 @@ bool PhysicalTerminal::setLevelOf(const QSharedPointer<RealTerminal> &terminal,
const int i = m_real_terminal.indexOf(terminal); const int i = m_real_terminal.indexOf(terminal);
if (i >= 0) if (i >= 0)
{ {
m_real_terminal.swapItemsAt(i, std::min(static_cast<qsizetype>(level), m_real_terminal.size()-1)); #if QT_VERSION >= QT_VERSION_CHECK(5,14,0)
m_real_terminal.swapItemsAt(i, std::min(level, m_real_terminal.size()-1));
#else
auto j = std::min(level, m_real_terminal.size()-1);
std::swap(m_real_terminal.begin()[i], m_real_terminal.begin()[j]);
#endif
return true; return true;
} }
return false; return false;
+4 -1
View File
@@ -64,8 +64,11 @@ bool TerminalStripData::fromXml(const QDomElement &xml_element)
"due to wrong tag name. Expected " << this->xmlTagName() << " used " << xml_element.tagName(); "due to wrong tag name. Expected " << this->xmlTagName() << " used " << xml_element.tagName();
return false; return false;
} }
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
m_uuid = QUuid::fromString(xml_element.attribute(QStringLiteral("uuid"))); m_uuid = QUuid::fromString(xml_element.attribute(QStringLiteral("uuid")));
#else
m_uuid = QUuid(xml_element.attribute(QStringLiteral("uuid")));
#endif
for (auto &xml_info : for (auto &xml_info :
QETXML::findInDomElement(xml_element.firstChildElement(QStringLiteral("informations")), QETXML::findInDomElement(xml_element.firstChildElement(QStringLiteral("informations")),
@@ -35,7 +35,11 @@ TerminalStripTreeDockWidget::TerminalStripTreeDockWidget(QETProject *project, QW
ui->setupUi(this); ui->setupUi(this);
setProject(project); setProject(project);
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
ui->m_tree_view->expandRecursively(ui->m_tree_view->rootIndex()); ui->m_tree_view->expandRecursively(ui->m_tree_view->rootIndex());
#else
ui->m_tree_view->expandAll();
#endif
} }
TerminalStripTreeDockWidget::~TerminalStripTreeDockWidget() TerminalStripTreeDockWidget::~TerminalStripTreeDockWidget()
@@ -89,7 +93,11 @@ void TerminalStripTreeDockWidget::reload()
buildTree(); buildTree();
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
ui->m_tree_view->expandRecursively(ui->m_tree_view->rootIndex()); ui->m_tree_view->expandRecursively(ui->m_tree_view->rootIndex());
#else
ui->m_tree_view->expandAll();
#endif
//Reselect the tree widget item of the current edited strip //Reselect the tree widget item of the current edited strip
auto item = m_item_strip_H.key(current_); auto item = m_item_strip_H.key(current_);
+4
View File
@@ -55,7 +55,11 @@ BorderTitleBlock::BorderTitleBlock(QObject *parent) :
m_titleblock_template_renderer = new TitleBlockTemplateRenderer(this); m_titleblock_template_renderer = new TitleBlockTemplateRenderer(this);
m_titleblock_template_renderer -> setTitleBlockTemplate(QETApp::defaultTitleBlockTemplate()); m_titleblock_template_renderer -> setTitleBlockTemplate(QETApp::defaultTitleBlockTemplate());
// disable the QPicture-based cache from Qt 4.8 to avoid rendering errors and crashes
#if QT_VERSION < QT_VERSION_CHECK(4, 8, 0) // ### Qt 6: remove
#else
m_titleblock_template_renderer -> setUseCache(false); m_titleblock_template_renderer -> setUseCache(false);
#endif
// dimensions par defaut du schema // dimensions par defaut du schema
importBorder(BorderProperties()); importBorder(BorderProperties());
+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
+7
View File
@@ -72,7 +72,14 @@ bool ConductorNumExport::toCsv()
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) if (file.open(QIODevice::WriteOnly | QIODevice::Text))
{ {
QTextStream stream(&file); QTextStream stream(&file);
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // ### Qt 6: remove
stream << wiresNum() << endl;
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.15 or later")
#endif
stream << wiresNum() << &Qt::endl(stream); stream << wiresNum() << &Qt::endl(stream);
#endif
} }
else { else {
return false; return false;
+7
View File
@@ -811,7 +811,14 @@ void ConductorProperties::readStyle(const QString &style_string) {
if (style_string.isEmpty()) return; if (style_string.isEmpty()) return;
// recupere la liste des couples style / valeur // recupere la liste des couples style / valeur
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
QStringList styles = style_string.split(";", QString::SkipEmptyParts);
#else
#if TODO_LIST
#pragma message("@TODO remove code QString::SkipEmptyParts for QT 5.14 or later")
#endif
QStringList styles = style_string.split(";", Qt::SkipEmptyParts); QStringList styles = style_string.split(";", Qt::SkipEmptyParts);
#endif
QRegularExpression Rx("^(?<name>[a-z-]+): (?<value>[a-z-]+)$"); QRegularExpression Rx("^(?<name>[a-z-]+): (?<value>[a-z-]+)$");
if (!Rx.isValid()) if (!Rx.isValid())
+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)) {
+13 -2
View File
@@ -47,8 +47,14 @@ ElementQueryWidget::ElementQueryWidget(QWidget *parent) :
m_button_group.addButton(ui->m_coil_cb, 4); m_button_group.addButton(ui->m_coil_cb, 4);
m_button_group.addButton(ui->m_protection_cb, 5); m_button_group.addButton(ui->m_protection_cb, 5);
m_button_group.addButton(ui->m_thumbnail_cb, 6); m_button_group.addButton(ui->m_thumbnail_cb, 6);
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // ### Qt 6: remove
connect(&m_button_group, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), [this](int id)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.15 or later")
#endif
connect(&m_button_group, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::idClicked), [this](int id) connect(&m_button_group, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::idClicked), [this](int id)
#endif
{ {
auto check_box = static_cast<QCheckBox *>(m_button_group.button(0)); auto check_box = static_cast<QCheckBox *>(m_button_group.button(0));
if (id == 0) if (id == 0)
@@ -367,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");
@@ -450,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);
+9
View File
@@ -1514,6 +1514,14 @@ bool Diagram::fromXml(QDomElement &document,
if (content_ptr) { if (content_ptr) {
content_ptr -> m_elements = added_elements; content_ptr -> m_elements = added_elements;
content_ptr -> m_conductors_to_move = added_conductors; content_ptr -> m_conductors_to_move = added_conductors;
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
content_ptr -> m_text_fields = added_texts.toSet();
content_ptr -> m_images = added_images.toSet();
content_ptr -> m_shapes = added_shapes.toSet();
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.14 or later")
#endif
content_ptr -> m_text_fields = QSet<IndependentTextItem *>( content_ptr -> m_text_fields = QSet<IndependentTextItem *>(
added_texts.begin(), added_texts.begin(),
added_texts.end()); added_texts.end());
@@ -1524,6 +1532,7 @@ bool Diagram::fromXml(QDomElement &document,
added_shapes.begin(), added_shapes.begin(),
added_shapes.end()); added_shapes.end());
content_ptr->m_terminal_strip.swap(added_strips); content_ptr->m_terminal_strip.swap(added_strips);
#endif
content_ptr->m_tables.swap(added_tables); content_ptr->m_tables.swap(added_tables);
} }
+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();
+1 -1
View File
@@ -17,7 +17,7 @@
*/ */
#ifndef DIAGRAM_CONTEXT_H #ifndef DIAGRAM_CONTEXT_H
#define DIAGRAM_CONTEXT_H #define DIAGRAM_CONTEXT_H
#include "pugixml.hpp" #include "pugixml/src/pugixml.hpp"
#include <QDomElement> #include <QDomElement>
#include <QHash> #include <QHash>
+47 -2
View File
@@ -210,10 +210,17 @@ void DiagramView::handleElementDrop(QDropEvent *event)
return; return;
} }
QPointF drop_pos;
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
drop_pos = mapToScene(event->pos());
#else
drop_pos = event->position();
#endif
if (location.path().endsWith(".qetmak")) { if (location.path().endsWith(".qetmak")) {
diagram()->setEventInterface(new DiagramEventAddMacro(location, diagram(), event->position())); diagram()->setEventInterface(new DiagramEventAddMacro(location, diagram(), drop_pos));
} else { } else {
diagram()->setEventInterface(new DiagramEventAddElement(location, diagram(), event->position())); diagram()->setEventInterface(new DiagramEventAddElement(location, diagram(), drop_pos));
} }
//Set focus to the view to get event //Set focus to the view to get event
@@ -283,8 +290,17 @@ void DiagramView::handleTextDrop(QDropEvent *e) {
iti -> setHtml (e -> mimeData() -> text()); iti -> setHtml (e -> mimeData() -> text());
} }
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
m_diagram->undoStack().push(new AddGraphicsObjectCommand(
iti, m_diagram, mapToScene(e->pos())));
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
m_diagram->undoStack().push(new AddGraphicsObjectCommand( m_diagram->undoStack().push(new AddGraphicsObjectCommand(
iti, m_diagram, e->position())); iti, m_diagram, e->position()));
#endif
} }
/** /**
@@ -442,7 +458,14 @@ void DiagramView::mousePressEvent(QMouseEvent *e)
if (m_event_interface && m_event_interface->mousePressEvent(e)) return; if (m_event_interface && m_event_interface->mousePressEvent(e)) return;
//Start drag view when hold the middle button //Start drag view when hold the middle button
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
if (e->button() == Qt::MidButton)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
if (e->button() == Qt::MiddleButton) if (e->button() == Qt::MiddleButton)
#endif
{ {
m_drag_last_pos = e->pos(); m_drag_last_pos = e->pos();
viewport()->setCursor(Qt::ClosedHandCursor); viewport()->setCursor(Qt::ClosedHandCursor);
@@ -492,7 +515,14 @@ void DiagramView::mouseMoveEvent(QMouseEvent *e)
if (m_event_interface && m_event_interface->mouseMoveEvent(e)) return; if (m_event_interface && m_event_interface->mouseMoveEvent(e)) return;
// Drag the view // Drag the view
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
if (e->buttons() == Qt::MidButton)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
if (e->buttons() == Qt::MiddleButton) if (e->buttons() == Qt::MiddleButton)
#endif
{ {
QScrollBar *h = horizontalScrollBar(); QScrollBar *h = horizontalScrollBar();
QScrollBar *v = verticalScrollBar(); QScrollBar *v = verticalScrollBar();
@@ -553,7 +583,14 @@ void DiagramView::mouseReleaseEvent(QMouseEvent *e)
if (m_event_interface && m_event_interface->mouseReleaseEvent(e)) return; if (m_event_interface && m_event_interface->mouseReleaseEvent(e)) return;
// Stop drag view // Stop drag view
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
if (e->button() == Qt::MidButton)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
if (e->button() == Qt::MiddleButton) if (e->button() == Qt::MiddleButton)
#endif
{ {
viewport()->setCursor(Qt::ArrowCursor); viewport()->setCursor(Qt::ArrowCursor);
} }
@@ -587,7 +624,14 @@ void DiagramView::mouseReleaseEvent(QMouseEvent *e)
QMenu *menu = new QMenu(this); QMenu *menu = new QMenu(this);
menu->addAction(act); menu->addAction(act);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
menu->popup(e->globalPos());
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
menu->popup(e->pos()); menu->popup(e->pos());
#endif
} }
m_free_rubberbanding = false; m_free_rubberbanding = false;
@@ -1311,6 +1355,7 @@ void DiagramView::createTemplateFromSelection()
QFile file(full_path); QFile file(full_path);
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&file); QTextStream out(&file);
out.setCodec("UTF-8");
out << macro_doc.toString(4); out << macro_doc.toString(4);
file.close(); file.close();
qDebug() << "Template successfully saved to:" << full_path; qDebug() << "Template successfully saved to:" << full_path;
+14
View File
@@ -276,7 +276,14 @@ void ChangeZValueCommand::applyRaise(const QList<QGraphicsItem *> &items_list) {
for (int i = my_items_list.count() - 2 ; i >= 0 ; -- i) { for (int i = my_items_list.count() - 2 ; i >= 0 ; -- i) {
if (my_items_list[i] -> isSelected()) { if (my_items_list[i] -> isSelected()) {
if (!my_items_list[i +1] -> isSelected()) { if (!my_items_list[i +1] -> isSelected()) {
#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) // ### Qt 6: remove
my_items_list.swap(i, i + 1);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.13 or later")
#endif
my_items_list.swapItemsAt(i, i + 1); my_items_list.swapItemsAt(i, i + 1);
#endif
} }
} }
} }
@@ -294,7 +301,14 @@ void ChangeZValueCommand::applyLower(const QList<QGraphicsItem *> &items_list) {
for (int i = 1 ; i < my_items_list.count() ; ++ i) { for (int i = 1 ; i < my_items_list.count() ; ++ i) {
if (my_items_list[i] -> isSelected()) { if (my_items_list[i] -> isSelected()) {
if (!my_items_list[i - 1] -> isSelected()) { if (!my_items_list[i - 1] -> isSelected()) {
#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) // ### Qt 6: remove
my_items_list.swap(i, i - 1);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.13 or later")
#endif
my_items_list.swapItemsAt(i, i - 1); my_items_list.swapItemsAt(i, i - 1);
#endif
} }
} }
} }
+21
View File
@@ -372,7 +372,14 @@ ElementContent ElementView::pasteWithOffset(const QDomDocument &xml_document) {
*/ */
void ElementView::mousePressEvent(QMouseEvent* e) void ElementView::mousePressEvent(QMouseEvent* e)
{ {
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
if (e->button() == Qt::MidButton)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
if (e->button() == Qt::MiddleButton) if (e->button() == Qt::MiddleButton)
#endif
{ {
setCursor( (Qt::ClosedHandCursor)); setCursor( (Qt::ClosedHandCursor));
reference_view_ = e->pos(); reference_view_ = e->pos();
@@ -387,7 +394,14 @@ void ElementView::mousePressEvent(QMouseEvent* e)
*/ */
void ElementView::mouseMoveEvent(QMouseEvent* e) void ElementView::mouseMoveEvent(QMouseEvent* e)
{ {
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
if (e->buttons() == Qt::MidButton)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
if (e->buttons() == Qt::MiddleButton) if (e->buttons() == Qt::MiddleButton)
#endif
{ {
QScrollBar *h = horizontalScrollBar(); QScrollBar *h = horizontalScrollBar();
QScrollBar *v = verticalScrollBar(); QScrollBar *v = verticalScrollBar();
@@ -406,7 +420,14 @@ void ElementView::mouseMoveEvent(QMouseEvent* e)
*/ */
void ElementView::mouseReleaseEvent(QMouseEvent* e) void ElementView::mouseReleaseEvent(QMouseEvent* e)
{ {
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
if (e->button() == Qt::MidButton)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
if (e->button() == Qt::MiddleButton) if (e->button() == Qt::MiddleButton)
#endif
{ {
setCursor(Qt::ArrowCursor); setCursor(Qt::ArrowCursor);
adjustSceneRect(); adjustSceneRect();
@@ -519,7 +519,14 @@ void CustomElementGraphicPart::stylesFromXml(const QDomElement &qde)
resetStyles(); resetStyles();
//Get the list of pair style/value //Get the list of pair style/value
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
QStringList styles = qde.attribute("style").split(";", QString::SkipEmptyParts);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.14 or later")
#endif
QStringList styles = qde.attribute("style").split(";", Qt::SkipEmptyParts); QStringList styles = qde.attribute("style").split(";", Qt::SkipEmptyParts);
#endif
//Check each pair of style //Check each pair of style
QRegularExpression rx("^\\s*([a-z-]+)\\s*:\\s*([a-zA-Z-]+)\\s*$"); QRegularExpression rx("^\\s*([a-z-]+)\\s*:\\s*([a-zA-Z-]+)\\s*$");
+6
View File
@@ -313,6 +313,12 @@ void PartText::setPlainText(const QString &text) {
void PartText::setFont(const QFont &font) { void PartText::setFont(const QFont &font) {
if (font != this -> font()) { if (font != this -> font()) {
QGraphicsTextItem::setFont(font); QGraphicsTextItem::setFont(font);
// Re-anchor: the item's position transform is -margin(), and margin()
// depends on the font ascent. Without re-running this on a font change,
// the transform keeps the previous font's ascent — so the text renders
// at a different spot after save/reopen (the position recomputes from
// the saved font on load). See #158.
adjustItemPosition();
emit fontChanged(font); emit fontChanged(font);
} }
} }
@@ -291,9 +291,11 @@ void ElementPropertiesEditorWidget::on_m_base_type_cb_currentIndexChanged(int in
ui->m_master_gb->setVisible(master); ui->m_master_gb->setVisible(master);
ui->m_terminal_gb->setVisible(terminal); ui->m_terminal_gb->setVisible(terminal);
#if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
ui->tabWidget->setTabVisible(1, ui->tabWidget->setTabVisible(1,
(type_ == ElementData::Simple || (type_ == ElementData::Simple ||
type_ == ElementData::Master)); type_ == ElementData::Master));
#endif
updateTree(); updateTree();
} }
+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();
@@ -567,7 +567,14 @@ void ElementPictureFactory::setPainterStyle(const QDomElement &dom, QPainter &pa
pen.setCapStyle(Qt::SquareCap); pen.setCapStyle(Qt::SquareCap);
//Get the couples style/value //Get the couples style/value
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
const QStringList styles = dom.attribute("style").split(";", QString::SkipEmptyParts);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.14 or later")
#endif
const QStringList styles = dom.attribute("style").split(";", Qt::SkipEmptyParts); const QStringList styles = dom.attribute("style").split(";", Qt::SkipEmptyParts);
#endif
QRegularExpression rx("^(?<name>[a-z-]+):(?<value>[a-zA-Z-]+)$"); QRegularExpression rx("^(?<name>[a-z-]+):(?<value>[a-zA-Z-]+)$");
if (!rx.isValid()) if (!rx.isValid())
+9 -3
View File
@@ -188,8 +188,10 @@ void MachineInfo::send_info_to_debug()
QDirIterator it1(QETApp::commonElementsDir().toLatin1(),nameFilters, QDir::Files, QDirIterator::Subdirectories); QDirIterator it1(QETApp::commonElementsDir().toLatin1(),nameFilters, QDir::Files, QDirIterator::Subdirectories);
while (it1.hasNext()) while (it1.hasNext())
{ {
it1.next(); if(it1.next() > 0 )
{
commomElementsDir ++; commomElementsDir ++;
}
} }
qInfo()<< " Common Elements count:"<< commomElementsDir << "Elements"; qInfo()<< " Common Elements count:"<< commomElementsDir << "Elements";
@@ -198,8 +200,10 @@ void MachineInfo::send_info_to_debug()
QDirIterator it2(QETApp::customElementsDir().toLatin1(), nameFilters, QDir::Files, QDirIterator::Subdirectories); QDirIterator it2(QETApp::customElementsDir().toLatin1(), nameFilters, QDir::Files, QDirIterator::Subdirectories);
while (it2.hasNext()) while (it2.hasNext())
{ {
it2.next(); if(it2.next() > 0 )
{
customElementsDir ++; customElementsDir ++;
}
} }
qInfo()<< " Custom Elements count:"<< customElementsDir << "Elements"; qInfo()<< " Custom Elements count:"<< customElementsDir << "Elements";
@@ -207,8 +211,10 @@ void MachineInfo::send_info_to_debug()
QDirIterator it3(QETApp::companyElementsDir().toLatin1(), nameFilters, QDir::Files, QDirIterator::Subdirectories); QDirIterator it3(QETApp::companyElementsDir().toLatin1(), nameFilters, QDir::Files, QDirIterator::Subdirectories);
while (it3.hasNext()) while (it3.hasNext())
{ {
it3.next(); if(it3.next() > 0 )
{
companyElementsDir ++; companyElementsDir ++;
}
} }
qInfo()<< " Company Elements count:"<< companyElementsDir << "Elements"; qInfo()<< " Company Elements count:"<< companyElementsDir << "Elements";
+38 -2
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>
@@ -174,10 +178,42 @@ int main(int argc, char **argv)
QCoreApplication::setApplicationName("QElectroTech"); QCoreApplication::setApplicationName("QElectroTech");
//Creation and execution of the application //Creation and execution of the application
//HighDPI //HighDPI
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
#endif
qputenv("QT_ENABLE_HIGHDPI_SCALING", "1");
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFactorRoundingPolicy());
#if QT_VERSION > QT_VERSION_CHECK(5, 7, 0) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
#endif
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
qputenv("QT_ENABLE_HIGHDPI_SCALING", "1");
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFactorRoundingPolicy());
#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
+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
+158 -35
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
@@ -190,40 +203,71 @@ ProjectPrintWindow::~ProjectPrintWindow()
*/ */
void ProjectPrintWindow::requestPaint() void ProjectPrintWindow::requestPaint()
{ {
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
auto screen = this->screen(); #ifdef QT_DEBUG
if(screen) qDebug() << "--";
{ qDebug() << "DiagramPrintDialog::print printer_->resolution() before " << m_printer->resolution();
#ifdef QT_DEBUG qDebug() << "DiagramPrintDialog::print screennumber " << QApplication::desktop()->screenNumber();
qDebug() << "--"; #endif
qDebug() << "DiagramPrintDialog::print printer_->resolution() before " << m_printer->resolution();
qDebug() << "DiagramPrintDialog::print screennumber " << screen->name();
#endif
qreal dotsPerInch = (qreal)screen->logicalDotsPerInch(); QScreen *srn = QApplication::screens().at(QApplication::desktop()->screenNumber());
m_printer->setResolution(dotsPerInch); qreal dotsPerInch = (qreal)srn->logicalDotsPerInch();
m_printer->setResolution(dotsPerInch);
#ifdef QT_DEBUG #ifdef QT_DEBUG
qDebug() << "DiagramPrintDialog::print dotsPerInch " << dotsPerInch; qDebug() << "DiagramPrintDialog::print dotsPerInch " << dotsPerInch;
qDebug() << "DiagramPrintDialog::print printer_->resolution() after" << m_printer->resolution(); qDebug() << "DiagramPrintDialog::print printer_->resolution() after" << m_printer->resolution();
qDebug() << "--"; qDebug() << "--";
#endif #endif
} #endif
#endif #endif
#endif
if (!m_project->diagrams().count()) { if (!m_project->diagrams().count()) {
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).
} }
/** /**
@@ -233,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////
@@ -268,9 +312,9 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter
#if TODO_LIST #if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later") #pragma message("@TODO remove code for QT 6 or later")
#endif #endif
qDebug()<<"Help code for QT 6 or later"; qDebug()<<"Help code for QT 6 or later";
auto printed_rect = full_page ? printer->paperRect(QPrinter::Millimeter) : auto printed_rect = full_page ? printer->paperRect(QPrinter::Millimeter) :
printer->pageRect(QPrinter::Millimeter); printer->pageRect(QPrinter::Millimeter);
#endif #endif
auto used_width = printed_rect.width(); auto used_width = printed_rect.width();
auto used_height = printed_rect.height(); auto used_height = printed_rect.height();
@@ -320,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);
@@ -344,7 +447,7 @@ QRect ProjectPrintWindow::diagramRect(Diagram *diagram, const ExportProperties &
diagram_rect.setHeight(diagram_rect.height() - titleblock_height); diagram_rect.setHeight(diagram_rect.height() - titleblock_height);
} }
//Adjust the border of diagram to 1px (width of the line) //Adjust the border of diagram to 1px (width of the line)
diagram_rect.adjust(0,0,1,1); diagram_rect.adjust(0,0,1,1);
return (diagram_rect.toAlignedRect()); return (diagram_rect.toAlignedRect());
@@ -359,7 +462,7 @@ QRect ProjectPrintWindow::diagramRect(Diagram *diagram, const ExportProperties &
* with the orientation and the paper format used by the actual printer * with the orientation and the paper format used by the actual printer
*/ */
int ProjectPrintWindow::horizontalPagesCount( int ProjectPrintWindow::horizontalPagesCount(
Diagram *diagram, const ExportProperties &option, bool full_page) const Diagram *diagram, const ExportProperties &option, bool full_page) const
{ {
QRect printable_area; QRect printable_area;
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove #if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
@@ -388,7 +491,7 @@ int ProjectPrintWindow::horizontalPagesCount(
* with the orientation and paper format used by the actual printer * with the orientation and paper format used by the actual printer
*/ */
int ProjectPrintWindow::verticalPagesCount( int ProjectPrintWindow::verticalPagesCount(
Diagram *diagram, const ExportProperties &option, bool full_page) const Diagram *diagram, const ExportProperties &option, bool full_page) const
{ {
QRect printable_area; QRect printable_area;
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove #if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
@@ -514,7 +617,7 @@ void ProjectPrintWindow::loadPageSetupForCurrentPrinter()
QString value = settings.value("orientation", "landscape").toString(); QString value = settings.value("orientation", "landscape").toString();
m_printer->setPageOrientation( m_printer->setPageOrientation(
value == "landscape" ? QPageLayout::Landscape : value == "landscape" ? QPageLayout::Landscape :
QPageLayout::Portrait); QPageLayout::Portrait);
} }
if (settings.contains("papersize")) if (settings.contains("papersize"))
{ {
@@ -775,17 +878,37 @@ 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)
{ {
auto index = ui->m_date_from_cb->currentIndex(); auto index = ui->m_date_from_cb->currentIndex();
// 0 = all date // 0 = all date
// 1 = from the date // 1 = from the date
// 2 = at the date // 2 = at the date
if (index) { on_m_uncheck_all_clicked(); } if (index) { on_m_uncheck_all_clicked(); }
else { on_m_check_all_pb_clicked(); } else { on_m_check_all_pb_clicked(); }
@@ -795,7 +918,7 @@ void ProjectPrintWindow::on_m_date_cb_userDateChanged(const QDate &date)
{ {
auto diagram_date = diagram->border_and_titleblock.date(); auto diagram_date = diagram->border_and_titleblock.date();
if ( (index == 1 && diagram_date >= date) || if ( (index == 1 && diagram_date >= date) ||
(index == 2 && diagram_date == date) ) (index == 2 && diagram_date == date) )
m_diagram_list_hash.value(diagram)->setChecked(true); m_diagram_list_hash.value(diagram)->setChecked(true);
} }
+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;
+33 -2
View File
@@ -183,7 +183,16 @@ bool QET::orthogonalProjection(
// determine le point d'intersection des deux droites = le projete orthogonal // determine le point d'intersection des deux droites = le projete orthogonal
QPointF intersection_point; QPointF intersection_point;
QLineF::IntersectType it = line.intersects(perpendicular_line, &intersection_point); #if TODO_LIST
#pragma message("@TODO remove code for QT 5.14 or later")
#endif
QLineF::IntersectType it = line.
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
intersect // ### Qt 6: remove
#else
intersects
#endif
(perpendicular_line, &intersection_point);
// ne devrait pas arriver (mais bon...) // ne devrait pas arriver (mais bon...)
if (it == QLineF::NoIntersection) return(false); if (it == QLineF::NoIntersection) return(false);
@@ -536,8 +545,16 @@ QString QET::joinWithSpaces(const QStringList &string_list) {
QStringList QET::splitWithSpaces(const QString &string) { QStringList QET::splitWithSpaces(const QString &string) {
// les chaines sont separees par des espaces non echappes // les chaines sont separees par des espaces non echappes
// = avec un nombre nul ou pair de backslashes devant // = avec un nombre nul ou pair de backslashes devant
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.14 or later")
#endif
QStringList escaped_strings = string.split(QRegularExpression("[^\\]?(?:\\\\)* "), QStringList escaped_strings = string.split(QRegularExpression("[^\\]?(?:\\\\)* "),
Qt::SkipEmptyParts); #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
QString
#else
Qt
#endif
::SkipEmptyParts);
QStringList returned_list; QStringList returned_list;
foreach(QString escaped_string, escaped_strings) { foreach(QString escaped_string, escaped_strings) {
@@ -667,7 +684,14 @@ bool QET::writeXmlFile(QDomDocument &xml_doc, const QString &filepath, QString *
} }
QTextStream out(&file); QTextStream out(&file);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
out.setCodec("UTF-8");
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
out.setEncoding(QStringConverter::Utf8); out.setEncoding(QStringConverter::Utf8);
#endif
out.setGenerateByteOrderMark(false); out.setGenerateByteOrderMark(false);
out << xml_doc.toString(4); out << xml_doc.toString(4);
if (!file.commit()) if (!file.commit())
@@ -798,7 +822,14 @@ bool QET::writeToFile(QDomDocument &xml_doc, QFile *file, QString *error_message
QTextStream out(file); QTextStream out(file);
out.seek(0); out.seek(0);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
out.setCodec("UTF-8");
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
out.setEncoding(QStringConverter::Utf8); out.setEncoding(QStringConverter::Utf8);
#endif
out.setGenerateByteOrderMark(false); out.setGenerateByteOrderMark(false);
out << xml_doc.toString(4); out << xml_doc.toString(4);
if (opened_here) { if (opened_here) {
+41 -16
View File
@@ -204,7 +204,14 @@ void QETApp::setLanguage(const QString &desired_language) {
QString languages_path = languagesPath(); QString languages_path = languagesPath();
// load Qt library translations // load Qt library translations
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
QString qt_l10n_path = QLibraryInfo::location(QLibraryInfo::TranslationsPath);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
QString qt_l10n_path = QLibraryInfo::path(QLibraryInfo::TranslationsPath); QString qt_l10n_path = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
#endif
if (!qtTranslator.load("qt_" + desired_language, qt_l10n_path)) if (!qtTranslator.load("qt_" + desired_language, qt_l10n_path))
{ {
qWarning() << "failed to load" qWarning() << "failed to load"
@@ -219,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);
@@ -256,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;
} }
@@ -1221,7 +1232,21 @@ QString QETApp::languagesPath()
* en l'absence d'option de compilation, on utilise le dossier lang, * en l'absence d'option de compilation, on utilise le dossier lang,
* situe a cote du binaire executable * situe a cote du binaire executable
*/ */
return(QCoreApplication::applicationDirPath() + "/lang/"); {
const QString bin_dir = QCoreApplication::applicationDirPath();
const QString next_to_bin = bin_dir + "/lang/";
// Some packagings (notably the Windows installer) put the binary in a
// "bin" subfolder while "lang" sits beside it (../lang). Fall back to
// that layout when the folder next to the binary is absent, so the
// translations are found without a --lang-dir argument. See issue #86.
if (!QDir(next_to_bin).exists()) {
const QString sibling_of_bin =
QDir::cleanPath(bin_dir + "/../lang") + "/";
if (QDir(sibling_of_bin).exists())
return(sibling_of_bin);
}
return(next_to_bin);
}
#else #else
#ifndef QET_LANG_PATH_RELATIVE_TO_BINARY_PATH #ifndef QET_LANG_PATH_RELATIVE_TO_BINARY_PATH
/* the compilation option represents /* the compilation option represents
@@ -451,8 +451,16 @@ void GraphicsTablePropertiesEditor::setUpEditConnection()
m_edit_connection << connect(ui->m_table_left_margin, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply); m_edit_connection << connect(ui->m_table_left_margin, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply);
m_edit_connection << connect(ui->m_table_right_margin, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply); m_edit_connection << connect(ui->m_table_right_margin, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply);
m_edit_connection << connect(ui->m_table_bottom_margin, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply); m_edit_connection << connect(ui->m_table_bottom_margin, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply);
m_edit_connection << connect(m_table_button_group, QOverload<int>::of(&QButtonGroup::idClicked), this, &GraphicsTablePropertiesEditor::apply); #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // ### Qt 6: remove
m_edit_connection << connect(m_header_button_group, QOverload<int>::of(&QButtonGroup::idClicked), this, &GraphicsTablePropertiesEditor::apply); m_edit_connection << connect(m_table_button_group, QOverload<int>::of(&QButtonGroup::buttonClicked), this, &GraphicsTablePropertiesEditor::apply);
m_edit_connection << connect(m_header_button_group, QOverload<int>::of(&QButtonGroup::buttonClicked), this, &GraphicsTablePropertiesEditor::apply);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.15 or later")
#endif
m_edit_connection << connect(m_table_button_group, QOverload<int>::of(&QButtonGroup::idClicked), this, &GraphicsTablePropertiesEditor::apply);
m_edit_connection << connect(m_header_button_group, QOverload<int>::of(&QButtonGroup::idClicked), this, &GraphicsTablePropertiesEditor::apply);
#endif
m_edit_connection << connect(ui->m_display_n_row_sb, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply); m_edit_connection << connect(ui->m_display_n_row_sb, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply);
m_edit_connection << connect(ui->m_display_n_row_sb, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::updateInfoLabel); m_edit_connection << connect(ui->m_display_n_row_sb, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::updateInfoLabel);
} }
+7
View File
@@ -1725,7 +1725,14 @@ QSet<Conductor *> Conductor::relatedPotentialConductors(const bool all_diagram,
for (Conductor *c : other_conductors_list_t) { for (Conductor *c : other_conductors_list_t) {
other_conductors += c->relatedPotentialConductors(all_diagram, t_list); other_conductors += c->relatedPotentialConductors(all_diagram, t_list);
} }
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
other_conductors += other_conductors_list_t.toSet();
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.14 or later")
#endif
other_conductors += QSet<Conductor*>(other_conductors_list_t.begin(),other_conductors_list_t.end()); other_conductors += QSet<Conductor*>(other_conductors_list_t.begin(),other_conductors_list_t.end());
#endif
} }
} }

Some files were not shown because too many files have changed in this diff Show More