Compare commits

...

82 Commits

Author SHA1 Message Date
Laurent Trinques 1b8dc5f410 texteditor: set font size spinbox minimum to 4pt to prevent SIGSEGV 2026-05-25 13:14:57 +02:00
Laurent Trinques 26d5d019cc texteditor: fix SIGSEGV caused by font size spinbox reaching 0 2026-05-25 13:04:37 +02:00
Laurent Trinques 6dd7c2d926 crossrefitem: fix SIGSEGV crash + improve double-click navigation + fix PDF/SVG/print rendering
Windows Build / build-windows (push) Has been cancelled
Test Windows VS2026 migration / Build on windows-2025-vs2026 (push) Has been cancelled
Windows Build / deploy-pages (push) Has been cancelled
Fix a use-after-free crash (SIGSEGV in QRegion::begin, Qt5Gui+0x49af60)
confirmed by analysis of 19 coredumps. The crash was triggered when the
scene viewport clip region was freed during zoom/resize events while
QPicture::play() replayed drawPolyline commands through the scene painter.
Qt's raster engine then dereferenced a stale QRegionData pointer.

Root cause: CrossRefItem used three nested QPicture objects (m_drawing,
m_hdr_no_ctc, m_hdr_nc_ctc). The nested drawPicture() calls amplified
the use-after-free risk on any repaint event.

Fix: remove all QPicture from CrossRefItem entirely.
- updateLabel() now uses a QImage-backed dummy painter to compute
  m_bounding_rect, m_shape_path and m_hovered_contacts_map geometry.
  A bool m_update_map flag prevents the map from being overwritten
  during paint().
- paint() calls drawAsCross()/drawAsContacts() directly on the scene
  painter — no QPicture::play() anywhere in the class.
- buildHeaderContact() now draws NO/NC symbols directly onto the painter
  instead of recording them into QPicture members.

Also fix mouseDoubleClickEvent: the element under the click is now found
directly from m_hovered_contacts_map using the event position, rather
than relying on m_hovered_contact which could be reset by hoverMoveEvent
between the two clicks of a double-click.

Also remove setBold(true) on terminal name labels: the Qt PDF/SVG/print
engine rendered bold at 4pt as extremely thick glyphs, making exports
unreadable. Normal weight at 4pt is correct and legible on all backends.

Fixes: SIGSEGV in CrossRefItem::paint() on zoom/resize
Fixes: double-click navigation unreliable on Xref contact symbols
Fixes: terminal name labels unreadable in PDF/SVG/print export
2026-05-24 16:00:45 +02:00
Laurent Trinques 47796e183a Update translation files 2026-05-23 19:28:22 +02:00
Laurent Trinques ddf5ffcd89 xref: add option to hide terminal names in cross-references
Add a new boolean property 'showTerminalName' (default: true) to
XRefProperties, with full persistence in XML and QSettings.

A new checkbox "Afficher les numéros de bornes dans les Xrefs" is
added to the XRefPropertiesWidget in the main display group (not in
the cross-only group), so it is active in both Cross and Contacts modes.

When unchecked, terminal names are hidden in all three rendering paths:
  - drawContact()            (Contacts mode: NO/NC/SW symbols)
  - fillCrossRef()           (Cross mode: NO and NC columns)
  - setUpCrossBoundingRect() (Cross mode: bounding rect sizing)

Backward compatible: existing project files without the attribute
default to showTerminalName=true (no visual change).

Files changed:
  sources/properties/xrefproperties.h
  sources/properties/xrefproperties.cpp
  sources/ui/xrefpropertieswidget.ui
  sources/ui/xrefpropertieswidget.cpp
  sources/qetgraphicsitem/crossrefitem.cpp
2026-05-23 19:23:43 +02:00
Laurent Trinques 7d718bb9a0 crossrefitem: fix terminal name sorting for power contacts
The previous sort used QString::toInt() to order terminal names,
which returns 0 for any string containing a non-numeric prefix
(e.g. "R1", "R2", "L1", "L2"...). This caused undefined sort order
and incorrect pole pairing in the Xref contact mirror.

Example: a 4-pole NC power contact with terminals R1..R8 was
displaying R1/R3, R5/R7, R2/R4, R6/R8 instead of the correct
R1/R2, R3/R4, R5/R6, R7/R8.

Fix: extract the trailing numeric part of each terminal name and
compare prefixes separately. If both names share the same prefix
and both have a trailing number, sort numerically on that number;
otherwise fall back to full string comparison.

This covers all naming conventions: "1"/"2"/"3", "R1"/"R2"/"R3",
"L1"/"L2"/"L3", etc.

Applied in both drawContact() and setUpCrossBoundingRect().
2026-05-23 15:50:11 +02:00
Laurent Trinques d949e6eb8c crossrefitem: fix SIGSEGV when dropping power NC contact on diagram
Three bugs were causing a crash (SIGSEGV in QRegion::begin) when
a power NC slave element was placed on a folio, even before linking
it to a master element.

1. m_drawing (QPicture) was never reset between updateLabel() calls.
   QPicture accumulates paint commands — calling qp.begin() on an
   existing QPicture appends to it rather than replacing its content.
   After several updates (load, move, hover...) the picture became
   corrupted and crashed on play().
   Fix: reset m_drawing = QPicture() at the start of updateLabel().

2. m_drawed_contacts was only initialized to 0 in drawAsContacts(),
   but not in drawAsCross(). When drawing in Cross mode, fillCrossRef()
   called drawContact() with an uninitialized m_drawed_contacts value,
   producing a garbage offset. The NC contact symbol uses drawPolyline()
   with a sub-pixel Y coordinate (offset+2.5); with a random offset Qt
   generated an invalid QRegion and crashed.
   This explains why NC contacts crashed but NO contacts did not: the
   NO symbol only uses drawLine() which is more tolerant of bad coords.
   Fix: add m_drawed_contacts = 0 at the start of drawAsCross().

3. setUpCrossBoundingRect() used QRectF() (null rect) as the reference
   rect for painter.boundingRect(), which can return invalid dimensions.
   Additionally, height was accumulated incorrectly: united() + setHeight()
   doubled the height at each iteration, causing an exponentially growing
   bounding rect with multiple contacts.
   Fix: use QRectF(0, 0, 500, 20) as reference rect and accumulate
   height and width independently.
2026-05-23 15:32:28 +02:00
Laurent Trinques dbda958261 terminaldata: add No, Nc, Common types for SW contacts
Extend TerminalData::Type enum with three new semantic values:
- No     : Normally Open terminal of a switch (SW) contact
- Nc     : Normally Closed terminal of a switch (SW) contact
- Common : Common terminal of a switch (SW) contact

Update typeToString() and typeFromString() accordingly.
Fully backward compatible: existing Generic/Inner/Outer types
are unchanged. Elements without typed terminals fall back
to the previous behavior (first 2 named terminals).

terminal: expose terminalType() as public accessor

Add Terminal::terminalType() returning the TerminalData::Type
of this terminal. This allows crossrefitem and other consumers
to filter terminals by semantic role (No, Nc, Common) without
accessing TerminalData internals directly.

terminaleditor: add No, Nc, Common entries to type combobox

Expose the three new TerminalData types (No, Nc, Common) in
the element editor UI so users can assign a semantic role to
each terminal of a SW contact element.

Also fix a pre-existing bug in updateForm() where m_type_cb
was incorrectly using m_orientation_cb->findData() instead
of m_type_cb->findData(), preventing the type from being
restored correctly when selecting a terminal.

terminaleditor: add No, Nc, Common entries to type combobox

Expose the three new TerminalData types (No, Nc, Common) in
the element editor UI so users can assign a semantic role to
each terminal of a SW contact element.

Also fix a pre-existing bug in updateForm() where m_type_cb
was incorrectly using m_orientation_cb->findData() instead
of m_type_cb->findData(), preventing the type from being
restored correctly when selecting a terminal.
2026-05-23 02:09:32 +02:00
Laurent Trinques d691489165 Update translations files 2026-05-22 10:16:24 +02:00
Laurent Trinques 202ea38e40 Merge pull request #464 from Kellermorph/makro-fix
New element: Line definition
2026-05-22 10:06:20 +02:00
Kellermorph 86dafcb576 Merge branch 'master' into makro-fix 2026-05-21 20:52:18 +02:00
Kellermorph f416c2a97e New element: Line definition 2026-05-21 20:47:44 +02:00
Laurent Trinques 7426fedba3 crossrefitem: display terminal names on contact symbols in Xref
When a slave element has named terminals in its element definition
(.elmt), the terminal names (e.g. 13/14 for NO, 11/12 for NC,
12/13/14 for SW) are now displayed on each side of the contact
symbol in the cross-reference view.

- NO/NC contacts: name[0] on the left, name[1] on the right
- SW contacts: name[0] (NO) top-left, name[1] (common) top-right,
  name[2] (NC) bottom-left

Terminal names are read from Terminal::name() which is populated
from TerminalData::m_name during element parsing. If terminals are
not named, nothing is displayed (fully backward compatible).

Users are expected to name their own terminals in the element
editor to avoid duplicating elements in the official collection.
2026-05-21 17:10:44 +02:00
Laurent Trinques 027050c7e7 Update windows-msi.yml 2026-05-21 14:32:07 +02:00
Laurent Trinques fc948ad963 QElectroTech.wxs — add ProductCode="$(var.ProductCode)" to <Package>
+ AllowSameVersionUpgrades="yes" to <MajorUpgrade>
windows-msi.yml — in the ‘Extract version’ step, calculate a unique GUID
based on the commit’s SHA, then pass -d ‘ProductCode=$productGuid’ to the WIX build
This ensures that each build will have a different ProductCode → MajorUpgrade will always be triggered
2026-05-21 14:10:17 +02:00
Laurent Trinques 24d075b64c msi: add The AllowSameVersionUpgrades="yes" setting forces uninstallation
even when the MSI version is identical (which is the case here, as 0.100.1.0
remains unchanged between two nightly builds).
2026-05-21 13:59:53 +02:00
Laurent Trinques f914f91e77 tests sign msi installer 2026-05-21 12:33:46 +02:00
Laurent Trinques fe03a0f643 Restore workflows to pre-test state 2026-05-21 12:29:22 +02:00
Laurent Trinques a7ad0278a6 Update windows-build.yml 2026-05-21 11:49:30 +02:00
Laurent Trinques f517489421 Update windows-build.yml 2026-05-21 11:15:25 +02:00
Laurent Trinques c8fa1c9fa4 test 2026-05-21 10:45:14 +02:00
Laurent Trinques d85aff0c0f Update CMakeLists.txt 2026-05-21 03:40:14 +02:00
Laurent Trinques 96b8e4b19c fix(msi): move Custom element conditions to Condition attribute (WIX0400)
WiX v7 requires conditions to be set via the Condition attribute,
not as inner text of the Custom element.
2026-05-21 03:00:49 +02:00
Laurent Trinques 9760288db6 fix(msi): correct CustomAction pattern for elements\ read-only
- Use SetProperty + WixQuietExec two-step pattern to pass runtime
  INSTALLDIR to a deferred CustomAction (fixes WIX1077 and WIX0400)
- Add WixToolset.Util.wixext/7.0.0 extension (required for WixQuietExec)
- Fix condition syntax: collapse multi-line conditions to single line
- Add -ext WixToolset.Util.wixext to wix build command in windows-msi.yml
2026-05-21 02:55:28 +02:00
Laurent Trinques eeaa059a77 This PR improves the MSI installer by removing the Lancer QET.bat wrapper and handling everything natively in QElectroTech.wxs.
**`build-aux/windows/QElectroTech.wxs`**
- Desktop and Start Menu shortcuts now point directly to `bin\qelectrotech.exe` with all required arguments (`--common-elements-dir`, `--common-tbt-dir`, `--lang-dir`, `-style windowsvista`) — no `.bat` wrapper needed
- Added a deferred `CustomAction` that runs after `InstallFiles` and recursively sets all files in `elements\` to read-only using an inline PowerShell command

**`.github/workflows/windows-msi.yml`**
- Replaced the step that created `Lancer QET.bat` with a step that removes it from the artifact before the WiX build, so it is not embedded in the MSI
- The `.bat` file remains untouched in the ZIP portable build (managed by `windows-build.yml`)

- No console window flashing when launching QElectroTech from the MSI shortcuts
- The `elements\` directory is properly set to read-only after installation, as required
- Cleaner MSI package — no `.bat` file shipped to end users installing via MSI
2026-05-21 02:34:38 +02:00
Laurent Trinques fa334d34a4 git submodule update --remote elements 2026-05-20 22:14:43 +02:00
Laurent Trinques 9664ff54ea Merge pull request #462 from Kellermorph/makro-fix
follow up: wiring list
2026-05-19 13:15:14 +02:00
Laurent Trinques be0da461bd Merge pull request #463 from zakb120/patch-Tr-Translate
Turkish Lang Update
2026-05-19 13:14:45 +02:00
Kellermorph 8c557a7f29 follow up: wiring list 2026-05-18 21:31:11 +02:00
zakb120 413e13a38c Tr Translate Zekeriya AKBABA 2026-05-18 09:29:46 +03:00
Laurent Trinques 87f9f40e91 Update MacQetDeploy_arm64.sh
Windows Build / build-windows (push) Has been cancelled
Test Windows VS2026 migration / Build on windows-2025-vs2026 (push) Has been cancelled
Windows Build / deploy-pages (push) Has been cancelled
2026-05-14 13:12:43 +02:00
Laurent Trinques 43fb34f852 Update MacQetDeploy_arm64.sh 2026-05-14 12:44:45 +02:00
Laurent Trinques d6251c901e Update MacQetDeploy_arm64.sh 2026-05-14 12:11:21 +02:00
Laurent Trinques c0ba961fb3 Update MacQetDeploy_arm64.sh 2026-05-14 11:45:11 +02:00
Laurent Trinques 56f5a09737 Update MacQetDeploy_arm64.sh 2026-05-14 11:20:36 +02:00
Laurent Trinques b5eebb2123 Update MacQetDeploy_arm64.sh 2026-05-14 11:06:56 +02:00
Laurent Trinques 7bf395afab Every Monday at 2 am (UTC) (cron) or manually
↓
  windows-build.yml
  ├── build-windows  →  generates an exe file + zip + portable artefact
  └── deploy-pages   →  clears old files, uploads the exe file + zip to the ‘release nightly’ repository
        ↓ (workflow_run: completed + successful)
  windows-msi.yml
  ├── uploads the portable artefact
  ├── builds the MSI with WiX v7
  ├── deletes the old .msi, uploads the MSI to the nightly version
  └── generates and deploys GitHub Pages  ← the 3 URLs are known here
The GitHub Pages page is no longer generated by windows-build.yml but by windows-msi.yml once the MSI is in the release
2026-05-14 09:53:29 +02:00
Laurent Trinques 93baa00d00 Update windows-msi.yml 2026-05-14 09:26:01 +02:00
Laurent Trinques 6a7ae44ce5 git submodule update --remote elements
Windows Build / build-windows (push) Has been cancelled
Windows Build / deploy-pages (push) Has been cancelled
2026-05-14 03:21:35 +02:00
Laurent Trinques 89131a21a3 Merge pull request #459 from Kellermorph/makro-fix
Fix and Improve Multi-selection for Diagram Operations
2026-05-14 03:18:20 +02:00
Kellermorph 0b79dfd149 Fix and Improve Multi-selection for Diagram Operations 2026-05-13 19:42:11 +02:00
Laurent Trinques 72178714fd git submodule update --remote elements
Windows Build / build-windows (push) Has been cancelled
Windows Build / deploy-pages (push) Has been cancelled
2026-05-13 17:25:39 +02:00
Laurent Trinques c7e236cd48 Update test-vs2026.yml 2026-05-13 13:43:55 +02:00
Laurent Trinques fb769b884c Try to fix https://github.com/qelectrotech/qelectrotech-source-mirror/issues/307
Windows Build / build-windows (push) Has been cancelled
Windows Build / deploy-pages (push) Has been cancelled
2026-05-13 13:16:43 +02:00
Laurent Trinques 2ae9ec87bb Update windows-build.yml
Windows Build / build-windows (push) Has been cancelled
Windows Build / deploy-pages (push) Has been cancelled
2026-05-13 01:40:55 +02:00
Laurent Trinques 76d311cb35 Move build-aux/windows/generate-page.py -> build-aux/generate-page.py 2026-05-13 01:23:18 +02:00
Laurent Trinques b016cc9f54 Update windows-build.yml 2026-05-13 01:06:17 +02:00
Laurent Trinques 526e39e909 Add files via upload 2026-05-13 01:05:31 +02:00
Laurent Trinques d781105dfd Update windows-build.yml 2026-05-13 01:02:32 +02:00
Laurent Trinques f6b93c6b71 Update windows-build.yml 2026-05-13 00:40:17 +02:00
Laurent Trinques 61319bbbd6 Update windows-build.yml 2026-05-13 00:08:18 +02:00
Laurent Trinques 1b522c251b Update windows-build.yml 2026-05-12 23:34:10 +02:00
Laurent Trinques acfdab77fa Update windows-msi.yml 2026-05-12 23:33:27 +02:00
Laurent Trinques 8e327448cc Update QElectroTech.wxs 2026-05-12 23:01:54 +02:00
Laurent Trinques 7a8cee0ce6 Update QElectroTech.wxs 2026-05-12 22:58:00 +02:00
Laurent Trinques 82b8e7947e Update windows-msi.yml 2026-05-12 22:54:20 +02:00
Laurent Trinques e542a05d3f Update QElectroTech.wxs 2026-05-12 22:53:33 +02:00
Laurent Trinques 0b337a1514 Update windows-msi.yml 2026-05-12 22:44:12 +02:00
Laurent Trinques 48fec1db98 Update QElectroTech.wxs 2026-05-12 22:30:23 +02:00
Laurent Trinques 31f946426b Update QElectroTech.wxs 2026-05-12 22:18:58 +02:00
Laurent Trinques efa74dd0f5 Update QElectroTech.wxs 2026-05-12 22:12:42 +02:00
Laurent Trinques 703797bb97 Update QElectroTech.wxs 2026-05-12 22:08:24 +02:00
Laurent Trinques ef75ee736a Update windows-msi.yml 2026-05-12 22:02:53 +02:00
Laurent Trinques 15e623ac5f Update QElectroTech.wxs 2026-05-12 21:58:07 +02:00
Laurent Trinques df82a1125d Update windows-msi.yml 2026-05-12 21:56:59 +02:00
Laurent Trinques 7edc2e0241 Update windows-msi.yml 2026-05-12 21:50:32 +02:00
Laurent Trinques 55ae3fc3c6 Update QElectroTech.wxs 2026-05-12 21:46:11 +02:00
Laurent Trinques 2f72e6164c Update windows-msi.yml
Removal of all envs: WIX_ACCEPT_EULA: true (does not work)
Addition of a dedicated ‘Accept WiX EULA’ step with wix eula accept wix7 before any other WiX command — this is the official CI/CD method, which writes a sentinel file to the user profile, thereby authorising all subsequent WiX commands in the same job.
2026-05-12 21:39:36 +02:00
Laurent Trinques e40f9c6b72 Update windows-msi.yml 2026-05-12 21:33:05 +02:00
Laurent Trinques ef261a7afd Update windows-msi.yml
Remove --accept-eula on dotnet tool install
2026-05-12 21:25:57 +02:00
Laurent Trinques 1550944011 Update windows-msi.yml
dotnet tool install --global wix --version 7.0.0 --accept-eula
wix extension add WixToolset.UI.wixext/7.0.0 --accept-eula
wix build ... --accept-eula
2026-05-12 20:55:22 +02:00
Laurent Trinques 32733187b8 Update windows-build.yml
Windows Build / build-windows (push) Has been cancelled
Windows Build / deploy-pages (push) Has been cancelled
2026-05-12 20:12:02 +02:00
Laurent Trinques 79542edd3b Try to build Windows msi files 2026-05-12 19:50:20 +02:00
Laurent Trinques 9e0ec69c61 Add windows spec files for msi test 2026-05-12 19:49:34 +02:00
Laurent Trinques 2e0a1a55e3 Test Windows VS2026 migration on GitHUB action 2026-05-12 19:43:06 +02:00
Laurent Trinques 308f2ea838 Update windows-build.yml 2026-05-12 15:01:21 +02:00
Laurent Trinques 9a9a5446cf Update windows-build.yml 2026-05-12 14:53:03 +02:00
Laurent Trinques 0f8d835a1b git submodule update --remote elements 2026-05-12 14:22:35 +02:00
Laurent Trinques 663336a7bc Update windows-build.yml
Windows Build / build-windows (push) Has been cancelled
Windows Build / deploy-pages (push) Has been cancelled
2026-05-11 15:00:59 +02:00
Laurent Trinques 4fab90d5b9 Update windows-build.yml
Windows Build / build-windows (push) Has been cancelled
Windows Build / deploy-pages (push) Has been cancelled
2026-05-10 21:27:10 +02:00
Laurent Trinques f67df92f0e Update windows-build.yml
Windows Build / build-windows (push) Has been cancelled
Windows Build / deploy-pages (push) Has been cancelled
Use 7-Zip is already installed on all GitHub Windows computers (C:\Program Files\7-Zip\7z.exe), and it is much faster than Compress-Archive when dealing with 9,931 files.
2026-05-10 13:04:31 +02:00
Laurent Trinques 7e2c2cccf8 Update windows-build.yml
Add Ready_to_use packages
2026-05-10 12:42:30 +02:00
74 changed files with 15732 additions and 12614 deletions
+112
View File
@@ -0,0 +1,112 @@
name: Test Windows VS2026 migration
# Ce workflow vérifie que le build QElectroTech fonctionne sur l'image
# windows-2025-vs2026, avant la migration forcée du 8 juin 2026.
# Il peut être supprimé une fois la migration confirmée OK.
on:
workflow_dispatch: # déclenchement manuel uniquement
schedule:
- cron: '0 4 * * 1' # chaque lundi à 4h00 UTC (optionnel)
jobs:
test-vs2026:
name: Build on windows-2025-vs2026
runs-on: windows-2025-vs2026 # <-- image avec VS 2026
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Install MSYS2 + dependencies
uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
update: false
install: >-
mingw-w64-ucrt-x86_64-gcc
mingw-w64-ucrt-x86_64-cmake
mingw-w64-ucrt-x86_64-ninja
mingw-w64-ucrt-x86_64-qt5-base
mingw-w64-ucrt-x86_64-qt5-svg
mingw-w64-ucrt-x86_64-qt5-tools
mingw-w64-ucrt-x86_64-qt5-translations
mingw-w64-ucrt-x86_64-sqlite3
mingw-w64-ucrt-x86_64-pkgconf
mingw-w64-ucrt-x86_64-extra-cmake-modules
mingw-w64-ucrt-x86_64-kwidgetsaddons-qt5
mingw-w64-ucrt-x86_64-kcoreaddons-qt5
mingw-w64-ucrt-x86_64-nsis
mingw-w64-ucrt-x86_64-ccache
mingw-w64-ucrt-x86_64-7zip
git
- name: Force Qt5 (remove Qt6 interference)
shell: msys2 {0}
run: |
rm -rf /ucrt64/lib/cmake/Qt6 || true
pacman -R --noconfirm mingw-w64-ucrt-x86_64-qt6-tools 2>/dev/null || true
- name: Cache ccache
uses: actions/cache@v4
with:
path: C:\Users\runneradmin\AppData\Local\ccache
key: ccache-vs2026-${{ runner.os }}-${{ github.sha }}
restore-keys: |
ccache-vs2026-${{ runner.os }}-
- name: Configure (CMake)
shell: msys2 {0}
run: |
set -euo pipefail
cmake -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DQt5_DIR=/ucrt64/lib/cmake/Qt5 \
-DQT_VERSION_MAJOR=5 \
-DCMAKE_DISABLE_FIND_PACKAGE_Qt6=ON \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_FLAGS="-DQET_EXPORT_PROJECT_DB" \
-DBUILD_TESTING=OFF \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_C_COMPILER_LAUNCHER=/ucrt64/bin/ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=/ucrt64/bin/ccache \
-DSQLite3_INCLUDE_DIR=/ucrt64/include \
-DSQLite3_LIBRARY=/ucrt64/lib/libsqlite3.dll.a \
- name: Build
shell: msys2 {0}
run: |
set -euo pipefail
cmake --build build --parallel $(nproc)
- name: Verify executable
shell: msys2 {0}
run: |
set -euo pipefail
EXE=$(find build -name "qelectrotech.exe" | head -1)
if [ -z "$EXE" ]; then
echo "ERROR: qelectrotech.exe introuvable après le build"
exit 1
fi
SIZE=$(stat -c%s "$EXE")
echo "Executable trouvé : $EXE ($SIZE octets)"
if [ "$SIZE" -lt 100000 ]; then
echo "ERROR: exe trop petit ($SIZE octets), build probablement incomplet"
exit 1
fi
echo "BUILD VS2026 : OK ✓"
- name: Summary
if: always()
shell: msys2 {0}
run: |
echo "=== Résumé de compatibilité VS2026 ==="
gcc --version
cmake --version
ninja --version
echo "Image runner : windows-2025-vs2026"
echo "Date du test : $(date -u)"
+62 -191
View File
@@ -1,11 +1,9 @@
name: Windows Build
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
workflow_dispatch:
schedule:
- cron: '0 2 * * 1' # Every Monday at 2:00 UTC
workflow_dispatch: # Manual trigger available at any time
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -316,11 +314,30 @@ jobs:
nsis_root/files/bin/
if-no-files-found: warn
- name: Zip portable (readytouse)
id: zip_portable
shell: pwsh
run: |
$version = "${{ steps.qet_version.outputs.base_version }}"
$head = "${{ steps.qet_version.outputs.head }}"
$zipName = "qelectrotech-${version}+git${head}-x86-win64-readytouse.zip"
$src = "$env:GITHUB_WORKSPACE\nsis_root\files"
$dst = "$env:GITHUB_WORKSPACE\dist\$zipName"
$7z = "C:\Program Files\7-Zip\7z.exe"
New-Item -ItemType Directory -Force -Path "$env:GITHUB_WORKSPACE\dist" | Out-Null
& $7z a -tzip -mx=5 -mmt=on $dst "$src\*"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$sizeMB = [math]::Round((Get-Item $dst).Length / 1MB, 1)
Write-Output "ZIP created: $zipName ($sizeMB MB)"
"zip_name=$zipName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
- name: Upload portable (files/ without installer)
uses: actions/upload-artifact@v4
with:
name: qelectrotech-${{ steps.qet_version.outputs.base_version }}+git${{ steps.qet_version.outputs.head }}-x86-win64-readytouse
path: nsis_root/files/
path: dist/${{ steps.zip_portable.outputs.zip_name }}
retention-days: 14
- name: Upload NSIS installer
@@ -330,6 +347,13 @@ jobs:
path: dist/Installer_*.exe
retention-days: 14
- name: Upload portable (nom fixe pour le workflow MSI)
uses: actions/upload-artifact@v4
with:
name: qelectrotech-windows-portable
path: nsis_root/files/
retention-days: 14
# ---------------------------------------------------------------------------
# Job 2 : Publie une release nightly + page GitHub Pages
# Ne tourne que sur push master (pas sur les PRs)
@@ -342,11 +366,13 @@ jobs:
contents: write
steps:
- name: Checkout gh-pages branch
- name: Checkout master (for build-aux scripts)
uses: actions/checkout@v4
with:
ref: gh-pages
path: gh-pages
ref: master
path: source
sparse-checkout: build-aux/generate-page.py
sparse-checkout-cone-mode: false
- name: Download installer artifact
uses: actions/download-artifact@v4
@@ -354,6 +380,27 @@ jobs:
name: qelectrotech-windows-installer
path: downloaded/installer/
- name: Download portable artifact
uses: actions/download-artifact@v4
with:
pattern: qelectrotech-*-readytouse
path: downloaded/portable/
merge-multiple: true
- name: Delete old nightly assets (.exe and .zip)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
run: |
# Fetch existing .exe and .zip asset names and delete them
gh release view nightly --repo "$REPO" --json assets \
--jq '.assets[] | select(.name | test("\\.(exe|zip)$")) | .name' \
| while read -r name; do
echo "Deleting old asset: $name"
gh release delete-asset nightly "$name" --repo "$REPO" --yes
done
echo "Old .exe and .zip assets deleted."
- name: Update nightly release
uses: softprops/action-gh-release@v2
with:
@@ -368,190 +415,14 @@ jobs:
| **Run** | https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} |
| **Date** | ${{ github.event.head_commit.timestamp }} |
> ⚠️ This release is overwritten on every push to `master`.
> ⚠️ This is a development version; it introduces new features you want, but may cause bugs that have not yet been identified yet.
> For stable releases, see the [Releases page](https://github.com/${{ github.repository }}/releases).
prerelease: true
make_latest: false
files: downloaded/installer/*.exe
files: |
downloaded/installer/*.exe
downloaded/portable/*.zip
token: ${{ secrets.GITHUB_TOKEN }}
- name: Generate download page (index.html)
run: |
DATE=$(date -u '+%Y-%m-%d %H:%M UTC')
SHORT="${{ github.sha }}"
SHORT="${SHORT:0:7}"
REPO="${{ github.repository }}"
RUN_URL="https://github.com/$REPO/actions/runs/${{ github.run_id }}"
EXE_NAME=$(ls downloaded/installer/*.exe | xargs -I{} basename {})
INSTALLER_URL="https://github.com/$REPO/releases/download/nightly/$EXE_NAME"
mkdir -p gh-pages
cat > gh-pages/index.html << HTMLEOF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QElectroTech Nightly Builds</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f0f4f8;
color: #2d3748;
min-height: 100vh;
}
header {
background: linear-gradient(135deg, #1a365d 0%, #2b6cb0 100%);
color: white;
padding: 48px 24px 40px;
text-align: center;
}
header h1 { font-size: 2.2em; letter-spacing: -0.5px; margin-bottom: 8px; }
header p { opacity: 0.8; font-size: 1.05em; }
main {
max-width: 680px;
margin: 40px auto;
padding: 0 20px 60px;
}
.card {
background: white;
border-radius: 12px;
padding: 28px;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
.card h2 {
font-size: 1em;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #718096;
margin-bottom: 16px;
}
.meta {
font-size: 0.875em;
color: #4a5568;
line-height: 1.8;
margin-bottom: 20px;
}
.meta a { color: #2b6cb0; text-decoration: none; }
.meta a:hover { text-decoration: underline; }
.badge {
display: inline-block;
background: #ebf8ff;
color: #2b6cb0;
border-radius: 4px;
font-size: 0.8em;
font-weight: 600;
padding: 2px 8px;
margin-left: 6px;
vertical-align: middle;
}
.warning {
background: #fffbeb;
border-left: 4px solid #f6ad55;
border-radius: 4px;
padding: 12px 16px;
font-size: 0.875em;
color: #744210;
margin-bottom: 24px;
line-height: 1.5;
}
.warning a { color: #c05621; }
.downloads { display: flex; flex-direction: column; gap: 12px; }
.btn {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 20px;
border-radius: 8px;
font-size: 0.95em;
font-weight: 600;
text-decoration: none;
transition: transform 0.1s, box-shadow 0.1s;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.btn-primary { background: #2b6cb0; color: white; }
.btn-secondary { background: #edf2f7; color: #2d3748; }
.btn-icon { font-size: 1.3em; }
.btn-text small {
display: block;
font-weight: 400;
font-size: 0.8em;
opacity: 0.75;
margin-top: 1px;
}
footer {
text-align: center;
font-size: 0.8em;
color: #a0aec0;
padding: 32px 0 0;
}
footer a { color: #718096; text-decoration: none; }
</style>
</head>
<body>
<header>
<h1>⚡ QElectroTech</h1>
<p>Nightly Windows Builds</p>
</header>
<main>
<div class="card">
<h2>Build info</h2>
<div class="meta">
📅 &nbsp;<strong>$DATE</strong><br>
🔀 &nbsp;Commit <a href="https://github.com/$REPO/commit/${{ github.sha }}"><code>$SHORT</code></a><br>
🔧 &nbsp;<a href="$RUN_URL">CI Run #${{ github.run_number }}</a>
<span class="badge">nightly</span>
</div>
<div class="warning">
⚠️ These builds are generated automatically on every commit to <code>master</code>
and may be unstable or incomplete.
For production use, download a
<a href="https://github.com/$REPO/releases">stable release</a>.
</div>
</div>
<div class="card">
<h2>🪟 Windows — x86_64</h2>
<div class="downloads">
<a class="btn btn-primary" href="$INSTALLER_URL">
<span class="btn-icon">⬇</span>
<span class="btn-text">
Windows Installer
<small>.exe — recommended, includes all dependencies</small>
</span>
</a>
<a class="btn btn-secondary" href="https://github.com/$REPO/releases/tag/nightly">
<span class="btn-icon">📦</span>
<span class="btn-text">
All nightly files on GitHub
<small>Release page with checksums</small>
</span>
</a>
</div>
</div>
</main>
<footer>
Auto-generated by GitHub Actions &nbsp;·&nbsp;
<a href="https://github.com/$REPO">Source on GitHub</a> &nbsp;·&nbsp;
<a href="https://qelectrotech.org">qelectrotech.org</a>
</footer>
</body>
</html>
HTMLEOF
- name: Commit and push to gh-pages
run: |
cd gh-pages
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add index.html
git diff --staged --quiet \
|| git commit -m "nightly: build #${{ github.run_number }} (${{ github.sha }})"
git push origin gh-pages
# GitHub Pages is generated and deployed by windows-msi.yml
# after the MSI upload, so that all 3 URLs (exe, zip, msi) are known.
+366
View File
@@ -0,0 +1,366 @@
name: Windows MSI (WiX v7)
on:
# Triggered automatically after a successful Windows build on master
workflow_run:
workflows: ["Windows Build"]
types: [completed]
branches: [master]
# Manual trigger still available (e.g. for a specific run_id)
workflow_dispatch:
inputs:
run_id:
description: "Run ID of 'Windows Build' (leave empty for automatic)"
required: false
default: ""
jobs:
build-msi:
name: Build MSI with WiX v7
runs-on: windows-latest
# Only runs if Windows Build succeeded (or triggered manually)
if: >
github.event_name == 'workflow_dispatch' ||
github.event.workflow_run.conclusion == 'success'
permissions:
contents: write
pages: write
id-token: write
steps:
# ----------------------------------------------------------------
# 1. Checkout (to retrieve QElectroTech.wxs and sources)
# ----------------------------------------------------------------
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# ----------------------------------------------------------------
# 2. Download the portable artifact from the main build
# ----------------------------------------------------------------
- name: Download portable artifact
uses: actions/download-artifact@v4
with:
name: qelectrotech-windows-portable
path: artifact\files
run-id: ${{ github.event.workflow_run.id || github.event.inputs.run_id || github.run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }}
# ----------------------------------------------------------------
# 3. Extract version from sources
# ----------------------------------------------------------------
- name: Extract version
id: version
shell: pwsh
run: |
$src = Get-Content "sources\qetversion.cpp" -Raw -ErrorAction SilentlyContinue
if ($src -match 'return QVersionNumber\{([^}]+)\}') {
$ver = $Matches[1] -replace '\s','' -replace ',','.'
} else {
$cmake = Get-Content "CMakeLists.txt" -Raw
if ($cmake -match 'project\s*\([^)]*VERSION\s+([\d]+\.[\d]+\.[\d]+)') {
$ver = $Matches[1]
} else {
$ver = "0.0.0"
}
}
$verMsi = "$ver.0"
$sha = git rev-parse --short HEAD 2>$null
if (-not $sha) { $sha = "unknown" }
$count = git rev-list HEAD --count 2>$null
$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
echo "VERSION_DISPLAY=$verDisplay" >> $env:GITHUB_OUTPUT
echo "PRODUCT_GUID=$productGuid" >> $env:GITHUB_OUTPUT
Write-Host "Version MSI : $verMsi"
Write-Host "Version display : $verDisplay"
Write-Host "Product GUID : $productGuid"
# ----------------------------------------------------------------
# 4. Install WiX v7, accept EULA and install WixUI extension
# ----------------------------------------------------------------
- name: Install WiX v7
shell: pwsh
run: |
dotnet tool install --global wix --version 7.0.0
$toolsPath = [System.IO.Path]::Combine($env:USERPROFILE, '.dotnet', 'tools')
$env:PATH = "$toolsPath;$env:PATH"
echo $toolsPath >> $env:GITHUB_PATH
wix eula accept wix7
wix extension add WixToolset.UI.wixext/7.0.0
wix extension add WixToolset.Util.wixext/7.0.0
Write-Host "WiX v7 installed, EULA accepted, UI extension added."
# ----------------------------------------------------------------
# 5. Check that the WXS file exists in the repository
# ----------------------------------------------------------------
- name: Check WXS file
shell: pwsh
run: |
$wxs = "build-aux\windows\QElectroTech.wxs"
if (-not (Test-Path $wxs)) {
Write-Error "WXS file not found: $wxs"
exit 1
}
Write-Host "WXS found: $wxs"
# ----------------------------------------------------------------
# 6. Check the artifact structure and locate files/
# ----------------------------------------------------------------
- name: Check artifact structure
shell: pwsh
run: |
Write-Host "=== Contents of artifact\files (2 levels) ==="
Get-ChildItem -Path "artifact\files" -Depth 2 |
Select-Object FullName | Format-Table -AutoSize
$exe = Get-ChildItem -Path "artifact\files" -Filter "qelectrotech.exe" -Recurse | Select-Object -First 1
if (-not $exe) {
$exe = Get-ChildItem -Path "artifact\files" -Filter "QElectroTech.exe" -Recurse | Select-Object -First 1
}
if (-not $exe) {
Write-Error "qelectrotech.exe not found in artifact"
exit 1
}
Write-Host "Executable: $($exe.FullName) ($([math]::Round($exe.Length/1MB,1)) MB)"
$binDir = $exe.Directory.FullName
$filesDir = Split-Path $binDir -Parent
echo "FILES_DIR=$filesDir" >> $env:GITHUB_ENV
Write-Host "FILES_DIR: $filesDir"
# ----------------------------------------------------------------
# 7. Convert LICENSE (GPL-2) to RTF for the WixUI licence screen
# ----------------------------------------------------------------
- name: Convert LICENSE to RTF
shell: pwsh
run: |
$licSrc = "LICENSE"
$licRtf = "$env:TEMP\License.rtf"
if (-not (Test-Path $licSrc)) {
Write-Error "LICENSE file not found in repository root"
exit 1
}
$lines = Get-Content $licSrc -Encoding UTF8
$rtf = New-Object System.Text.StringBuilder
[void]$rtf.AppendLine('{\rtf1\ansi\ansicpg1252\deff0')
[void]$rtf.AppendLine('{\fonttbl{\f0\fmodern\fprq1\fcharset0 Courier New;}}')
[void]$rtf.AppendLine('{\colortbl;\red0\green0\blue0;}')
[void]$rtf.AppendLine('\f0\fs18\cf1')
foreach ($line in $lines) {
$escaped = $line `
-replace '\\', '\\\\' `
-replace '\{', '\{' `
-replace '\}', '\}'
[void]$rtf.AppendLine("$escaped\par")
}
[void]$rtf.AppendLine('}')
[System.IO.File]::WriteAllText($licRtf, $rtf.ToString(), [System.Text.Encoding]::ASCII)
echo "LICENSE_RTF=$licRtf" >> $env:GITHUB_ENV
Write-Host "License.rtf generated: $licRtf ($([math]::Round((Get-Item $licRtf).Length/1KB,1)) KB)"
# ----------------------------------------------------------------
# 8. Remove Lancer QET.bat from the artifact
# The MSI does not use the .bat: shortcuts point directly to
# qelectrotech.exe, and elements\ is set read-only via a
# CustomAction in QElectroTech.wxs.
# The .bat is kept as-is in the ZIP portable build.
# ----------------------------------------------------------------
- name: Remove Lancer QET.bat from artifact
shell: pwsh
run: |
$bat = "$env:FILES_DIR\Lancer QET.bat"
if (Test-Path $bat) {
Remove-Item $bat -Force
Write-Host "Lancer QET.bat removed from artifact (MSI uses direct exe shortcut)."
} else {
Write-Host "Lancer QET.bat not found in artifact (already absent)."
}
# ----------------------------------------------------------------
# 9. Build the MSI
# ----------------------------------------------------------------
- name: Build MSI
shell: pwsh
run: |
$version = "${{ steps.version.outputs.VERSION_MSI }}"
$verDisplay = "${{ steps.version.outputs.VERSION_DISPLAY }}"
$filesDir = $env:FILES_DIR
$licRtf = $env:LICENSE_RTF
$wxs = "build-aux\windows\QElectroTech.wxs"
$outputName = "QElectroTech-${verDisplay}.msi"
New-Item -ItemType Directory -Force -Path "dist" | Out-Null
Write-Host "=== wix build ==="
Write-Host " WXS : $wxs"
Write-Host " Version : $version"
Write-Host " FilesDir : $filesDir"
Write-Host " LicenseRtf : $licRtf"
Write-Host " Output : dist\$outputName"
$productGuid = "${{ steps.version.outputs.PRODUCT_GUID }}"
wix build $wxs `
-arch x64 `
-d "Version=$version" `
-d "ProductVersion=$verDisplay" `
-d "ProductCode=$productGuid" `
-d "FilesDir=$filesDir" `
-d "LicenseRtf=$licRtf" `
-ext WixToolset.UI.wixext `
-ext WixToolset.Util.wixext `
-o "dist\$outputName"
if (-not (Test-Path "dist\$outputName")) {
Write-Error "MSI not generated: dist\$outputName"
exit 1
}
$size = [math]::Round((Get-Item "dist\$outputName").Length / 1MB, 1)
Write-Host "MSI generated: dist\$outputName ($size MB) ✓"
echo "MSI_NAME=$outputName" >> $env:GITHUB_ENV
# ----------------------------------------------------------------
# 10. Sign the MSI with SignPath
# ----------------------------------------------------------------
- name: Install SignPath PowerShell module
shell: pwsh
run: |
Install-Module -Name SignPath -Force -Scope CurrentUser
- name: Sign MSI with SignPath
shell: pwsh
run: |
$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
# ----------------------------------------------------------------
- name: Upload MSI artifact
uses: actions/upload-artifact@v4
with:
name: qelectrotech-windows-msi
path: dist\*.msi
retention-days: 14
if-no-files-found: error
# ----------------------------------------------------------------
# 11. Delete old .msi asset then upload new MSI to nightly release
# ----------------------------------------------------------------
- name: Delete old nightly .msi asset
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
run: |
gh release view nightly --repo "$REPO" --json assets \
--jq '.assets[] | select(.name | test("\\.msi$")) | .name' \
| while read -r name; do
echo "Deleting old asset: $name"
gh release delete-asset nightly "$name" --repo "$REPO" --yes
done
echo "Old .msi assets deleted."
shell: bash
- name: Upload MSI to nightly release
uses: softprops/action-gh-release@v2
with:
tag_name: nightly
files: dist/*.msi
fail_on_unmatched_files: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ----------------------------------------------------------------
# 12. Summary
# ----------------------------------------------------------------
- name: Summary
if: always()
shell: pwsh
run: |
Write-Host "=== MSI build summary ==="
Write-Host "Version : ${{ steps.version.outputs.VERSION_DISPLAY }}"
Write-Host "WiX : v7.0.0"
Write-Host "Runner image : ${{ runner.os }} / ${{ runner.arch }}"
if (Test-Path "dist\$env:MSI_NAME") {
$size = [math]::Round((Get-Item "dist\$env:MSI_NAME").Length / 1MB, 1)
Write-Host "MSI : $env:MSI_NAME ($size MB) ✓"
} else {
Write-Host "MSI : FAILED ✗"
}
# ----------------------------------------------------------------
# 13. Generate and deploy the GitHub Pages download page
# ----------------------------------------------------------------
- name: Checkout (for generate-page.py)
uses: actions/checkout@v4
with:
ref: master
path: source
sparse-checkout: build-aux/generate-page.py
sparse-checkout-cone-mode: false
- name: Generate download page (index.html)
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
REPO="${{ github.repository }}"
ASSETS=$(gh release view nightly --repo "$REPO" --json assets --jq '.assets[].name')
EXE_NAME=$(echo "$ASSETS" | grep '\.exe$' | head -1)
ZIP_NAME=$(echo "$ASSETS" | grep '\.zip$' | head -1)
MSI_NAME=$(echo "$ASSETS" | grep '\.msi$' | head -1 || echo "")
BASE="https://github.com/$REPO/releases/download/nightly"
INSTALLER_URL="$BASE/$EXE_NAME"
PORTABLE_URL="$BASE/$ZIP_NAME"
MSI_URL=""
[ -n "$MSI_NAME" ] && MSI_URL="$BASE/$MSI_NAME"
SHA="${{ github.event.workflow_run.head_sha || github.sha }}"
SHORT="${SHA:0:7}"
DATE=$(date -u '+%Y-%m-%d %H:%M UTC')
RUN_URL="https://github.com/$REPO/actions/runs/${{ github.run_id }}"
RUN_NUMBER="${{ github.run_number }}"
export DATE SHORT REPO SHA RUN_URL RUN_NUMBER
export INSTALLER_URL PORTABLE_URL MSI_URL
python3 source/build-aux/generate-page.py
- name: Add .nojekyll
shell: bash
run: touch gh-pages/.nojekyll
- name: Upload GitHub Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: gh-pages/
- name: Deploy to GitHub Pages
uses: actions/deploy-pages@v4
+18 -5
View File
@@ -19,7 +19,7 @@ include(cmake/hoto_update_cmake_message.cmake)
cmake_minimum_required(VERSION 3.14...3.19 FATAL_ERROR)
project(qelectrotech
VERSION 0.9.0
VERSION 0.100.1
DESCRIPTION "QET is a CAD/CAE editor focusing on schematics drawing features."
HOMEPAGE_URL "https://qelectrotech.org/"
LANGUAGES CXX)
@@ -46,11 +46,9 @@ include(cmake/fetch_pugixml.cmake)
include(cmake/qet_compilation_vars.cmake)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
SET(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -68,9 +66,11 @@ find_package(
COMPONENTS
${QET_COMPONENTS}
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})
@@ -78,6 +78,19 @@ 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}
@@ -85,6 +98,7 @@ add_executable(
${QM_FILES}
${QET_DIR}/qelectrotech.qrc
)
endif()
target_link_libraries(
${PROJECT_NAME}
@@ -134,8 +148,8 @@ target_include_directories(
)
install(TARGETS ${PROJECT_NAME})
if (NOT MINGW)
if (NOT MINGW)
install(DIRECTORY ico/breeze-icons/16x16 DESTINATION ${QET_ICONS_PATH})
install(DIRECTORY ico/breeze-icons/22x22 DESTINATION ${QET_ICONS_PATH})
install(DIRECTORY ico/breeze-icons/32x32 DESTINATION ${QET_ICONS_PATH})
@@ -151,5 +165,4 @@ install(FILES misc/org.qelectrotech.qelectrotech.desktop DESTINATION share/appli
install(FILES misc/qelectrotech.xml DESTINATION share/mime/packages)
install(FILES misc/qelectrotech.appdata.xml DESTINATION ${QET_APPDATA_PATH})
install(FILES ${QM_FILES} DESTINATION ${QET_LANG_PATH})
endif()
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""
generate-page.py — Generates gh-pages/index.html for QElectroTech nightly builds.
Called from windows-build.yml deploy-pages job.
Environment variables required:
DATE, SHORT, REPO, SHA, RUN_URL, RUN_NUMBER,
INSTALLER_URL, PORTABLE_URL, MSI_URL (optional)
"""
import os
date = os.environ.get("DATE", "")
short = os.environ.get("SHORT", "")
repo = os.environ.get("REPO", "")
sha = os.environ.get("SHA", "")
run_url = os.environ.get("RUN_URL", "")
run_number = os.environ.get("RUN_NUMBER", "")
installer_url = os.environ.get("INSTALLER_URL", "")
portable_url = os.environ.get("PORTABLE_URL", "")
msi_url = os.environ.get("MSI_URL", "")
msi_block = ""
if msi_url:
msi_block = f"""
<a class="btn btn-msi" href="{msi_url}">
<span class="btn-icon">&#11015;</span>
<span class="btn-text">Windows Installer .msi<small>.msi &mdash; for enterprise / GPO deployment</small></span>
</a>"""
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QElectroTech &ndash; Nightly Builds</title>
<style>
*,*::before,*::after{{box-sizing:border-box;margin:0;padding:0}}
body{{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#f0f4f8;color:#2d3748;min-height:100vh}}
header{{background:linear-gradient(135deg,#1a365d 0%,#2b6cb0 100%);color:white;padding:48px 24px 40px;text-align:center}}
header h1{{font-size:2.2em;letter-spacing:-0.5px;margin-bottom:8px}}
header p{{opacity:.8;font-size:1.05em}}
main{{max-width:680px;margin:40px auto;padding:0 20px 60px}}
.card{{background:white;border-radius:12px;padding:28px;margin-bottom:24px;box-shadow:0 2px 12px rgba(0,0,0,.08)}}
.card h2{{font-size:1em;text-transform:uppercase;letter-spacing:.06em;color:#718096;margin-bottom:16px}}
.meta{{font-size:.875em;color:#4a5568;line-height:1.8;margin-bottom:20px}}
.meta a{{color:#2b6cb0;text-decoration:none}}
.meta a:hover{{text-decoration:underline}}
.badge{{display:inline-block;background:#ebf8ff;color:#2b6cb0;border-radius:4px;font-size:.8em;font-weight:600;padding:2px 8px;margin-left:6px;vertical-align:middle}}
.warning{{background:#fffbeb;border-left:4px solid #f6ad55;border-radius:4px;padding:12px 16px;font-size:.875em;color:#744210;margin-bottom:24px;line-height:1.5}}
.warning a{{color:#c05621}}
.downloads{{display:flex;flex-direction:column;gap:12px}}
.btn{{display:flex;align-items:center;gap:12px;padding:14px 20px;border-radius:8px;font-size:.95em;font-weight:600;text-decoration:none;transition:transform .1s,box-shadow .1s}}
.btn:hover{{transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,.15)}}
.btn-primary{{background:#2b6cb0;color:white}}
.btn-msi{{background:#6b46c1;color:white}}
.btn-secondary{{background:#edf2f7;color:#2d3748}}
.btn-icon{{font-size:1.3em}}
.btn-text small{{display:block;font-weight:400;font-size:.8em;opacity:.75;margin-top:1px}}
footer{{text-align:center;font-size:.8em;color:#a0aec0;padding:32px 0 0}}
footer a{{color:#718096;text-decoration:none}}
</style>
</head>
<body>
<header>
<h1>&#9889; QElectroTech</h1>
<p>Nightly Windows Builds</p>
</header>
<main>
<div class="card">
<h2>Build info</h2>
<div class="meta">
&#128197; &nbsp;<strong>{date}</strong><br>
&#128256; &nbsp;Commit <a href="https://github.com/{repo}/commit/{sha}"><code>{short}</code></a><br>
&#128295; &nbsp;<a href="{run_url}">CI Run #{run_number}</a>
<span class="badge">nightly</span>
</div>
<div class="warning">
&#9888;&#65039; This is a development version; it introduces new features you want,
but may cause bugs that have not yet been identified yet in <code>master</code>.
For production use, download a <a href="https://github.com/{repo}/releases">stable release</a>.
</div>
</div>
<div class="card">
<h2>&#127993; Windows &mdash; x86_64</h2>
<div class="downloads">
<a class="btn btn-primary" href="{installer_url}">
<span class="btn-icon">&#11015;</span>
<span class="btn-text">Windows Installer<small>.exe &mdash; recommended, includes all dependencies</small></span>
</a>
{msi_block}
<a class="btn btn-secondary" href="{portable_url}">
<span class="btn-icon">&#128230;</span>
<span class="btn-text">Windows Portable<small>.zip &mdash; no installation required, extract and run &quot;Lancer QET.bat&quot;</small></span>
</a>
<a class="btn btn-secondary" href="https://github.com/{repo}/releases/tag/nightly">
<span class="btn-icon">&#128230;</span>
<span class="btn-text">All nightly files on GitHub<small>Release page with checksums</small></span>
</a>
</div>
</div>
</main>
<footer>
Auto-generated by GitHub Actions &nbsp;&middot;&nbsp;
<a href="https://github.com/{repo}">Source on GitHub</a> &nbsp;&middot;&nbsp;
<a href="https://qelectrotech.org">qelectrotech.org</a>
</footer>
</body>
</html>"""
os.makedirs("gh-pages", exist_ok=True)
with open("gh-pages/index.html", "w", encoding="utf-8") as f:
f.write(html)
print("index.html written OK")
+161
View File
@@ -0,0 +1,161 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
QElectroTech.wxs - WiX v7 installer definition
Generates a self-contained x64 MSI for QElectroTech (MSYS2/UCRT64 build).
Variables passed from the wix build command line:
-d FilesDir=<absolute path to artifact\files>
-d Version=<numeric version e.g. 0.100.1.0>
-d ProductVersion=<display version e.g. 0.100.1-r8770-abc1234_x86_64-win64>
-d LicenseRtf=<absolute path to License.rtf>
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<Package
Name="QElectroTech"
Manufacturer="QElectroTech Team"
Version="$(var.Version)"
ProductCode="$(var.ProductCode)"
UpgradeCode="A1B2C3D4-E5F6-7890-ABCD-EF1234567890"
Language="1033"
Codepage="1252"
InstallerVersion="500"
Scope="perMachine">
<!-- Embed all cabinet files inside the MSI (self-contained installer) -->
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
<!-- In-place upgrade: automatically uninstalls the previous version -->
<MajorUpgrade
DowngradeErrorMessage="A newer version of QElectroTech is already installed."
AllowSameVersionUpgrades="yes"
Schedule="afterInstallInitialize" />
<!-- Installation directory -->
<StandardDirectory Id="ProgramFiles64Folder">
<Directory Id="INSTALLDIR" Name="QElectroTech" />
</StandardDirectory>
<!-- ============================================================
All application files harvested in one pass from files\**
(Lancer QET.bat has been removed from the artifact before
this build — see windows-msi.yml step "Remove Lancer QET.bat")
============================================================ -->
<ComponentGroup Id="CG_AllFiles" Directory="INSTALLDIR">
<Files Include="$(var.FilesDir)\**" />
</ComponentGroup>
<!-- ============================================================
Desktop + Start Menu shortcuts
Point directly to qelectrotech.exe with all required arguments.
No .bat wrapper needed.
============================================================ -->
<ComponentGroup Id="CG_Shortcuts" Directory="INSTALLDIR">
<Component Id="C_ShortcutDesktop" Guid="F1A2B3C4-D5E6-7890-5678-012345678901">
<Shortcut Id="DesktopShortcut"
Directory="DesktopFolder"
Name="QElectroTech"
Target="[INSTALLDIR]bin\qelectrotech.exe"
Arguments="--common-elements-dir=&quot;[INSTALLDIR]elements/&quot; --common-tbt-dir=&quot;[INSTALLDIR]titleblocks/&quot; --lang-dir=&quot;[INSTALLDIR]lang/&quot; -style windowsvista"
Icon="qet.ico"
WorkingDirectory="INSTALLDIR" />
<RegistryValue Root="HKCU"
Key="Software\QElectroTech"
Name="DesktopShortcut"
Type="integer" Value="1"
KeyPath="yes" />
</Component>
<Component Id="C_ShortcutStartMenu" Guid="A2B3C4D5-E6F7-8901-6789-123456789012">
<Shortcut Id="StartMenuShortcut"
Directory="ProgramMenuFolder"
Name="QElectroTech"
Target="[INSTALLDIR]bin\qelectrotech.exe"
Arguments="--common-elements-dir=&quot;[INSTALLDIR]elements/&quot; --common-tbt-dir=&quot;[INSTALLDIR]titleblocks/&quot; --lang-dir=&quot;[INSTALLDIR]lang/&quot; -style windowsvista"
Icon="qet.ico"
WorkingDirectory="INSTALLDIR" />
<RegistryValue Root="HKCU"
Key="Software\QElectroTech"
Name="StartMenuShortcut"
Type="integer" Value="1"
KeyPath="yes" />
</Component>
</ComponentGroup>
<!-- ============================================================
.qet file association
============================================================ -->
<ComponentGroup Id="CG_FileAssoc" Directory="INSTALLDIR">
<Component Id="C_FileAssoc" Guid="B3C4D5E6-F7A8-9012-7890-234567890123">
<RegistryValue Root="HKCR" Key=".qet" Type="string" Value="QElectroTech.Document" KeyPath="yes" />
<RegistryValue Root="HKCR" Key="QElectroTech.Document" Type="string" Value="QElectroTech Project" />
<RegistryValue Root="HKCR" Key="QElectroTech.Document\DefaultIcon" Type="string"
Value="[INSTALLDIR]ico\qelectrotech.ico" />
<RegistryValue Root="HKCR" Key="QElectroTech.Document\shell\open\command" Type="string"
Value="&quot;[INSTALLDIR]bin\qelectrotech.exe&quot; &quot;%1&quot;" />
</Component>
</ComponentGroup>
<!-- ============================================================
Icon for shortcuts
============================================================ -->
<Icon Id="qet.ico" SourceFile="$(var.FilesDir)\ico\qelectrotech.ico" />
<!-- ============================================================
CustomAction: set elements\ subtree read-only after install
Pattern used: two-step SetProperty + Exec
1. CA_ResolveElementsPath (immediate, SetProperty):
copies the runtime value of INSTALLDIR into the property
ELEMENTS_READONLY_CMD, building the full powershell command.
2. CA_SetElementsReadOnly (deferred, Execute="deferred"):
runs the command stored in ELEMENTS_READONLY_CMD.
This is the correct WiX v4/v7 pattern for passing a
directory path (resolved at runtime) to a deferred CA.
============================================================ -->
<!-- Step 1: build the powershell command with the resolved INSTALLDIR -->
<CustomAction Id="CA_ResolveElementsPath"
Property="CA_SetElementsReadOnly"
Value="powershell.exe -NonInteractive -NoProfile -WindowStyle Hidden -Command &quot;Get-ChildItem -LiteralPath '[INSTALLDIR]elements' -Recurse -File | ForEach-Object { `$_.IsReadOnly = `$true }&quot;" />
<!-- Step 2: deferred execution of the powershell command -->
<CustomAction Id="CA_SetElementsReadOnly"
BinaryRef="Wix4UtilCA_X86"
DllEntry="WixQuietExec"
Execute="deferred"
Impersonate="no"
Return="ignore" />
<InstallExecuteSequence>
<Custom Action="CA_ResolveElementsPath" After="InstallFiles" Condition="NOT Installed AND NOT REMOVE" />
<Custom Action="CA_SetElementsReadOnly" After="CA_ResolveElementsPath" Condition="NOT Installed AND NOT REMOVE" />
</InstallExecuteSequence>
<!-- ============================================================
Main feature (everything included)
============================================================ -->
<Feature Id="ProductFeature" Title="QElectroTech" Level="1">
<ComponentGroupRef Id="CG_AllFiles" />
<ComponentGroupRef Id="CG_Shortcuts" />
<ComponentGroupRef Id="CG_FileAssoc" />
</Feature>
<!-- WixUI_Minimal with QElectroTech GPL-2 license -->
<ui:WixUI Id="WixUI_Minimal" />
<!-- WixVariable without ui: namespace, at Package level -->
<WixVariable Id="WixUILicenseRtf" Value="$(var.LicenseRtf)" />
<!-- Properties shown in Programs and Features -->
<Property Id="ARPPRODUCTICON" Value="qet.ico" />
<Property Id="ARPHELPLINK" Value="https://qelectrotech.org" />
<Property Id="ARPURLINFOABOUT" Value="https://qelectrotech.org" />
<Property Id="ARPCOMMENTS" Value="Free electrical schematic editor" />
</Package>
</Wix>
+405 -354
View File
File diff suppressed because it is too large Load Diff
+405 -354
View File
File diff suppressed because it is too large Load Diff
+405 -354
View File
File diff suppressed because it is too large Load Diff
+405 -355
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+401 -350
View File
File diff suppressed because it is too large Load Diff
+405 -354
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+405 -354
View File
File diff suppressed because it is too large Load Diff
+405 -354
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+400 -349
View File
File diff suppressed because it is too large Load Diff
+400 -349
View File
File diff suppressed because it is too large Load Diff
+401 -350
View File
File diff suppressed because it is too large Load Diff
+406 -355
View File
File diff suppressed because it is too large Load Diff
+401 -350
View File
File diff suppressed because it is too large Load Diff
+405 -354
View File
File diff suppressed because it is too large Load Diff
+400 -349
View File
File diff suppressed because it is too large Load Diff
+401 -350
View File
File diff suppressed because it is too large Load Diff
+405 -354
View File
File diff suppressed because it is too large Load Diff
+400 -349
View File
File diff suppressed because it is too large Load Diff
+405 -354
View File
File diff suppressed because it is too large Load Diff
+405 -354
View File
File diff suppressed because it is too large Load Diff
+401 -350
View File
File diff suppressed because it is too large Load Diff
+405 -354
View File
File diff suppressed because it is too large Load Diff
+400 -349
View File
File diff suppressed because it is too large Load Diff
+405 -354
View File
File diff suppressed because it is too large Load Diff
+400 -349
View File
File diff suppressed because it is too large Load Diff
+400 -349
View File
File diff suppressed because it is too large Load Diff
+400 -349
View File
File diff suppressed because it is too large Load Diff
+401 -350
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1197 -1103
View File
File diff suppressed because it is too large Load Diff
+401 -350
View File
File diff suppressed because it is too large Load Diff
+401 -350
View File
File diff suppressed because it is too large Load Diff
+271 -99
View File
@@ -16,7 +16,6 @@
# along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
# Need homebrew and coreutils installed see <http://brew.sh>.
#Force MacOSX12.3.sdk
#see: https://www.downtowndougbrown.com/2023/08/how-to-create-a-qt-5-arm-intel-universal-binary-for-mac/
@@ -26,14 +25,20 @@ export DEVELOPER_DIR=/Applications/Xcode_14.01.app/Contents/Developer
APPNAME='qelectrotech'
BUNDLE=$APPNAME.app
APPBIN="$BUNDLE/Contents/MacOS/$APPNAME"
IDENTITY="Developer ID Application: Laurent TRINQUES (Y73WZ6WZ5X)"
# Emplacement du script
# Temp paths
RW_DMG="/tmp/qet_rw.dmg"
MOUNT_POINT="/tmp/qet_dmg_mount"
STAGING="/tmp/qet_dmg_staging"
# Script location
current_dir=$(dirname "$0")
# On se remet au depart
# Go back to repo root
cd "${current_dir}/../"
# Emplacement courant
# Current directory
current_dir=$(PWD)
@@ -47,10 +52,10 @@ echo "page in the Qt documentation for more information."
echo
echo "This script :"
echo "\t - update the git depot"
echo "\t - built the application bundle,"
echo "\t - build the application bundle,"
echo "\t - copy over required Qt frameworks,"
echo "\t - copy additional files: translations, titleblocks and elements,"
echo "\t - create image disk."
echo "\t - notarize the .app, then create a signed DMG."
echo
echo "Enjoy ;-)"
echo
@@ -70,25 +75,23 @@ echo
echo "______________________________________________________________"
echo "Run GIT:"
# Fait une mise à jour
git submodule init
git submodule update
git pull --recurse-submodules
git pull
#git checkout foliolist_position
# recupere le numero de la nouvelle revision
# Get revision number and version
GITCOMMIT=$(git rev-parse --short HEAD)
A=$(git rev-list HEAD --count)
HEAD=$(($A+473))
VERSION=$(cat sources/qetversion.cpp | grep "return QVersionNumber{"| head -n 1| awk -F "{" '{ print $2 }' | awk -F "}" '{ print $1 }' | sed -e 's/,/./g' -e 's/ //g')
#VERSION=$(cat sources/qetversion.cpp | grep "return QVersionNumber{ 0, "| head -n 1| cut -c32-40| sed -e 's/,/./g' -e 's/ //g') #Find major, minor, and micro version numbers in sources/qetversion.cp
# Tarball de la dernière revision déjà créé
if [ -e "build-aux/mac-osx/${APPNAME}-$VERSION-r$HEAD-arm64.zip" ] ; then
DMG_NAME="${APPNAME}-$VERSION-r$HEAD-arm64.dmg"
DMG_PATH="build-aux/mac-osx/$DMG_NAME"
# Check if already built
if [ -e "$DMG_PATH" ] ; then
echo "There are not new updates, make disk image can"
echo "take a lot of time (5 min). Can you continu?"
echo "[y/n]"
@@ -108,26 +111,25 @@ echo
echo "______________________________________________________________"
echo "Run make install:"
# pour effacer lancienne compilation
# Remove old bundle
if [ -d $BUNDLE ] ; then
echo "Removing hold bundle..."
echo "Removing old bundle..."
rm -rf $BUNDLE
fi
if [ -e Makefile ] ; then
echo "Removing hold Makefile..."
echo "Removing old Makefile..."
rm .qmake.stash
make clean
fi
# genere le Makefile
# Generate Makefile
echo "Generating new makefile..."
qmake -spec macx-clang
# compilation
# Compile
if [ -e Makefile.Release ] ; then
START_TIME=$SECONDS
# arret du script si erreur de compilation
testSuccessBuild () {
if [ $? -ne 0 ]; then
cleanVerionTag
@@ -138,7 +140,6 @@ if [ -e Makefile.Release ] ; then
fi
}
# utilise tout les coeurs pour une compilation plus rapide
coeur=$(sysctl hw.ncpu | awk '{print $2}')
if [ $? -ne 0 ]; then
make -f Makefile.Release
@@ -158,8 +159,7 @@ fi
cp -R ${current_dir}/misc/Info.plist qelectrotech.app/Contents/
cp -R ${current_dir}/ico/mac_icon/*.icns qelectrotech.app/Contents/Resources/
# On rajoute le numero de version pour "cmd + i"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION r$HEAD" "qelectrotech.app/Contents/Info.plist" # Version number
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION r$HEAD" "qelectrotech.app/Contents/Info.plist"
### copy over frameworks ############################################
@@ -173,38 +173,29 @@ if [ ! -d $BUNDLE ] ; then
echo "ERROR: cannot find application bundle \"$BUNDLE\" in current directory"
exit
fi
#~/Qt/5.5/clang_64/bin/macdeployqt $BUNDLE
macdeployqt $BUNDLE
### add file missing #######################################
### add missing files ###############################################
echo
echo "______________________________________________________________"
echo "Copy file missing:"
echo "Copy missing files:"
# Dossier à ajouter
QET_ELMT_DIR="${current_dir}/elements/"
QET_TBT_DIR="${current_dir}/titleblocks/"
QET_LANG_DIR="${current_dir}/lang/"
QET_EXAMPLES_DIR="${current_dir}/examples/"
QET_FONTS_DIR="${current_dir}/fonts/"
QET_LICENSES_DIR="${current_dir}/licenses/"
# Add new folder for Qt dialog translation see
## see <https://download.tuxfamily.org/qet/Qt_lang/>.
LANG_DIR="${current_dir}/lang1/"
if [ -d "${QET_ELMT_DIR}" ]; then
echo "Copying add elements in the bundle..."
#mkdir $BUNDLE/Contents/Resources/elements
echo "Copying elements in the bundle..."
cp -R ${QET_ELMT_DIR} $BUNDLE/Contents/Resources/elements
fi
if [ -d "${QET_TBT_DIR}" ]; then
echo "Copying titleblocks in the bundle..."
#mkdir $BUNDLE/Contents/Resources/titleblocks
cp -R ${QET_TBT_DIR} $BUNDLE/Contents/Resources/titleblocks
fi
@@ -215,23 +206,20 @@ if [ -d "${QET_LANG_DIR}" ]; then
fi
if [ -d "${LANG_DIR}" ]; then
echo "Copying translations in the bundle... "
echo "Copying extra translations in the bundle..."
cp ${current_dir}/lang1/*.qm $BUNDLE/Contents/Resources/lang
fi
if [ -d "${QET_EXAMPLES_DIR}" ]; then
echo "Copying examples in the bundle..."
mkdir $BUNDLE/Contents/Resources/examples
cp ${current_dir}/examples/*.qet $BUNDLE/Contents/Resources/examples
fi
if [ -d "${QET_FONTS_DIR}" ]; then
echo "Copying fonts in the bundle..."
mkdir $BUNDLE/Contents/Resources/fonts
cp ${current_dir}/fonts/*.ttf $BUNDLE/Contents/Resources/fonts
fi
if [ -d "${QET_LICENSES_DIR}" ]; then
@@ -240,98 +228,282 @@ if [ -d "${QET_LICENSES_DIR}" ]; then
cp -R -L ${QET_LICENSES_DIR} $BUNDLE/Contents/Resources/licenses
fi
codesign --force --deep --sign --timestamp -s "Developer ID Application: Laurent TRINQUES (Y73WZ6WZ5X)" --options=runtime $BUNDLE
### create zip tarball ###############################################
### Sign the bundle #################################################
# Sign in correct order: all dylibs first (including flat libs copied
# by macdeployqt from Homebrew), then frameworks, plugins, bundle last.
echo
echo "______________________________________________________________"
echo "Create zip tarball:"
echo "Code signing (dylibs -> frameworks -> plugins -> bundle):"
/usr/bin/ditto -c -k --keepParent $BUNDLE "build-aux/mac-osx/${APPNAME}-$VERSION-r$HEAD-arm64.zip"
# 1. Sign all flat .dylib files in Frameworks/
echo "-- Signing dylibs in Frameworks/..."
find "$BUNDLE/Contents/Frameworks" -name "*.dylib" | while read lib; do
echo " $(basename $lib)"
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
done
### notarize zip tarball ###############################################
echo -e "\033[1;31mWould you like to upload for Notarize packages "${APPNAME}"-"$VERSION"-"r$HEAD-arm64.zip", n/Y?.\033[m"
# 2. Sign .framework bundles
echo "-- Signing .framework bundles..."
find "$BUNDLE/Contents/Frameworks" -maxdepth 1 -name "*.framework" | while read fw; do
echo " $(basename $fw)"
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$fw"
done
# 3. Sign plugins
echo "-- Signing plugins..."
find "$BUNDLE/Contents/PlugIns" \( -name "*.dylib" -o -name "*.so" \) | while read lib; do
echo " $(basename $lib)"
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
done
# 4. Sign any dylibs in MacOS/
echo "-- Signing dylibs in MacOS/..."
find "$BUNDLE/Contents/MacOS" -name "*.dylib" | while read lib; do
echo " $(basename $lib)"
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
done
# 5. Sign the main executable explicitly
echo "-- Signing main executable..."
codesign --force --sign "$IDENTITY" --timestamp --options=runtime \
"$BUNDLE/Contents/MacOS/$APPNAME"
# 6. Sign the bundle itself last
echo "-- Signing bundle..."
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$BUNDLE"
# 7. Verify
echo
echo "Verifying bundle signature..."
codesign --verify --deep --strict --verbose=2 "$BUNDLE"
if [ $? -ne 0 ]; then
echo "ERROR: bundle signature verification failed, aborting."
exit 1
fi
spctl -a -vv "$BUNDLE"
echo "Bundle signature OK."
### Notarize the .app (via temporary ZIP) ###########################
echo
echo "______________________________________________________________"
echo "Create temporary ZIP for notarization:"
NOTARIZE_ZIP="/tmp/${APPNAME}-$VERSION-r$HEAD-arm64-notarize.zip"
/usr/bin/ditto -c -k --keepParent "$BUNDLE" "$NOTARIZE_ZIP"
echo -e "\033[1;31mWould you like to notarize the .app \"${APPNAME}-${VERSION}-r${HEAD}\", n/Y?\033[m"
read a
if [[ $a == "Y" || $a == "y" ]]; then
echo
echo "______________________________________________________________"
echo "Notarize zip tarball:"
xcrun notarytool submit build-aux/mac-osx/${APPNAME}-$VERSION-r$HEAD-arm64.zip --keychain-profile "org.qelectrotech" --wait
echo "Notarizing .app:"
xcrun notarytool submit "$NOTARIZE_ZIP" --keychain-profile "org.qelectrotech" --wait
if [ $? -ne 0 ]; then
echo "ERROR: notarization failed. Check the log with:"
echo " xcrun notarytool log <submission-id> --keychain-profile org.qelectrotech"
rm -f "$NOTARIZE_ZIP"
exit 1
fi
else
echo -e "\033[1;33mExit.\033[m"
fi
### The end, process is done ##########################################
echo "Cleaning up temporary notarization ZIP..."
rm -f "$NOTARIZE_ZIP"
echo
echo "______________________________________________________________"
echo "The process of creating deployable application zip is done."
echo The disque image is in the folder \'build-aux/mac-osx\'.
# Affiche les mise à jour depuis l'ancienne revision
#if [ ! $(($HEAD - $revAv)) -eq 0 ] ; then
# echo
# echo "There are new updates. This numero of revision is $HEAD."
# svn log -l $(($HEAD - $revAv))
#else
# echo
# echo "There are not new updates. This numero of revision is $HEAD."
# fi
# echo
### Staple the .app #################################################
# La version en local n'est pas conforme à la dernière version svn
# svnversion | grep -q '[MS:]' ; if [ $? -eq 0 ] ; then
# echo Please note that the latest \local version is $(svnversion).
# echo This is not the same version as the deposit.
# echo You can use \'svn diff\' to see the differences.
# echo And use \'svn revert \<fichier\>\' to delete the difference.
# echo To go back, you can use svn update -r 360
# echo to go to revision number 360.
# echo
#fi
# Clean up disk folder
echo 'Cleaning up... '
rm "build-aux/mac-osx/${APPNAME}-$VERSION-r$HEAD-arm64.zip"
# staple the app
echo -e "\033[1;31mWould you like to staple the app MacOS packages "${APPNAME}"-"$VERSION"-"r$HEAD", n/Y?.\033[m"
echo -e "\033[1;31mWould you like to staple the .app \"${APPNAME}-${VERSION}-r${HEAD}\", n/Y?\033[m"
read a
if [[ $a == "Y" || $a == "y" ]]; then
xcrun stapler staple -v $BUNDLE
xcrun stapler staple -v "$BUNDLE"
if [ $? -ne 0 ]; then
echo "ERROR: stapling .app failed."
exit 1
fi
echo "Verifying staple on .app..."
xcrun stapler validate -v "$BUNDLE"
spctl -a -vv "$BUNDLE"
echo ".app stapled OK."
else
echo -e "\033[1;33mExit.\033[m"
fi
### Create staging folder with Applications symlink #################
# The staging folder contains the .app and a symlink to /Applications
# so the user can drag-and-drop to install directly from the DMG.
echo
echo "______________________________________________________________"
echo "Re Create zip tarball:"
echo "Preparing DMG staging folder:"
/usr/bin/ditto -c -k --sequesterRsrc --keepParent $BUNDLE "build-aux/mac-osx/${APPNAME}-$VERSION-r$HEAD-arm64.zip"
rm -rf "$STAGING"
mkdir -p "$STAGING"
cp -R "$BUNDLE" "$STAGING/"
ln -s /Applications "$STAGING/Applications"
echo "Staging folder ready: $STAGING"
### Create writable DMG (UDRW) ######################################
# We use a writable DMG first so we can re-sign the .app inside
# after hdiutil copies it (hdiutil can invalidate Sealed Resources
# during the copy, so we must re-sign inside the mounted volume).
# Clean up disk folder
echo 'Cleaning up... '
rm -rf $BUNDLE
echo
echo "______________________________________________________________"
echo "Create writable DMG (UDRW) and re-sign .app inside:"
rm -f "$RW_DMG"
hdiutil create \
-volname "QElectroTech $VERSION" \
-srcfolder "$STAGING" \
-ov \
-format UDRW \
-fs HFS+ \
"$RW_DMG"
#rsync to TF DMG builds
echo -e "\033[1;31mWould you like to upload MacOS packages "${APPNAME}"-"$VERSION"-"r$HEAD-arm64.zip", n/Y?.\033[m"
if [ $? -ne 0 ]; then
echo "ERROR: hdiutil failed to create writable DMG."
rm -rf "$STAGING"
exit 1
fi
# Mount the writable DMG
rm -rf "$MOUNT_POINT"
mkdir -p "$MOUNT_POINT"
hdiutil attach "$RW_DMG" -mountpoint "$MOUNT_POINT" -nobrowse -noverify
if [ $? -ne 0 ]; then
echo "ERROR: failed to mount writable DMG."
rm -f "$RW_DMG"
rm -rf "$STAGING"
exit 1
fi
# Re-sign all binaries inside the mounted DMG
echo "-- Re-signing dylibs inside DMG..."
find "$MOUNT_POINT/$BUNDLE/Contents/Frameworks" -name "*.dylib" | while read lib; do
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
done
find "$MOUNT_POINT/$BUNDLE/Contents/Frameworks" -maxdepth 1 -name "*.framework" | while read fw; do
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$fw"
done
find "$MOUNT_POINT/$BUNDLE/Contents/PlugIns" \( -name "*.dylib" -o -name "*.so" \) | while read lib; do
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
done
echo "-- Re-signing main executable inside DMG..."
codesign --force --sign "$IDENTITY" --timestamp --options=runtime \
"$MOUNT_POINT/$BUNDLE/Contents/MacOS/$APPNAME"
echo "-- Re-signing bundle inside DMG..."
codesign --force --sign "$IDENTITY" --timestamp --options=runtime \
"$MOUNT_POINT/$BUNDLE"
# Verify signature inside the mounted DMG
echo "Verifying bundle signature inside DMG..."
codesign --verify --deep --strict --verbose=2 "$MOUNT_POINT/$BUNDLE"
if [ $? -ne 0 ]; then
echo "ERROR: bundle signature invalid inside DMG, aborting."
hdiutil detach "$MOUNT_POINT"
rm -f "$RW_DMG"
rm -rf "$STAGING" "$MOUNT_POINT"
exit 1
fi
echo "Bundle signature inside DMG OK."
# Detach the writable DMG
hdiutil detach "$MOUNT_POINT"
### Convert UDRW to final compressed UDZO ###########################
echo
echo "______________________________________________________________"
echo "Convert to final compressed DMG (UDZO):"
mkdir -p "build-aux/mac-osx"
rm -f "$DMG_PATH"
hdiutil convert "$RW_DMG" \
-format UDZO \
-o "$DMG_PATH"
if [ $? -ne 0 ]; then
echo "ERROR: hdiutil convert failed."
rm -f "$RW_DMG"
rm -rf "$STAGING" "$MOUNT_POINT"
exit 1
fi
rm -f "$RW_DMG"
rm -rf "$STAGING" "$MOUNT_POINT"
### Sign the final DMG ##############################################
echo "Signing final DMG..."
codesign --sign "$IDENTITY" --timestamp "$DMG_PATH"
### Notarize and staple the final DMG ###############################
echo -e "\033[1;31mWould you like to notarize the DMG \"${DMG_NAME}\", n/Y?\033[m"
read a
if [[ $a == "Y" || $a == "y" ]]; then
cp -Rf "build-aux/mac-osx/${APPNAME}-$VERSION-r$HEAD-arm64.zip" /Users/laurent/MAC_OS_X/
rsync -e ssh -av --delete-after --no-owner --no-g --chmod=g+w --progress --exclude='.DS_Store' /Users/laurent/MAC_OS_X/ server:download.qelectrotech.org/qet/builds/MAC_OS_X/arm64/
echo
echo "______________________________________________________________"
echo "Notarizing DMG:"
xcrun notarytool submit "$DMG_PATH" --keychain-profile "org.qelectrotech" --wait
if [ $? -ne 0 ]; then
echo "ERROR: DMG notarization failed. Check the log with:"
echo " xcrun notarytool log <submission-id> --keychain-profile org.qelectrotech"
exit 1
fi
echo "Stapling DMG..."
xcrun stapler staple "$DMG_PATH"
if [ $? -ne 0 ]; then
echo "ERROR: stapling DMG failed."
exit 1
fi
echo "DMG notarized and stapled OK."
echo "Verifying final DMG..."
spctl -a -vv "$DMG_PATH"
else
echo -e "\033[1;33mExit.\033[m"
fi
### Clean up bundle #################################################
echo "Cleaning up bundle..."
rm -rf "$BUNDLE"
### The end #########################################################
echo
echo "______________________________________________________________"
echo "The process is done."
echo "DMG is in the folder 'build-aux/mac-osx'."
### Upload via rsync ################################################
echo -e "\033[1;31mWould you like to upload MacOS package \"${DMG_NAME}\", n/Y?\033[m"
read a
if [[ $a == "Y" || $a == "y" ]]; then
cp -Rf "$DMG_PATH" /Users/laurent/MAC_OS_X/
rsync -e ssh -av --delete-after --no-owner --no-g --chmod=g+w \
--progress --exclude='.DS_Store' \
/Users/laurent/MAC_OS_X/ \
server:download.qelectrotech.org/qet/builds/MAC_OS_X/arm64/
if [ $? != 0 ]; then
{
echo "RSYNC ERROR: problem syncing ${APPNAME}-$VERSION-r$HEAD-arm64.zip"
rsync -e ssh -av --delete-after --no-owner --no-g --chmod=g+w --progress --exclude='.DS_Store' /Users/laurent/MAC_OS_X/ server:download.qelectrotech.org/qet/builds/MAC_OS_X/arm64/
} fi
echo "RSYNC ERROR: problem syncing ${DMG_NAME}, retrying..."
rsync -e ssh -av --delete-after --no-owner --no-g --chmod=g+w \
--progress --exclude='.DS_Store' \
/Users/laurent/MAC_OS_X/ \
server:download.qelectrotech.org/qet/builds/MAC_OS_X/arm64/
fi
else
echo -e "\033[1;33mExit.\033[m"
fi
+3 -2
View File
@@ -220,7 +220,7 @@ void DynamicTextFieldEditor::fillInfoComboBox()
QStringList strl;
auto type = elementEditor()->elementScene()->elementData().m_type;
if(type & ElementData::AllReport) {
if((type & ElementData::AllReport) || (type == ElementData::ConductorDefinition)) {
strl = QETInformation::folioReportInfoKeys();
}
else {
@@ -365,7 +365,8 @@ void DynamicTextFieldEditor::on_m_text_from_cb_activated(int index) {
void DynamicTextFieldEditor::on_m_composite_text_pb_clicked()
{
bool isReport = false;
if (elementEditor()->elementScene()->elementData().m_type & ElementData::AllReport) {
auto type = elementEditor()->elementScene()->elementData().m_type;
if ((type & ElementData::AllReport) || (type == ElementData::ConductorDefinition)) {
isReport = true;
}
@@ -133,6 +133,7 @@ void ElementPropertiesEditorWidget::setUpInterface()
ui->m_base_type_cb->addItem (tr("Renvoi de folio précédent"), ElementData::PreviousReport);
ui->m_base_type_cb->addItem (tr("Bornier"), ElementData::Terminal);
ui->m_base_type_cb->addItem (tr("Vignette"), ElementData::Thumbnail);
ui->m_base_type_cb->addItem (tr("Définition de conducteur"), ElementData::ConductorDefinition);
// Slave option
ui->m_state_cb->addItem(tr("Normalement ouvert"), ElementData::NO);
@@ -188,6 +189,9 @@ void ElementPropertiesEditorWidget::updateTree()
case ElementData::PreviousReport:
ui->m_tree->setDisabled(true);
break;
case ElementData::ConductorDefinition:
ui->m_tree->setDisabled(true);
break;
case ElementData::Master:
ui->m_tree->setEnabled(true);
break;
+24 -2
View File
@@ -737,9 +737,10 @@ bool QETElementEditor::checkElement()
QList<QETWarning> errors;
// Warning #1: Element haven't got terminal
// (except for report, because report must have one terminal and this checking is do below)
// (except for report and conductor definition, because they must have one terminal and this checking is done below)
if (!m_elmt_scene -> containsTerminals() &&
!(m_elmt_scene->elementData().m_type & ElementData::AllReport)) {
!(m_elmt_scene->elementData().m_type & ElementData::AllReport) &&
m_elmt_scene->elementData().m_type != ElementData::ConductorDefinition) {
warnings << qMakePair(
tr("Absence de borne", "warning title"),
tr(
@@ -771,6 +772,27 @@ bool QETElementEditor::checkElement()
}
}
// Check conductor definition element
if (m_elmt_scene->elementData().m_type == ElementData::ConductorDefinition)
{
int terminal =0;
for(auto qgi : m_elmt_scene -> items()) {
if (qgraphicsitem_cast<PartTerminal *>(qgi)) {
terminal ++;
}
}
// Error: Conductor definition must have exactly one terminal
if (terminal != 1) {
errors << qMakePair (tr("Nombre de bornes incorrect"),
tr("<br><b>Erreur</b> :"
"<br>Les définitions de conducteur ne peuvent posséder qu'une seule borne."
"<br><b>Solution</b> :"
"<br>Vérifier que l'élément ne possède qu'une seule borne"));
}
}
if (!errors.count() && !warnings.count()) {
return(true);
}
+4 -1
View File
@@ -61,7 +61,7 @@ void TerminalEditor::updateForm()
ui->m_y_dsb->setValue(m_part->property("y").toReal());
ui->m_orientation_cb->setCurrentIndex(ui->m_orientation_cb->findData(m_part->property("orientation")));
ui->m_name_le->setText(m_part->terminalName());
ui->m_type_cb->setCurrentIndex(ui->m_orientation_cb->findData(m_part->terminalType()));
ui->m_type_cb->setCurrentIndex(ui->m_type_cb->findData(m_part->terminalType()));
activeConnections(true);
}
@@ -122,6 +122,9 @@ void TerminalEditor::init()
ui->m_type_cb->addItem(tr("Générique"), TerminalData::Generic);
ui->m_type_cb->addItem(tr("Bornier intérieur"), TerminalData::Inner);
ui->m_type_cb->addItem(tr("Bornier extérieur"), TerminalData::Outer);
ui->m_type_cb->addItem(tr("NO (contact SW)"), TerminalData::No);
ui->m_type_cb->addItem(tr("NC (contact SW)"), TerminalData::Nc);
ui->m_type_cb->addItem(tr("Commun (contact SW)"), TerminalData::Common);
}
/**
+1
View File
@@ -335,6 +335,7 @@ void TextEditor::setUpWidget(QWidget *parent)
m_size_sb = new QSpinBox(parent);
m_size_sb->setObjectName(QString::fromUtf8("m_size_sb"));
m_size_sb->setMinimum(4);
gridLayout->addWidget(m_size_sb, 2, 1, 1, 1);
+25 -4
View File
@@ -22,6 +22,7 @@
#include "qeticons.h"
#include "qetproject.h"
#include "titleblock/templatescollection.h"
#include <QApplication>
/*
Lorsque le flag ENABLE_PANEL_DND_CHECKS est defini, le panel d'elements
@@ -42,7 +43,7 @@ ElementsPanel::ElementsPanel(QWidget *parent) :
first_reload_(true)
{
// selection unique
setSelectionMode(QAbstractItemView::SingleSelection);
setSelectionMode(QAbstractItemView::ExtendedSelection);
setColumnCount(1);
setExpandsOnDoubleClick(true);
setMouseTracking(true);
@@ -299,11 +300,14 @@ void ElementsPanel::reload()
}
/**
@brief ElementsPanel::slot_clicked
handle click on qtwi
@param qtwi item that was clickerd on
* @brief ElementsPanel::slot_clicked
* handle click on qtwi
* @param qtwi item that was clickerd on
*/
void ElementsPanel::slot_clicked(QTreeWidgetItem *clickedItem, int) {
if (QApplication::keyboardModifiers() & (Qt::ShiftModifier | Qt::ControlModifier)) {
return;
}
requestForItem(clickedItem);
}
@@ -553,3 +557,20 @@ void ElementsPanel::keyPressEvent(QKeyEvent *event)
QTreeView::keyPressEvent(event);
}
}
/**
* @brief ElementsPanel::selectedDiagrams
* @return A list of all currently selected diagrams in the panel.
*/
QList<Diagram *> ElementsPanel::selectedDiagrams() const
{
QList<Diagram *> diagrams;
foreach (QTreeWidgetItem *item, selectedItems()) {
if (item->type() == QET::Diagram) {
if (Diagram *diagram = valueForItem<Diagram *>(item)) {
diagrams.append(diagram);
}
}
}
return diagrams;
}
+1
View File
@@ -49,6 +49,7 @@ class ElementsPanel : public GenericPanel {
// methods used to get what is represented by a particular visual item
QString dirPathForItem(QTreeWidgetItem *);
QString filePathForItem(QTreeWidgetItem *);
QList<Diagram *> selectedDiagrams() const;
signals:
void requestForProject(QETProject *);
+147 -88
View File
@@ -25,6 +25,7 @@
#include "qetproject.h"
#include "titleblock/templatedeleter.h"
#include <QFileInfo>
#include <QMessageBox>
/*
When the ENABLE_PANEL_WIDGET_DND_CHECKS flag is set, the panel
@@ -242,85 +243,134 @@ void ElementsPanelWidget::newDiagram()
}
/**
Emet le signal requestForDiagramDeletion avec le schema selectionne
* Emet le signal requestForDiagramsDeletion avec les schemas selectionnes
*/
void ElementsPanelWidget::deleteDiagram()
{
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
emit(requestForDiagramDeletion(selected_diagram));
QList<Diagram *> diagrams_to_delete = elements_panel->selectedDiagrams();
if (diagrams_to_delete.isEmpty()) return;
emit(requestForDiagramsDeletion(diagrams_to_delete));
elements_panel->reload();
}
}
/**
Emet le signal requestForDiagramMoveUpTop avec le schema selectionne
+*/
void ElementsPanelWidget::moveDiagramUpTop()
{
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
emit(requestForDiagramMoveUpTop(selected_diagram));
}
}
/**
Emet le signal requestForDiagramMoveUp avec le schema selectionne
* Emits the requestForDiagramMoveUpTop signal with all selected diagrams.
*/
void ElementsPanelWidget::moveDiagramUp()
{
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
emit(requestForDiagramMoveUp(selected_diagram));
void ElementsPanelWidget::moveDiagramUpTop() {
QList<Diagram *> diagrams_to_move = elements_panel->selectedDiagrams();
if (diagrams_to_move.isEmpty()) return;
// Emit the entire list at once
emit requestForDiagramMoveUpTop(diagrams_to_move);
// Clear messy tree selection caused by moving items, then restore clean selection
elements_panel->clearSelection();
for (Diagram *d : diagrams_to_move) {
if (auto item = elements_panel->getItemForDiagram(d)) item->setSelected(true);
}
}
/**
Emet le signal requestForDiagramMoveDown avec le schema selectionne
* Emits the requestForDiagramMoveUp signal with all selected diagrams.
*/
void ElementsPanelWidget::moveDiagramDown()
{
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
emit(requestForDiagramMoveDown(selected_diagram));
void ElementsPanelWidget::moveDiagramUp() {
QList<Diagram *> diagrams_to_move = elements_panel->selectedDiagrams();
if (diagrams_to_move.isEmpty()) return;
// Emit the entire list at once
emit requestForDiagramMoveUp(diagrams_to_move);
// Clear messy tree selection caused by moving items, then restore clean selection
elements_panel->clearSelection();
for (Diagram *d : diagrams_to_move) {
if (auto item = elements_panel->getItemForDiagram(d)) item->setSelected(true);
}
}
/**
Emet le signal requestForDiagramMoveUpx10 avec le schema selectionne
* Emits the requestForDiagramMoveDown signal with all selected diagrams.
*/
void ElementsPanelWidget::moveDiagramUpx10()
{
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
emit(requestForDiagramMoveUpx10(selected_diagram));
void ElementsPanelWidget::moveDiagramDown() {
QList<Diagram *> diagrams_to_move = elements_panel->selectedDiagrams();
if (diagrams_to_move.isEmpty()) return;
// Emit the entire list at once
emit requestForDiagramMoveDown(diagrams_to_move);
// Clear messy tree selection caused by moving items, then restore clean selection
elements_panel->clearSelection();
for (Diagram *d : diagrams_to_move) {
if (auto item = elements_panel->getItemForDiagram(d)) item->setSelected(true);
}
}
/**
Emet le signal requestForDiagramMoveUpx100 avec le schema selectionne
* Emits the requestForDiagramMoveUpx10 signal with all selected diagrams.
*/
void ElementsPanelWidget::moveDiagramUpx100()
{
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
emit(requestForDiagramMoveUpx100(selected_diagram));
void ElementsPanelWidget::moveDiagramUpx10() {
QList<Diagram *> diagrams_to_move = elements_panel->selectedDiagrams();
if (diagrams_to_move.isEmpty()) return;
// Emit the entire list at once
emit requestForDiagramMoveUpx10(diagrams_to_move);
// Clear messy tree selection caused by moving items, then restore clean selection
elements_panel->clearSelection();
for (Diagram *d : diagrams_to_move) {
if (auto item = elements_panel->getItemForDiagram(d)) item->setSelected(true);
}
}
/**
Emet le signal requestForDiagramMoveDownx10 avec le schema selectionne
* Emits the requestForDiagramMoveUpx100 signal with all selected diagrams.
*/
void ElementsPanelWidget::moveDiagramDownx10()
{
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
emit(requestForDiagramMoveDownx10(selected_diagram));
void ElementsPanelWidget::moveDiagramUpx100() {
QList<Diagram *> diagrams_to_move = elements_panel->selectedDiagrams();
if (diagrams_to_move.isEmpty()) return;
// Emit the entire list at once
emit requestForDiagramMoveUpx100(diagrams_to_move);
// Clear messy tree selection caused by moving items, then restore clean selection
elements_panel->clearSelection();
for (Diagram *d : diagrams_to_move) {
if (auto item = elements_panel->getItemForDiagram(d)) item->setSelected(true);
}
}
/**
Emet le signal requestForDiagramMoveDownx100 avec le schema selectionne
* Emits the requestForDiagramMoveDownx10 signal with all selected diagrams.
*/
void ElementsPanelWidget::moveDiagramDownx100()
{
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
emit(requestForDiagramMoveDownx100(selected_diagram));
void ElementsPanelWidget::moveDiagramDownx10() {
QList<Diagram *> diagrams_to_move = elements_panel->selectedDiagrams();
if (diagrams_to_move.isEmpty()) return;
// Emit the entire list at once
emit requestForDiagramMoveDownx10(diagrams_to_move);
// Clear messy tree selection caused by moving items, then restore clean selection
elements_panel->clearSelection();
for (Diagram *d : diagrams_to_move) {
if (auto item = elements_panel->getItemForDiagram(d)) item->setSelected(true);
}
}
/**
* Emits the requestForDiagramMoveDownx100 signal with all selected diagrams.
*/
void ElementsPanelWidget::moveDiagramDownx100() {
QList<Diagram *> diagrams_to_move = elements_panel->selectedDiagrams();
if (diagrams_to_move.isEmpty()) return;
// Emit the entire list at once
emit requestForDiagramMoveDownx100(diagrams_to_move);
// Clear messy tree selection caused by moving items, then restore clean selection
elements_panel->clearSelection();
for (Diagram *d : diagrams_to_move) {
if (auto item = elements_panel->getItemForDiagram(d)) item->setSelected(true);
}
}
@@ -378,21 +428,35 @@ void ElementsPanelWidget::updateButtons()
bool is_writable = !(elements_panel -> selectedProject() -> isReadOnly());
prj_add_diagram -> setEnabled(is_writable);
} else if (current_type == QET::Diagram) {
Diagram *selected_diagram = elements_panel -> selectedDiagram();
QETProject *selected_diagram_project = selected_diagram -> project();
// Fetch ALL selected diagrams instead of just one
QList<Diagram *> selected_diagrams = elements_panel -> selectedDiagrams();
if (!selected_diagrams.isEmpty()) {
QETProject *selected_diagram_project = selected_diagrams.first() -> project();
bool is_writable = !(selected_diagram_project -> isReadOnly());
int project_diagrams_count = selected_diagram_project -> diagrams().count();
int diagram_position = selected_diagram_project -> diagrams().indexOf(selected_diagram);
// Find the highest (min) and lowest (max) index among the selection
int min_position = project_diagrams_count;
int max_position = -1;
for (Diagram *diagram : selected_diagrams) {
int pos = selected_diagram_project -> diagrams().indexOf(diagram);
if (pos < min_position) min_position = pos;
if (pos > max_position) max_position = pos;
}
prj_del_diagram -> setEnabled(is_writable);
prj_move_diagram_up -> setEnabled(is_writable && diagram_position > 0);
prj_move_diagram_down -> setEnabled(is_writable && diagram_position < project_diagrams_count - 1);
prj_move_diagram_top -> setEnabled(is_writable && diagram_position > 0);
prj_move_diagram_upx10 -> setEnabled(is_writable && diagram_position > 10);
prj_move_diagram_upx100 -> setEnabled(is_writable && diagram_position > 100);
prj_move_diagram_downx10 -> setEnabled(is_writable && diagram_position < project_diagrams_count - 10);
prj_move_diagram_downx100 -> setEnabled(is_writable && diagram_position < project_diagrams_count - 100);
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_top -> setEnabled(is_writable && min_position > 0);
// Adjusted to >= to allow exactly 10 or 100 steps if space permits
prj_move_diagram_upx10 -> setEnabled(is_writable && min_position > 10);
prj_move_diagram_upx100 -> setEnabled(is_writable && min_position > 100);
prj_move_diagram_downx10 -> setEnabled(is_writable && max_position < project_diagrams_count - 10);
prj_move_diagram_downx100 -> setEnabled(is_writable && max_position < project_diagrams_count - 100);
}
} else if (current_type == QET::TitleBlockTemplatesCollection) {
TitleBlockTemplateLocation location = elements_panel -> templateLocationForItem(current_item);
tbt_add -> setEnabled(!location.isReadOnly());
@@ -475,62 +539,57 @@ void ElementsPanelWidget::filterEdited(const QString &next_text) {
}
/**
Treat key press event inside elements panel widget
* Treat key press event inside elements panel widget
*/
/**
* Treat key press event inside elements panel widget.
* Respects the enabled/disabled state of the corresponding QActions.
*/
void ElementsPanelWidget::keyPressEvent(QKeyEvent *e) {
switch(e->key()) {
case Qt::Key_Delete: //delete diagram through elements panel widget
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
emit(requestForDiagramDeletion(selected_diagram));
case Qt::Key_Delete:
if (prj_del_diagram && prj_del_diagram->isEnabled()) {
deleteDiagram();
}
break;
case Qt::Key_F3:
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
elements_panel->setSelectedItem(elements_panel->getItemForDiagram(selected_diagram));
emit(requestForDiagramMoveUp(selected_diagram));
if (prj_move_diagram_up && prj_move_diagram_up->isEnabled()) {
moveDiagramUp();
}
break;
case Qt::Key_F4:
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
elements_panel->setSelectedItem(elements_panel->getItemForDiagram(selected_diagram));
emit(requestForDiagramMoveDown(selected_diagram));
if (prj_move_diagram_down && prj_move_diagram_down->isEnabled()) {
moveDiagramDown();
}
break;
case Qt::Key_F5:
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
elements_panel->setSelectedItem(elements_panel->getItemForDiagram(selected_diagram));
emit(requestForDiagramMoveUpTop(selected_diagram));
if (prj_move_diagram_top && prj_move_diagram_top->isEnabled()) {
moveDiagramUpTop();
}
break;
case Qt::Key_F6:
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
elements_panel->setSelectedItem(elements_panel->getItemForDiagram(selected_diagram));
emit(requestForDiagramMoveDownx10(selected_diagram));
if (prj_move_diagram_downx10 && prj_move_diagram_downx10->isEnabled()) {
moveDiagramDownx10();
}
break;
case Qt::Key_F7:
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
elements_panel->setSelectedItem(elements_panel->getItemForDiagram(selected_diagram));
emit(requestForDiagramMoveDownx100(selected_diagram));
if (prj_move_diagram_downx100 && prj_move_diagram_downx100->isEnabled()) {
moveDiagramDownx100();
}
break;
case Qt::Key_F8:
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
elements_panel->setSelectedItem(elements_panel->getItemForDiagram(selected_diagram));
emit(requestForDiagramMoveUpx10(selected_diagram));
if (prj_move_diagram_upx10 && prj_move_diagram_upx10->isEnabled()) {
moveDiagramUpx10();
}
break;
case Qt::Key_F9:
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
elements_panel->setSelectedItem(elements_panel->getItemForDiagram(selected_diagram));
emit(requestForDiagramMoveUpx100(selected_diagram));
if (prj_move_diagram_upx100 && prj_move_diagram_upx100->isEnabled()) {
moveDiagramUpx100();
}
break;
default:
// Pass unhandled key events to the base class
QWidget::keyPressEvent(e);
break;
}
return;
}
+8 -7
View File
@@ -69,13 +69,14 @@ class ElementsPanelWidget : public QWidget {
void requestForProjectPropertiesEdition(QETProject *);
void requestForDiagramPropertiesEdition(Diagram *);
void requestForDiagramDeletion(Diagram *);
void requestForDiagramMoveUp(Diagram *);
void requestForDiagramMoveDown(Diagram *);
void requestForDiagramMoveUpTop(Diagram *);
void requestForDiagramMoveUpx10(Diagram *);
void requestForDiagramMoveUpx100(Diagram *);
void requestForDiagramMoveDownx10(Diagram *);
void requestForDiagramMoveDownx100(Diagram *);
void requestForDiagramsDeletion(const QList<Diagram *> &diagrams);
void requestForDiagramMoveUp(const QList<Diagram *> &diagrams);
void requestForDiagramMoveDown(const QList<Diagram *> &diagrams);
void requestForDiagramMoveUpTop(const QList<Diagram *> &diagrams);
void requestForDiagramMoveUpx10(const QList<Diagram *> &diagrams);
void requestForDiagramMoveUpx100(const QList<Diagram *> &diagrams);
void requestForDiagramMoveDownx10(const QList<Diagram *> &diagrams);
void requestForDiagramMoveDownx100(const QList<Diagram *> &diagrams);
public slots:
void openDirectoryForSelectedItem();
+62 -9
View File
@@ -364,11 +364,12 @@ QETResult ProjectView::noProjectResult() const
}
/**
@brief ProjectView::removeDiagram
Remove a diagram (folio) of the project
@param diagram_view : diagram view to remove
* @brief ProjectView::removeDiagram
* Remove a diagram (folio) of the project
* @param diagram_view : diagram view to remove
* @param silent : if true, bypasses the confirmation message box
*/
void ProjectView::removeDiagram(DiagramView *diagram_view)
void ProjectView::removeDiagram(DiagramView *diagram_view, bool silent)
{
if (!diagram_view)
return;
@@ -377,7 +378,7 @@ void ProjectView::removeDiagram(DiagramView *diagram_view)
if (!m_diagram_ids.values().contains(diagram_view))
return;
if (!silent) {
//Ask confirmation to user.
int answer = QET::QetMessageBox::question(
this,
@@ -389,6 +390,7 @@ void ProjectView::removeDiagram(DiagramView *diagram_view)
if (answer != QMessageBox::Yes) {
return;
}
}
//Remove the diagram view of the tabs widget
int index_to_remove = m_diagram_ids.key(diagram_view);
@@ -405,14 +407,15 @@ void ProjectView::removeDiagram(DiagramView *diagram_view)
}
/**
Enleve un schema du ProjectView
@param diagram Schema a enlever
* Enleve un schema du ProjectView
* @param diagram Schema a enlever
* @param silent Si vrai, supprime sans demander confirmation
*/
void ProjectView::removeDiagram(Diagram *diagram) {
void ProjectView::removeDiagram(Diagram *diagram, bool silent) {
if (!diagram) return;
if (DiagramView *diagram_view = findDiagram(diagram)) {
removeDiagram(diagram_view);
removeDiagram(diagram_view, silent);
}
}
@@ -557,6 +560,56 @@ void ProjectView::moveDiagramUpx10(Diagram *diagram) {
moveDiagramUpx10(findDiagram(diagram));
}
/**
* @brief ProjectView::moveDiagramUpx100
* Moves the diagram_view up / left x100
* @param diagram_view View to move
*/
void ProjectView::moveDiagramUpx100(DiagramView *diagram_view) {
if (!diagram_view) return;
int diagram_view_position = m_diagram_ids.key(diagram_view);
if (!diagram_view_position) {
// The diagram is the first of the project
return;
}
m_tab->tabBar()->moveTab(diagram_view_position, diagram_view_position - 100);
}
/**
* @brief ProjectView::moveDiagramUpx100
* Moves the diagram up / left x100
* @param diagram Diagram to move
*/
void ProjectView::moveDiagramUpx100(Diagram *diagram) {
moveDiagramUpx100(findDiagram(diagram));
}
/**
* @brief ProjectView::moveDiagramDownx100
* Moves the diagram_view down / right x100
* @param diagram_view View to move
*/
void ProjectView::moveDiagramDownx100(DiagramView *diagram_view) {
if (!diagram_view) return;
int diagram_view_position = m_diagram_ids.key(diagram_view);
if (diagram_view_position + 1 == m_diagram_ids.count()) {
// The diagram is the last of the project
return;
}
m_tab->tabBar()->moveTab(diagram_view_position, diagram_view_position + 100);
}
/**
* @brief ProjectView::moveDiagramDownx100
* Moves the diagram down / right x100
* @param diagram Diagram to move
*/
void ProjectView::moveDiagramDownx100(Diagram *diagram) {
moveDiagramDownx100(findDiagram(diagram));
}
/**
Deplace le schema diagram_view vers le bas / la droite x10
*/
+6 -2
View File
@@ -104,8 +104,8 @@ class ProjectView : public QWidget
void changeLastTab();
public slots:
void removeDiagram(DiagramView *);
void removeDiagram(Diagram *);
void removeDiagram(DiagramView *diagram_view, bool silent = false);
void removeDiagram(Diagram *diagram, bool silent = false);
void showDiagram(DiagramView *);
void showDiagram(Diagram *);
void editProjectProperties();
@@ -122,6 +122,10 @@ class ProjectView : public QWidget
void moveDiagramUpx10(Diagram *);
void moveDiagramDownx10(DiagramView *);
void moveDiagramDownx10(Diagram *);
void moveDiagramUpx100(DiagramView *);
void moveDiagramUpx100(Diagram *);
void moveDiagramDownx100(DiagramView *);
void moveDiagramDownx100(Diagram *);
void exportProject();
QETResult save();
QETResult saveAs();
+17 -1
View File
@@ -49,7 +49,19 @@ bool ElementData::fromXml(const QDomElement &xml_element)
return false;
}
m_type = typeFromString(xml_element.attribute(QStringLiteral("link_type"), QStringLiteral("simple")));
// --- HIER STARTET UNSER DEBUG-BLOCK ---
// Wir holen den String aus der XML und speichern ihn kurz zwischen
QString raw_type_string = xml_element.attribute(QStringLiteral("link_type"), QStringLiteral("simple"));
qDebug() << "\n=== NEUES BAUTEIL WIRD GELADEN ===";
qDebug() << "[XML Parser] Roher 'link_type' String aus der .elmt Datei:" << raw_type_string;
// Jetzt übergeben wir ihn an deine Übersetzungs-Funktion
m_type = typeFromString(raw_type_string);
qDebug() << "[XML Parser] Übersetzter ElementData-Typ:" << typeToString(m_type);
// --- HIER ENDET UNSER DEBUG-BLOCK ---
kindInfoFromXml(xml_element);
m_informations.fromXml(xml_element.firstChildElement(QStringLiteral("elementInformations")),
QStringLiteral("elementInformation"));
@@ -323,6 +335,8 @@ QString ElementData::typeToString(ElementData::Type type)
return QStringLiteral("terminal");
case ElementData::Thumbnail:
return QStringLiteral("thumbnail");
case ElementData::ConductorDefinition:
return QStringLiteral("conductor_definition");
default:
qDebug() << "ElementData::typeToString : type don't exist"
<< "return failsafe value 'simple'";
@@ -346,6 +360,8 @@ ElementData::Type ElementData::typeFromString(const QString &string)
return ElementData::Terminal;
} else if (string == QLatin1String("thumbnail")) {
return ElementData::Thumbnail;
} else if (string == QLatin1String("conductor_definition")) {
return ElementData::ConductorDefinition;
}
//Return simple if nothing match
+2 -1
View File
@@ -41,7 +41,8 @@ class ElementData : public PropertiesInterface
Master = 8,
Slave = 16,
Terminal = 32,
Thumbnail = 64};
Thumbnail = 64,
ConductorDefinition = 128};
Q_ENUM(Type)
Q_DECLARE_FLAGS(Types, Type)
+12
View File
@@ -174,6 +174,12 @@ QString TerminalData::typeToString(TerminalData::Type type)
return QString("Inner");
case Outer :
return QString("Outer");
case No :
return QString("No");
case Nc :
return QString("Nc");
case Common :
return QString("Common");
}
return QString("Generic");
}
@@ -193,6 +199,12 @@ TerminalData::Type TerminalData::typeFromString(const QString &string)
return TerminalData::Inner;
} else if (string == "Outer") {
return TerminalData::Outer;
} else if (string == "No") {
return TerminalData::No;
} else if (string == "Nc") {
return TerminalData::Nc;
} else if (string == "Common") {
return TerminalData::Common;
} else {
qDebug() << "TerminalData::typeFromString, argument string is invalid"
" failsafe type 'TerminalData::Generic' is returned";
+4 -1
View File
@@ -41,7 +41,10 @@ class TerminalData : public PropertiesInterface
enum Type {
Generic,
Inner,
Outer
Outer,
No, ///< Normally Open terminal (for SW contacts)
Nc, ///< Normally Closed terminal (for SW contacts)
Common ///< Common terminal (for SW contacts)
};
Q_ENUM(Type)
+6
View File
@@ -29,6 +29,7 @@
XRefProperties::XRefProperties()
{
m_show_power_ctc = true;
m_show_terminal_name = true;
m_display = Cross;
m_snap_to = Bottom;
m_prefix_keys << "power" << "delay" << "switch";
@@ -48,6 +49,7 @@ void XRefProperties::toSettings(QSettings &settings,
const QString prefix) const
{
settings.setValue(prefix % "showpowerctc", m_show_power_ctc);
settings.setValue(prefix % "showterminalname", m_show_terminal_name);
QString display = m_display == Cross? "cross" : "contacts";
settings.setValue(prefix % "displayhas", display);
QString snap = m_snap_to == Bottom? "bottom" : "label";
@@ -78,6 +80,7 @@ void XRefProperties::fromSettings(const QSettings &settings,
const QString prefix)
{
m_show_power_ctc = settings.value(prefix % "showpowerctc", true).toBool();
m_show_terminal_name = settings.value(prefix % "showterminalname", true).toBool();
QString display = settings.value(prefix % "displayhas", "cross").toString();
display == "cross"? m_display = Cross : m_display = Contacts;
QString snap = settings.value(prefix % "snapto", "label").toString();
@@ -107,6 +110,7 @@ QDomElement XRefProperties::toXml(QDomDocument &xml_document) const
xml_element.setAttribute("type", m_key);
xml_element.setAttribute("showpowerctc", m_show_power_ctc? "true" : "false");
xml_element.setAttribute("showterminalname", m_show_terminal_name? "true" : "false");
QString display = m_display == Cross? "cross" : "contacts";
xml_element.setAttribute("displayhas", display);
QString snap = m_snap_to == Bottom? "bottom" : "label";
@@ -137,6 +141,7 @@ QDomElement XRefProperties::toXml(QDomDocument &xml_document) const
*/
bool XRefProperties::fromXml(const QDomElement &xml_element) {
m_show_power_ctc = xml_element.attribute("showpowerctc") == "true";
m_show_terminal_name = xml_element.attribute("showterminalname", "true") == "true";
QString display = xml_element.attribute("displayhas", "cross");
display == "cross"? m_display = Cross : m_display = Contacts;
QString snap = xml_element.attribute("snapto", "label");
@@ -188,6 +193,7 @@ QHash<QString, XRefProperties> XRefProperties::defaultProperties()
bool XRefProperties::operator ==(const XRefProperties &xrp) const{
return (m_show_power_ctc == xrp.m_show_power_ctc
&& m_show_terminal_name == xrp.m_show_terminal_name
&& m_display == xrp.m_display
&& m_snap_to == xrp.m_snap_to
&& m_prefix == xrp.m_prefix
+4
View File
@@ -57,6 +57,9 @@ class XRefProperties : public PropertiesInterface
void setShowPowerContac (const bool a) {m_show_power_ctc = a;}
bool showPowerContact () const {return m_show_power_ctc;}
void setShowTerminalName (const bool a) {m_show_terminal_name = a;}
bool showTerminalName () const {return m_show_terminal_name;}
void setDisplayHas (const DisplayHas dh) {m_display = dh;}
DisplayHas displayHas () const {return m_display;}
@@ -81,6 +84,7 @@ class XRefProperties : public PropertiesInterface
private:
bool m_show_power_ctc;
bool m_show_terminal_name;
DisplayHas m_display;
SnapTo m_snap_to;
Qt::AlignmentFlag m_xref_pos;
+150 -92
View File
@@ -16,7 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "qetdiagrameditor.h"
#include <QCoreApplication>
#include "ElementsCollection/elementscollectionwidget.h"
#include "QWidgetAnimation/qwidgetanimation.h"
#include "autoNum/ui/autonumberingdockwidget.h"
@@ -47,7 +47,7 @@
#include "TerminalStrip/ui/addterminalstripitemdialog.h"
#include "wiringlistexport.h"
#include "ui/terminalnumberingdialog.h"
#include <QDebug>
#ifdef BUILD_WITHOUT_KF5
#else
# include <KAutoSaveFile>
@@ -176,12 +176,14 @@ void QETDiagramEditor::setUpElementsPanel()
connect(pa, SIGNAL(requestForProjectPropertiesEdition (QETProject *)), this, SLOT(editProjectProperties(QETProject *)));
connect(pa, SIGNAL(requestForNewDiagram (QETProject *)), this, SLOT(addDiagramToProject(QETProject *)));
connect(pa, SIGNAL(requestForDiagramPropertiesEdition (Diagram *)), this, SLOT(editDiagramProperties(Diagram *)));
connect(pa, SIGNAL(requestForDiagramDeletion (Diagram *)), this, SLOT(removeDiagram(Diagram *)));
connect(pa, SIGNAL(requestForDiagramMoveUp (Diagram *)), this, SLOT(moveDiagramUp(Diagram *)));
connect(pa, SIGNAL(requestForDiagramMoveDown (Diagram *)), this, SLOT(moveDiagramDown(Diagram *)));
connect(pa, SIGNAL(requestForDiagramMoveUpTop (Diagram *)), this, SLOT(moveDiagramUpTop(Diagram *)));
connect(pa, SIGNAL(requestForDiagramMoveUpx10 (Diagram *)), this, SLOT(moveDiagramUpx10(Diagram *)));
connect(pa, SIGNAL(requestForDiagramMoveDownx10 (Diagram *)), this, SLOT(moveDiagramDownx10(Diagram *)));
connect(pa, SIGNAL(requestForDiagramsDeletion (const QList<Diagram *> &)), this, SLOT(removeDiagrams(const QList<Diagram *> &)));
connect(pa, SIGNAL(requestForDiagramMoveUp (const QList<Diagram *> &)), this, SLOT(moveDiagramUp(const QList<Diagram *>&)));
connect(pa, SIGNAL(requestForDiagramMoveDown (const QList<Diagram *> &)), this, SLOT(moveDiagramDown(const QList<Diagram *>&)));
connect(pa, SIGNAL(requestForDiagramMoveUpTop (const QList<Diagram *> &)), this, SLOT(moveDiagramUpTop(const QList<Diagram *>&)));
connect(pa, SIGNAL(requestForDiagramMoveUpx10 (const QList<Diagram *> &)), this, SLOT(moveDiagramUpx10(const QList<Diagram *>&)));
connect(pa, SIGNAL(requestForDiagramMoveDownx10 (const QList<Diagram *> &)), this, SLOT(moveDiagramDownx10(const QList<Diagram *>&)));
connect(pa, SIGNAL(requestForDiagramMoveUpx100 (const QList<Diagram *> &)), this, SLOT(moveDiagramUpx100(const QList<Diagram *>&)));
connect(pa, SIGNAL(requestForDiagramMoveDownx100 (const QList<Diagram *> &)), this, SLOT(moveDiagramDownx100(const QList<Diagram *>&)));
}
/**
@@ -2183,126 +2185,182 @@ void QETDiagramEditor::addDiagramToProject(QETProject *project)
project_view->project()->addNewDiagram();
}
}
/**
* @brief QETDiagramEditor::removeDiagram
* Wrapper für einzelne Diagramme, um Abwärtskompatibilität zu erhalten.
*/
void QETDiagramEditor::removeDiagram(Diagram *diagram)
{
if (!diagram) return;
QList<Diagram *> list;
list << diagram;
removeDiagrams(list);
}
/**
* @brief QETDiagramEditor::removeDiagrams
* Deletes a list of folios with a single query.
*/
void QETDiagramEditor::removeDiagrams(const QList<Diagram *> &diagrams)
{
if (diagrams.isEmpty()) return;
if (diagrams.count() == 1) {
QMessageBox::StandardButton reply;
reply = QMessageBox::question(this, tr("Supprimer le folio"),
tr("Êtes-vous sûr de vouloir supprimer ce folio ?"),
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::No) return;
} else {
QMessageBox::StandardButton reply;
reply = QMessageBox::question(this, tr("Supprimer les folios"),
tr("Êtes-vous sûr de vouloir supprimer les %1 folios sélectionnés ?").arg(diagrams.count()),
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::No) return;
}
ProjectView *project_view = nullptr;
if (QETProject *diagram_project = diagrams.first()->project()) {
project_view = findProject(diagram_project);
}
if (project_view) project_view->setUpdatesEnabled(false);
if (pa) pa->setUpdatesEnabled(false);
foreach (Diagram *diagram, diagrams) {
removeDiagramSilent(diagram);
}
if (pa) pa->setUpdatesEnabled(true);
if (project_view) project_view->setUpdatesEnabled(true);
emit syncElementsPanel();
}
/**
Supprime un schema de son projet
@param diagram Schema a supprimer
*/
void QETDiagramEditor::removeDiagram(Diagram *diagram)
void QETDiagramEditor::removeDiagramSilent(Diagram *diagram)
{
if (!diagram) return;
// recupere le projet contenant le schema
if (QETProject *diagram_project = diagram -> project()) {
// recupere la vue sur ce projet
if (ProjectView *project_view = findProject(diagram_project)) {
// affiche le schema en question
project_view -> showDiagram(diagram);
// supprime le schema
project_view -> removeDiagram(diagram);
project_view -> removeDiagram(diagram, true);
}
}
}
/**
Change l'ordre des schemas d'un projet, en decalant le schema vers le haut /
la gauche
@param diagram Schema a decaler vers le haut / la gauche
*/
void QETDiagramEditor::moveDiagramUp(Diagram *diagram)
{
if (!diagram) return;
// recupere le projet contenant le schema
if (QETProject *diagram_project = diagram -> project()) {
if (diagram_project -> isReadOnly()) return;
// recupere la vue sur ce projet
void QETDiagramEditor::moveDiagramUp(const QList<Diagram *> &diagrams) {
if (diagrams.isEmpty()) return;
QList<Diagram *> safeDiagrams = diagrams;
if (QETProject *diagram_project = safeDiagrams.first()->project()) {
if (!diagram_project->isReadOnly()) {
if (ProjectView *project_view = findProject(diagram_project)) {
project_view -> moveDiagramUp(diagram);
// Forward loop for moving up
for (int i = 0; i < safeDiagrams.size(); ++i) {
project_view->moveDiagramUp(safeDiagrams.at(i));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
}
}
}
}
}
/**
Change l'ordre des schemas d'un projet, en decalant le schema vers le bas /
la droite
@param diagram Schema a decaler vers le bas / la droite
*/
void QETDiagramEditor::moveDiagramDown(Diagram *diagram)
{
if (!diagram) return;
// recupere le projet contenant le schema
if (QETProject *diagram_project = diagram -> project()) {
if (diagram_project -> isReadOnly()) return;
// recupere la vue sur ce projet
void QETDiagramEditor::moveDiagramDown(const QList<Diagram *> &diagrams) {
if (diagrams.isEmpty()) return;
QList<Diagram *> safeDiagrams = diagrams;
if (QETProject *diagram_project = safeDiagrams.first()->project()) {
if (!diagram_project->isReadOnly()) {
if (ProjectView *project_view = findProject(diagram_project)) {
project_view -> moveDiagramDown(diagram);
// Backward loop for moving down
for (int i = safeDiagrams.size() - 1; i >= 0; --i) {
project_view->moveDiagramDown(safeDiagrams.at(i));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
}
}
}
}
}
/**
Change l'ordre des schemas d'un projet, en decalant le schema vers le haut /
la gauche en position 0
@param diagram Schema a decaler vers le haut / la gauche en position 0
*/
void QETDiagramEditor::moveDiagramUpTop(Diagram *diagram)
{
if (!diagram) return;
// recupere le projet contenant le schema
if (QETProject *diagram_project = diagram -> project()) {
if (diagram_project -> isReadOnly()) return;
// recupere la vue sur ce projet
void QETDiagramEditor::moveDiagramUpTop(const QList<Diagram *> &diagrams) {
if (diagrams.isEmpty()) return;
QList<Diagram *> safeDiagrams = diagrams;
if (QETProject *diagram_project = safeDiagrams.first()->project()) {
if (!diagram_project->isReadOnly()) {
if (ProjectView *project_view = findProject(diagram_project)) {
project_view -> moveDiagramUpTop(diagram);
// Backward loop to preserve relative order of the selected items when moving to top
for (int i = safeDiagrams.size() - 1; i >= 0; --i) {
project_view->moveDiagramUpTop(safeDiagrams.at(i));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
}
}
}
}
}
/**
Change l'ordre des schemas d'un projet, en decalant le schema vers le haut /
la gauche x10
@param diagram Schema a decaler vers le haut / la gauche x10
*/
void QETDiagramEditor::moveDiagramUpx10(Diagram *diagram)
{
if (!diagram) return;
// recupere le projet contenant le schema
if (QETProject *diagram_project = diagram -> project()) {
if (diagram_project -> isReadOnly()) return;
// recupere la vue sur ce projet
void QETDiagramEditor::moveDiagramUpx10(const QList<Diagram *> &diagrams) {
if (diagrams.isEmpty()) return;
QList<Diagram *> safeDiagrams = diagrams;
if (QETProject *diagram_project = safeDiagrams.first()->project()) {
if (!diagram_project->isReadOnly()) {
if (ProjectView *project_view = findProject(diagram_project)) {
project_view -> moveDiagramUpx10(diagram);
// Forward loop for moving up
for (int i = 0; i < safeDiagrams.size(); ++i) {
project_view->moveDiagramUpx10(safeDiagrams.at(i));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
}
}
}
}
}
/**
Change l'ordre des schemas d'un projet, en decalant le schema vers le bas /
la droite x10
@param diagram Schema a decaler vers le bas / la droite x10
*/
void QETDiagramEditor::moveDiagramDownx10(Diagram *diagram)
{
if (!diagram) return;
// recupere le projet contenant le schema
if (QETProject *diagram_project = diagram -> project()) {
if (diagram_project -> isReadOnly()) return;
// recupere la vue sur ce projet
void QETDiagramEditor::moveDiagramDownx10(const QList<Diagram *> &diagrams) {
if (diagrams.isEmpty()) return;
QList<Diagram *> safeDiagrams = diagrams;
if (QETProject *diagram_project = safeDiagrams.first()->project()) {
if (!diagram_project->isReadOnly()) {
if (ProjectView *project_view = findProject(diagram_project)) {
project_view -> moveDiagramDownx10(diagram);
// Backward loop for moving down
for (int i = safeDiagrams.size() - 1; i >= 0; --i) {
project_view->moveDiagramDownx10(safeDiagrams.at(i));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
}
}
}
}
}
void QETDiagramEditor::moveDiagramUpx100(const QList<Diagram *> &diagrams) {
if (diagrams.isEmpty()) return;
QList<Diagram *> safeDiagrams = diagrams;
if (QETProject *diagram_project = safeDiagrams.first()->project()) {
if (!diagram_project->isReadOnly()) {
if (ProjectView *project_view = findProject(diagram_project)) {
// Forward loop for moving up
for (int i = 0; i < safeDiagrams.size(); ++i) {
project_view->moveDiagramUpx100(safeDiagrams.at(i));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
}
}
}
}
}
void QETDiagramEditor::moveDiagramDownx100(const QList<Diagram *> &diagrams) {
if (diagrams.isEmpty()) return;
QList<Diagram *> safeDiagrams = diagrams;
if (QETProject *diagram_project = safeDiagrams.first()->project()) {
if (!diagram_project->isReadOnly()) {
if (ProjectView *project_view = findProject(diagram_project)) {
// Backward loop for moving down
for (int i = safeDiagrams.size() - 1; i >= 0; --i) {
project_view->moveDiagramDownx100(safeDiagrams.at(i));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
}
}
}
}
}
+10 -5
View File
@@ -138,12 +138,15 @@ class QETDiagramEditor : public QETMainWindow
void editDiagramProperties(Diagram *);
void addDiagramToProject(QETProject *);
void removeDiagram(Diagram *);
void removeDiagrams(const QList<Diagram *> &diagrams);
void removeDiagramFromProject();
void moveDiagramUp(Diagram *);
void moveDiagramDown(Diagram *);
void moveDiagramUpTop(Diagram *);
void moveDiagramUpx10(Diagram *);
void moveDiagramDownx10(Diagram *);
void moveDiagramUp(const QList<Diagram *> &diagrams);
void moveDiagramDown(const QList<Diagram *> &diagrams);
void moveDiagramUpTop(const QList<Diagram *> &diagrams);
void moveDiagramUpx10(const QList<Diagram *> &diagrams);
void moveDiagramDownx10(const QList<Diagram *> &diagrams);
void moveDiagramUpx100(const QList<Diagram *> &diagrams);
void moveDiagramDownx100(const QList<Diagram *> &diagrams);
void reloadOldElementPanel();
void diagramWasAdded(DiagramView *);
void findElementInPanel(const ElementsLocation &);
@@ -222,6 +225,8 @@ class QETDiagramEditor : public QETMainWindow
QList <QAction *> m_zoom_action_toolBar; ///Only zoom action must displayed in the toolbar
void removeDiagramSilent(Diagram *diagram);
QMdiArea m_workspace;
QSignalMapper windowMapper;
QDir open_dialog_dir; /// Directory to use for file dialogs such as File > save
+398 -92
View File
@@ -17,6 +17,8 @@
*/
#include "crossrefitem.h"
#include <QTimer>
#include "../autoNum/assignvariables.h"
#include "../diagram.h"
#include "../diagramposition.h"
@@ -25,6 +27,7 @@
#include "element.h"
#include "elementtextitemgroup.h"
#include "qgraphicsitemutility.h"
#include "terminal.h"
//define the height of the header.
static int header = 5;
@@ -221,9 +224,12 @@ void CrossRefItem::updateLabel()
prepareGeometryChange();
m_bounding_rect = QRectF();
//init the painter
QPainter qp;
qp.begin(&m_drawing);
// Build geometry and m_hovered_contacts_map using a QImage-backed
// painter so font metrics match the screen painter in paint().
// m_update_map=true allows the draw functions to populate the map;
// paint() calls them with m_update_map=false so the map is stable.
QImage dummy(1, 1, QImage::Format_ARGB32_Premultiplied);
QPainter qp(&dummy);
QPen pen_;
pen_.setWidthF(0.5);
qp.setPen(pen_);
@@ -232,17 +238,21 @@ void CrossRefItem::updateLabel()
//Draw cross or contact, only if master element is linked.
if (! m_element->linkedElements().isEmpty())
{
m_update_map = true;
XRefProperties::DisplayHas dh = m_properties.displayHas();
if (dh == XRefProperties::Cross)
drawAsCross(qp);
else if (dh == XRefProperties::Contacts)
drawAsContacts(qp);
m_update_map = false;
}
qp.end();
autoPos();
update();
// Schedule a second update after the scene has finished laying out,
// so the initial render uses the correct bounding rect.
QTimer::singleShot(0, this, [this]{ update(); });
}
/**
@@ -310,7 +320,26 @@ void CrossRefItem::paint(
{
Q_UNUSED(option)
Q_UNUSED(widget)
m_drawing.play(painter);
// Draw directly — no QPicture involved anywhere.
// QPicture::play() + nested drawPicture() (m_hdr_no_ctc/m_hdr_nc_ctc)
// caused a use-after-free crash (QRegion::begin, Qt5Gui+0x49af60)
// confirmed by analysis of 19+ coredumps.
// m_update_map=false: draw functions do not overwrite m_hovered_contacts_map.
if (m_element->linkedElements().isEmpty()) return;
QPen pen_;
pen_.setWidthF(0.5);
painter->save();
painter->setPen(pen_);
painter->setFont(QETApp::diagramTextsFont(5));
m_update_map = false;
XRefProperties::DisplayHas dh = m_properties.displayHas();
if (dh == XRefProperties::Cross)
drawAsCross(*painter);
else if (dh == XRefProperties::Contacts)
drawAsContacts(*painter);
painter->restore();
}
/**
@@ -320,7 +349,24 @@ void CrossRefItem::paint(
void CrossRefItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
event->accept();
QetGraphicsItem::showItem(m_hovered_contact);
// Find the element under the click position directly from the map,
// rather than relying on m_hovered_contact which may have been reset
// by hoverMoveEvent between the two clicks of the double-click.
QPointF pos = event->pos();
Element *target = m_hovered_contact;
if (!target) {
for (auto it = m_hovered_contacts_map.begin();
it != m_hovered_contacts_map.end(); ++it) {
if (it.value().contains(pos)) {
target = it.key();
break;
}
}
}
QetGraphicsItem::showItem(target);
}
/**
@@ -432,49 +478,41 @@ void CrossRefItem::linkedChanged()
/**
@brief CrossRefItem::buildHeaderContact
Draw the QPicture of m_hdr_no_ctc and m_hdr_nc_ctc
Draw NO and NC contact symbols directly onto painter.
Previously used QPicture (m_hdr_no_ctc/m_hdr_nc_ctc) which caused
use-after-free crashes via nested QPicture::play() calls.
*/
void CrossRefItem::buildHeaderContact()
void CrossRefItem::buildHeaderContact(QPainter &painter, QPointF no_pos, QPointF nc_pos)
{
if (!m_hdr_no_ctc.isNull() && !m_hdr_nc_ctc.isNull()) return;
//init the painter
QPainter qp;
QPen pen_;
pen_.setWidthF(0.2);
painter.save();
painter.setPen(pen_);
//draw the NO contact
if (m_hdr_no_ctc.isNull()) {
qp.begin(&m_hdr_no_ctc);
qp.setPen(pen_);
qp.drawLine(0, 3, 5, 3);
//draw the NO contact header symbol
painter.drawLine(no_pos.x()+0, no_pos.y()+3, no_pos.x()+5, no_pos.y()+3);
QPointF p1[3] = {
QPointF(5, 0),
QPointF(10, 3),
QPointF(15, 3),
QPointF(no_pos.x()+5, no_pos.y()+0),
QPointF(no_pos.x()+10, no_pos.y()+3),
QPointF(no_pos.x()+15, no_pos.y()+3),
};
qp.drawPolyline(p1,3);
qp.end();
}
painter.drawPolyline(p1, 3);
//draw the NC contact
if (m_hdr_nc_ctc.isNull()) {
qp.begin(&m_hdr_nc_ctc);
qp.setPen(pen_);
//draw the NC contact header symbol
QPointF p2[3] = {
QPointF(0, 3),
QPointF(5, 3),
QPointF(5, 0)
QPointF(nc_pos.x()+0, nc_pos.y()+3),
QPointF(nc_pos.x()+5, nc_pos.y()+3),
QPointF(nc_pos.x()+5, nc_pos.y()+0)
};
qp.drawPolyline(p2,3);
painter.drawPolyline(p2, 3);
QPointF p3[3] = {
QPointF(4, 0),
QPointF(10, 3),
QPointF(15, 3),
QPointF(nc_pos.x()+4, nc_pos.y()+0),
QPointF(nc_pos.x()+10, nc_pos.y()+3),
QPointF(nc_pos.x()+15, nc_pos.y()+3),
};
qp.drawPolyline(p3,3);
qp.end();
}
painter.drawPolyline(p3, 3);
painter.restore();
}
/**
@@ -492,11 +530,95 @@ void CrossRefItem::setUpCrossBoundingRect(QPainter &painter)
QStringList no_str, nc_str;
// Helper lambda: build "[13-14] pos" string for an element.
// For power contacts (e.g. 3-pole contactor), all named terminals
// are collected (e.g. "[1-2-3-4-5-6] pos").
// For other contacts (NO/NC/SW), only the first 2 named terminals
// are used — users are expected to name and order their terminals
// in the element editor.
// Helper lambda: build "[13-14] pos" for an element.
// - Power: all terminals sorted numerically → "[1-2-3-4-5-6] pos"
// - SW with typed terminals: show relevant pair per column (handled below)
// - Others: first 2 named terminals
auto buildLabel = [this](Element *elmt, bool is_no_col) -> QString {
const bool is_power =
elmt->kindInformations()["type"].toString() == "power";
const bool is_sw =
elmt->kindInformations()["state"].toString() == "SW";
QStringList tnames;
if (is_sw) {
// Check if terminals have explicit No/Nc/Common types
bool has_typed = false;
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
if (t->terminalType() == TerminalData::No ||
t->terminalType() == TerminalData::Nc ||
t->terminalType() == TerminalData::Common) {
has_typed = true; break;
}
}
if (has_typed) {
QString no_name, nc_name, common_name;
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
if (!t->name().isEmpty()) {
if (t->terminalType() == TerminalData::No) no_name = t->name();
else if (t->terminalType() == TerminalData::Nc) nc_name = t->name();
else if (t->terminalType() == TerminalData::Common) common_name = t->name();
}
}
// NO column: show NO+Common pair; NC column: show NC+Common pair
if (is_no_col)
tnames << no_name << common_name;
else
tnames << nc_name << common_name;
} else {
// Fallback: first 2 named terminals
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
const QString tn = t->name();
if (!tn.isEmpty()) { tnames << tn; if (tnames.size() >= 2) break; }
}
}
} else {
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
const QString tn = t->name();
if (!tn.isEmpty()) {
tnames << tn;
if (!is_power && tnames.size() >= 2) break;
}
}
if (is_power) {
std::sort(tnames.begin(), tnames.end(),
[](const QString &a, const QString &b){
int i_a = a.size();
while (i_a > 0 && a[i_a-1].isDigit()) --i_a;
int i_b = b.size();
while (i_b > 0 && b[i_b-1].isDigit()) --i_b;
bool a_ok = false, b_ok = false;
int ai = a.mid(i_a).toInt(&a_ok);
int bi = b.mid(i_b).toInt(&b_ok);
if (a_ok && b_ok && a.left(i_a) == b.left(i_b))
return ai < bi;
return a < b;
});
}
}
QString pos = elementPositionText(elmt, true);
if (!tnames.isEmpty() && m_properties.showTerminalName())
return QStringLiteral("[") + tnames.join("-") + QStringLiteral("] ") + pos;
return pos;
};
for (auto elmt : NOElements()) {
no_str.append(elementPositionText(elmt, true));
no_str.append(buildLabel(elmt, true));
}
for (auto elmt : NCElements()) {
nc_str.append(elementPositionText(elmt, true));
nc_str.append(buildLabel(elmt, false));
}
//There is no string to display, we return now
@@ -509,9 +631,10 @@ void CrossRefItem::setUpCrossBoundingRect(QPainter &painter)
QRectF no_bounding;
for (auto str : no_str)
{
QRectF bounding = painter.boundingRect(QRectF (), Qt::AlignCenter, str);
no_bounding = no_bounding.united(bounding);
QRectF bounding = painter.boundingRect(QRectF(0, 0, 500, 20), Qt::AlignLeft, str);
no_bounding.setHeight(no_bounding.height() + bounding.height());
if (bounding.width() > no_bounding.width())
no_bounding.setWidth(bounding.width());
}
//Adjust according to the NO
if (no_bounding.height() > default_bounding.height() - header)
@@ -523,9 +646,10 @@ void CrossRefItem::setUpCrossBoundingRect(QPainter &painter)
QRectF nc_bounding;
for (auto str : nc_str)
{
QRectF bounding = painter.boundingRect(QRectF (), Qt::AlignCenter, str);
nc_bounding = nc_bounding.united(bounding);
QRectF bounding = painter.boundingRect(QRectF(0, 0, 500, 20), Qt::AlignLeft, str);
nc_bounding.setHeight(nc_bounding.height() + bounding.height());
if (bounding.width() > nc_bounding.width())
nc_bounding.setWidth(bounding.width());
}
//Adjust according to the NC
if (nc_bounding.height() > default_bounding.height() - header)
@@ -549,7 +673,8 @@ void CrossRefItem::drawAsCross(QPainter &painter)
{
//calculate the size of the cross
setUpCrossBoundingRect(painter);
m_hovered_contacts_map.clear();
m_drawed_contacts = 0;
if (m_update_map) m_hovered_contacts_map.clear();
//Bounding rect is empty that mean there's no contact to draw
if (boundingRect().isEmpty()) return;
@@ -559,12 +684,11 @@ void CrossRefItem::drawAsCross(QPainter &painter)
painter.drawLine(br.width()/2, 0, br.width()/2, br.height()); //vertical line
painter.drawLine(0, header, br.width(), header); //horizontal line
//Add the symbolic contacts
buildHeaderContact();
QPointF p((m_bounding_rect.width()/4) - (m_hdr_no_ctc.width()/2), 0);
painter.drawPicture (p, m_hdr_no_ctc);
p.setX((m_bounding_rect.width() * 3/4) - (m_hdr_nc_ctc.width()/2));
painter.drawPicture (p, m_hdr_nc_ctc);
//Add the symbolic contacts (drawn directly, no QPicture)
static const qreal hdr_symbol_width = 15.0;
QPointF no_pos((m_bounding_rect.width()/4) - (hdr_symbol_width/2), 0);
QPointF nc_pos((m_bounding_rect.width() * 3/4) - (hdr_symbol_width/2), 0);
buildHeaderContact(painter, no_pos, nc_pos);
//and fill it
fillCrossRef(painter);
@@ -581,7 +705,7 @@ void CrossRefItem::drawAsContacts(QPainter &painter)
return;
m_drawed_contacts = 0;
m_hovered_contacts_map.clear();
if (m_update_map) m_hovered_contacts_map.clear();
QRectF bounding_rect;
//Draw each linked contact
@@ -605,7 +729,7 @@ void CrossRefItem::drawAsContacts(QPainter &painter)
else if (type == "delayOff") option += DelayOff;
else if (type == "delayOnOff") option += DelayOnOff;
QRectF br = drawContact(painter, option, elmt);
QRectF br = drawContact(painter, option, elmt, i);
bounding_rect = bounding_rect.united(br);
}
}
@@ -624,9 +748,81 @@ void CrossRefItem::drawAsContacts(QPainter &painter)
@param elmt : the element to display text (the position of the contact)
@return The bounding rect of the draw (contact + text)
*/
QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt)
QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, int pole_index)
{
QString str = elementPositionText(elmt);
// Collect terminal names from the element definition (.elmt)
// e.g. name="13" and name="14" on each terminal
// For power contacts, sort numerically and pick the pair for pole_index.
// For SW contacts with typed terminals (No/Nc/Common), filter by role.
QStringList terminal_names;
const bool is_power_ctc =
elmt->kindInformations()["type"].toString() == "power";
const bool is_sw = (flags & SW) && !(flags & NOC);
// Check if SW terminals have explicit No/Nc/Common types
bool sw_has_typed_terminals = false;
if (is_sw) {
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
if (t->terminalType() == TerminalData::No ||
t->terminalType() == TerminalData::Nc ||
t->terminalType() == TerminalData::Common) {
sw_has_typed_terminals = true;
break;
}
}
}
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
const QString tname = t->name();
if (!tname.isEmpty())
terminal_names << tname;
}
if (is_power_ctc) {
// Sort terminals alphanumerically so names like "R1","R2"... or "1","2"...
// are ordered correctly. Extract trailing digits for numeric comparison;
// fall back to full string comparison when no digits are found.
std::sort(terminal_names.begin(), terminal_names.end(),
[](const QString &a, const QString &b){
// Extract trailing numeric part
int i_a = a.size();
while (i_a > 0 && a[i_a-1].isDigit()) --i_a;
int i_b = b.size();
while (i_b > 0 && b[i_b-1].isDigit()) --i_b;
bool a_ok = false, b_ok = false;
int ai = a.mid(i_a).toInt(&a_ok);
int bi = b.mid(i_b).toInt(&b_ok);
if (a_ok && b_ok && a.left(i_a) == b.left(i_b))
return ai < bi;
return a < b;
});
// Pick the pair for this pole: pole 0 → [0,1], pole 1 → [2,3], etc.
int idx = pole_index * 2;
if (idx + 1 < terminal_names.size())
terminal_names = QStringList() << terminal_names[idx] << terminal_names[idx+1];
else
terminal_names.clear();
} else if (is_sw && sw_has_typed_terminals) {
// Build [NO_name, Common_name, NC_name] from typed terminals
QString no_name, nc_name, common_name;
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
if (!t->name().isEmpty()) {
if (t->terminalType() == TerminalData::No) no_name = t->name();
else if (t->terminalType() == TerminalData::Nc) nc_name = t->name();
else if (t->terminalType() == TerminalData::Common) common_name = t->name();
}
}
// drawText expects: [0]=NC, [1]=NO, [2]=Common
// (drawText uses [1] for NO top-left, [0] for NC bottom-left, [2] for Common right)
terminal_names.clear();
terminal_names << nc_name << no_name << common_name;
}
int offset = m_drawed_contacts*10;
QRectF bounding_rect = QRectF(0, offset, 24, 10);
@@ -643,15 +839,17 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt)
painter.drawLine(0, offset+6, 8, offset+6);
painter.drawLine(16, offset+6, 24, offset+6);
///take example of this code for display the terminal text
/*QFont font = QETApp::diagramTextsFont(4);
font.setBold(true);
painter.setFont(font);
// Draw terminal names on each side of the contact symbol
// terminal_names[0] on the left, terminal_names[1] on the right
if (!terminal_names.isEmpty() && m_properties.showTerminalName()) {
painter.setFont(QETApp::diagramTextsFont(4));
QRectF bt(0, offset, 24, 10);
int txt = 10 + m_drawed_contacts;
painter.drawText(bt, Qt::AlignLeft|Qt::AlignTop, QString::number(txt));
painter.drawText(bt, Qt::AlignRight|Qt::AlignTop, QString::number(txt));
painter.setFont(QETApp::diagramTextsFont(5));*/
if (terminal_names.size() >= 1)
painter.drawText(bt, Qt::AlignLeft|Qt::AlignTop, terminal_names[0]);
if (terminal_names.size() >= 2)
painter.drawText(bt, Qt::AlignRight|Qt::AlignTop, terminal_names[1]);
painter.setFont(QETApp::diagramTextsFont(5));
}
//draw open contact
if (flags &NO) {
@@ -729,14 +927,8 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt)
painter.drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, str);
bounding_rect = bounding_rect.united(text_rect);
if (m_hovered_contacts_map.contains(elmt))
{
if (m_update_map)
m_hovered_contacts_map.insert(elmt, bounding_rect);
}
else
{
m_hovered_contacts_map.insert(elmt, bounding_rect);
}
++m_drawed_contacts;
}
@@ -768,6 +960,26 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt)
};
painter.drawPolyline(p2, 3);
// Draw terminal names for switch contact (3 terminals)
// terminal_names[0] = NO side (top left)
// terminal_names[1] = NC side (bottom left)
// terminal_names[2] = common side (right)
if (!terminal_names.isEmpty() && m_properties.showTerminalName()) {
painter.setFont(QETApp::diagramTextsFont(4));
// Sort order from parseTerminal (top->bottom, left->right):
// [0]=12 (NO, top-left), [1]=14 (common, top-center), [2]=13 (NC, bottom-center)
if (terminal_names.size() >= 1)
painter.drawText(QRectF(0, offset, 8, 8),
Qt::AlignLeft|Qt::AlignTop, terminal_names[1]); // 12 NO left
if (terminal_names.size() >= 2)
painter.drawText(QRectF(16, offset+4, 8, 6),
Qt::AlignRight|Qt::AlignTop, terminal_names[2]); // 14 common right
if (terminal_names.size() >= 3)
painter.drawText(QRectF(0, offset+9, 8, 6),
Qt::AlignLeft|Qt::AlignTop, terminal_names[0]); // 13 NC left-bottom
painter.setFont(QETApp::diagramTextsFont(5));
}
//Draw the half ellipse off delay
if (flags &Delay)
{
@@ -799,12 +1011,8 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt)
str);
bounding_rect = bounding_rect.united(text_rect);
if (m_hovered_contacts_map.contains(elmt)) {
if (m_update_map)
m_hovered_contacts_map.insert(elmt, bounding_rect);
}
else {
m_hovered_contacts_map.insert(elmt, bounding_rect);
}
//a switch contact take place of two normal contact
m_drawed_contacts += 2;
@@ -835,12 +1043,8 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt)
str);
bounding_rect = bounding_rect.united(text_rect);
if (m_hovered_contacts_map.contains(elmt)) {
if (m_update_map)
m_hovered_contacts_map.insert(elmt, bounding_rect);
}
else {
m_hovered_contacts_map.insert(elmt, bounding_rect);
}
++m_drawed_contacts;
}
return bounding_rect;
@@ -866,7 +1070,60 @@ void CrossRefItem::fillCrossRef(QPainter &painter)
m_hovered_contact == elmt ? pen.setColor(Qt::blue) :pen.setColor(Qt::black);
painter.setPen(pen);
// Collect terminal names for NO column.
// Power: all terminals sorted numerically.
// SW with typed terminals: NO+Common pair.
// Others: first 2 named terminals.
const bool is_power_no =
elmt->kindInformations()["type"].toString() == "power";
const bool is_sw_no =
elmt->kindInformations()["state"].toString() == "SW";
QStringList tnames;
if (is_sw_no) {
bool has_typed = false;
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
if (t->terminalType() == TerminalData::No ||
t->terminalType() == TerminalData::Nc ||
t->terminalType() == TerminalData::Common) {
has_typed = true; break;
}
}
if (has_typed) {
QString no_name, common_name;
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
if (!t->name().isEmpty()) {
if (t->terminalType() == TerminalData::No) no_name = t->name();
else if (t->terminalType() == TerminalData::Common) common_name = t->name();
}
}
tnames << no_name << common_name;
} else {
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
const QString tn = t->name();
if (!tn.isEmpty()) { tnames << tn; if (tnames.size() >= 2) break; }
}
}
} else {
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
const QString tn = t->name();
if (!tn.isEmpty()) {
tnames << tn;
if (!is_power_no && tnames.size() >= 2) break;
}
}
}
QString terminal_label;
if (!tnames.isEmpty() && m_properties.showTerminalName())
terminal_label = QStringLiteral("[") + tnames.join("-") + QStringLiteral("]");
QString str = elementPositionText(elmt, true);
if (!terminal_label.isEmpty())
str = terminal_label + QStringLiteral(" ") + str;
QRectF bounding = painter.boundingRect(
QRectF(no_top_left,
QSize(middle_cross, 1)),
@@ -874,13 +1131,11 @@ void CrossRefItem::fillCrossRef(QPainter &painter)
str);
painter.drawText(bounding, Qt::AlignLeft, str);
if (m_hovered_contacts_map.contains(elmt))
{
m_hovered_contacts_map.insert(elmt, bounding);
}
else
{
m_hovered_contacts_map.insert(elmt, bounding);
if (m_update_map) {
QString pos_str = elementPositionText(elmt, true);
QRectF pos_rect = painter.boundingRect(bounding, Qt::AlignRight, pos_str);
pos_rect.adjust(-2, -1, 2, 1); // extend hit area slightly
m_hovered_contacts_map.insert(elmt, pos_rect);
}
no_top_left.ry() += bounding.height();
@@ -895,7 +1150,60 @@ void CrossRefItem::fillCrossRef(QPainter &painter)
:pen.setColor(Qt::black);
painter.setPen(pen);
// Collect terminal names for NC column.
// Power: all terminals sorted numerically.
// SW with typed terminals: NC+Common pair.
// Others: first 2 named terminals.
const bool is_power_nc =
elmt->kindInformations()["type"].toString() == "power";
const bool is_sw_nc =
elmt->kindInformations()["state"].toString() == "SW";
QStringList tnames_nc;
if (is_sw_nc) {
bool has_typed = false;
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
if (t->terminalType() == TerminalData::No ||
t->terminalType() == TerminalData::Nc ||
t->terminalType() == TerminalData::Common) {
has_typed = true; break;
}
}
if (has_typed) {
QString nc_name, common_name;
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
if (!t->name().isEmpty()) {
if (t->terminalType() == TerminalData::Nc) nc_name = t->name();
else if (t->terminalType() == TerminalData::Common) common_name = t->name();
}
}
tnames_nc << nc_name << common_name;
} else {
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
const QString tn = t->name();
if (!tn.isEmpty()) { tnames_nc << tn; if (tnames_nc.size() >= 2) break; }
}
}
} else {
for (Terminal *t : elmt->terminals()) {
if (!t) continue;
const QString tn = t->name();
if (!tn.isEmpty()) {
tnames_nc << tn;
if (!is_power_nc && tnames_nc.size() >= 2) break;
}
}
}
QString terminal_label;
if (!tnames_nc.isEmpty() && m_properties.showTerminalName())
terminal_label = QStringLiteral("[") + tnames_nc.join("-") + QStringLiteral("]");
QString str = elementPositionText(elmt, true);
if (!terminal_label.isEmpty())
str = terminal_label + QStringLiteral(" ") + str;
QRectF bounding = painter.boundingRect(
QRectF(nc_top_left,
QSize(middle_cross, 1)),
@@ -903,13 +1211,11 @@ void CrossRefItem::fillCrossRef(QPainter &painter)
str);
painter.drawText(bounding, Qt::AlignRight, str);
if (m_hovered_contacts_map.contains(elmt))
{
m_hovered_contacts_map.insert(elmt, bounding);
}
else
{
m_hovered_contacts_map.insert(elmt, bounding);
if (m_update_map) {
QString pos_str = elementPositionText(elmt, true);
QRectF pos_rect = painter.boundingRect(bounding, Qt::AlignRight, pos_str);
pos_rect.adjust(-2, -1, 2, 1); // extend hit area slightly
m_hovered_contacts_map.insert(elmt, pos_rect);
}
nc_top_left.ry() += bounding.height();
+3 -4
View File
@@ -22,7 +22,6 @@
#include <QGraphicsObject>
#include <QMultiMap>
#include <QPicture>
class Element;
class DynamicElementTextItem;
@@ -103,11 +102,11 @@ class CrossRefItem : public QGraphicsObject
private:
void linkedChanged();
void buildHeaderContact();
void buildHeaderContact(QPainter &painter, QPointF no_pos, QPointF nc_pos);
void setUpCrossBoundingRect(QPainter &painter);
void drawAsCross(QPainter &painter);
void drawAsContacts(QPainter &painter);
QRectF drawContact(QPainter &painter, int flags, Element *elmt);
QRectF drawContact(QPainter &painter, int flags, Element *elmt, int pole_index = 0);
void fillCrossRef(QPainter &painter);
void AddExtraInfo(QPainter &painter, const QString&);
QList<Element *> NOElements() const;
@@ -117,10 +116,10 @@ class CrossRefItem : public QGraphicsObject
private:
Element *m_element; //element to display the cross reference
QRectF m_bounding_rect;
QPicture m_drawing, m_hdr_no_ctc, m_hdr_nc_ctc;
QPainterPath m_shape_path;
XRefProperties m_properties;
int m_drawed_contacts;
bool m_update_map = false;
QMultiMap <Element *, QRectF> m_hovered_contacts_map;
Element *m_hovered_contact = nullptr;
DynamicElementTextItem *m_text = nullptr;
@@ -302,7 +302,8 @@ void DynamicElementTextItem::refreshLabelConnection()
if ((m_text_from == ElementInfo && m_info_name == "label") ||
(m_text_from == CompositeText && m_composite_text.contains("%{label}")))
{
if(m_parent_element.data()->linkType() & Element::AllReport)
if((m_parent_element.data()->linkType() & Element::AllReport) ||
m_parent_element.data()->linkType() == Element::ConductorDefinition)
{
updateReportFormulaConnection();
updateReportText();
@@ -418,7 +419,8 @@ void DynamicElementTextItem::setInfoName(const QString &info_name)
updateXref();
}
if (m_parent_element && (m_parent_element.data()->linkType() & Element::AllReport)) //special treatment for report
if (m_parent_element && ((m_parent_element.data()->linkType() & Element::AllReport) ||
m_parent_element.data()->linkType() == Element::ConductorDefinition)) //special treatment for report
{
if(old_info_name != info_name)
{
@@ -472,7 +474,8 @@ void DynamicElementTextItem::setCompositeText(const QString &text)
updateXref();
}
if (m_parent_element && (m_parent_element.data()->linkType() & Element::AllReport)) //special treatment for report
if (m_parent_element && ((m_parent_element.data()->linkType() & Element::AllReport) ||
m_parent_element.data()->linkType() == Element::ConductorDefinition)) //special treatment for report
{
/*
* May be in some case the old and new composite text both have the var %{label},
@@ -726,7 +729,8 @@ QVariant DynamicElementTextItem::itemChange(QGraphicsItem::GraphicsItemChange ch
if(!m_parent_element.data()->linkedElements().isEmpty())
masterChanged();
}
else if(m_parent_element.data()->linkType() & Element::AllReport)
else if((m_parent_element.data()->linkType() & Element::AllReport) ||
m_parent_element.data()->linkType() == Element::ConductorDefinition)
{
//Get the report formula, and add connection to keep up to date the formula.
if (m_parent_element.data()->diagram() && m_parent_element.data()->diagram()->project())
@@ -1027,7 +1031,8 @@ void DynamicElementTextItem::clearFormulaConnection()
void DynamicElementTextItem::updateReportFormulaConnection()
{
if(!(m_parent_element.data()->linkType() & Element::AllReport))
if(!(m_parent_element.data()->linkType() & Element::AllReport) &&
m_parent_element.data()->linkType() != Element::ConductorDefinition)
return;
removeConnectionForReportFormula(m_report_formula);
@@ -1041,7 +1046,8 @@ void DynamicElementTextItem::updateReportFormulaConnection()
*/
void DynamicElementTextItem::updateReportText()
{
if(!(m_parent_element.data()->linkType() & Element::AllReport))
if(!(m_parent_element.data()->linkType() & Element::AllReport) &&
m_parent_element.data()->linkType() != Element::ConductorDefinition)
return;
if (m_text_from == ElementInfo && m_info_name == "label")
@@ -1123,7 +1129,8 @@ void DynamicElementTextItem::conductorWasRemoved(Conductor *conductor)
*/
void DynamicElementTextItem::setPotentialConductor()
{
if(parentElement() && (parentElement()->linkType() & Element::AllReport))
if(parentElement() && ((parentElement()->linkType() & Element::AllReport) || (parentElement()->linkType() == Element::ConductorDefinition) ||
parentElement()->linkType() == Element::ConductorDefinition))
{
if(parentElement()->terminals().isEmpty())
return;
@@ -1156,6 +1163,7 @@ void DynamicElementTextItem::setPotentialConductor()
connect(m_watched_conductor.data(), &Conductor::propertiesChange, this, &DynamicElementTextItem::conductorPropertiesChanged);
}
}
conductorPropertiesChanged();
}
else //This text haven't got a parent element, then ther isn't a conductor in the potential
{
@@ -1172,7 +1180,8 @@ void DynamicElementTextItem::setPotentialConductor()
*/
void DynamicElementTextItem::conductorPropertiesChanged()
{
if(m_parent_element && (m_parent_element.data()->linkType() & Element::AllReport))
if(m_parent_element && ((m_parent_element.data()->linkType() & Element::AllReport) || (m_parent_element.data()->linkType() == Element::ConductorDefinition) ||
m_parent_element.data()->linkType() == Element::ConductorDefinition))
{
if(m_text_from == ElementInfo)
{
@@ -1201,7 +1210,8 @@ QString DynamicElementTextItem::reportReplacedCompositeText() const
{
QString string;
if(m_parent_element.data()->linkType() & Element::AllReport)
if((m_parent_element.data()->linkType() & Element::AllReport) || (m_parent_element.data()->linkType() == Element::ConductorDefinition) ||
m_parent_element.data()->linkType() == Element::ConductorDefinition)
{
string = m_composite_text;
+33
View File
@@ -36,6 +36,7 @@
#include "../qetxml.h"
#include "../qetversion.h"
#include "qgraphicsitemutility.h"
#include <QDebug>
#include <QDomElement>
#include <utility>
@@ -431,6 +432,11 @@ bool Element::buildFromXml(const QDomElement &xml_def_elmt, int *state)
m_data.fromXml(xml_def_elmt);
setToolTip(name());
QString my_type_str = xml_def_elmt.attribute(QStringLiteral("link_type"), QStringLiteral("simple"));
if (my_type_str == QLatin1String("conductor_definition")) {
m_link_type = Element::ConductorDefinition;
}
//load kind informations
m_kind_informations.fromXml(
xml_def_elmt.firstChildElement(QStringLiteral("kindInformations")),
@@ -627,6 +633,9 @@ Terminal *Element::parseTerminal(const QDomElement &dom_element)
Terminal *new_terminal = new Terminal(data, this);
m_terminals << new_terminal;
connect(new_terminal, &Terminal::conductorWasAdded, this, &Element::updateConductorTexts);
connect(new_terminal, &Terminal::conductorWasRemoved, this, &Element::updateConductorTexts);
//Sort from top to bottom and left to right
std::sort(m_terminals.begin(),
m_terminals.end(),
@@ -1288,6 +1297,8 @@ QString Element::linkTypeToString() const
return QStringLiteral("Terminale");
case Thumbnail:
return QStringLiteral("Thumbnail");
case ConductorDefinition:
return QStringLiteral("ConductorDefinition");
default:
return QStringLiteral("Unknown");
}
@@ -1555,3 +1566,25 @@ ElementsLocation Element::location() const
{
return m_location;
}
/**
* @brief Element::updateConductorTexts
*Slot that is triggered when a cable is *
*connected to or disconnected from a terminal on this component.
*/
/**
* @brief Element::updateConductorTexts
*/
void Element::updateConductorTexts()
{
if (m_link_type != Element::ConductorDefinition) {
return;
}
for (DynamicElementTextItem *deti : m_dynamic_text_list) {
if (deti) {
deti->setPotentialConductor();
deti->updateLabel();
}
}
}
+4 -1
View File
@@ -59,7 +59,8 @@ class Element : public QetGraphicsItem
Master = 8,
Slave = 16,
Terminale = 32,
Thumbnail = 64};
Thumbnail = 64,
ConductorDefinition = 128};
Element(const ElementsLocation &location,
QGraphicsItem * = nullptr,
@@ -95,6 +96,8 @@ class Element : public QetGraphicsItem
DynamicElementTextItem *text,
ElementTextItemGroup *group);
public slots:
void updateConductorTexts();
public:
QList<Terminal *> terminals() const;
+9
View File
@@ -753,6 +753,15 @@ QString Terminal::name() const
return d->m_name;
}
/**
@brief Terminal::terminalType
@return the type of this terminal (Generic, Inner, Outer, No, Nc, Common)
*/
TerminalData::Type Terminal::terminalType() const
{
return d->m_type;
}
/**
@brief Conductor::relatedPotentialTerminal
Return terminal at the same potential from the same
+3 -1
View File
@@ -18,13 +18,14 @@
#ifndef TERMINAL_H
#define TERMINAL_H
#include "../qet.h"
#include "../properties/terminaldata.h"
#include <QtWidgets>
#include <QtXml>
class Conductor;
class Diagram;
class Element;
class TerminalData;
/**
@brief The Terminal class
@@ -75,6 +76,7 @@ class Terminal : public QGraphicsObject
Element *parentElement () const;
QUuid uuid () const;
QString name () const;
TerminalData::Type terminalType() const;
QList<Conductor *> conductors() const;
Qet::Orientation orientation() const;
+3
View File
@@ -308,6 +308,9 @@ void ElementPropertiesWidget::updateUi()
case Element::Terminale:
m_list_editor << new ElementInfoWidget(m_element, this);
break;
case Element::ConductorDefinition:
break;
default:
break;
}
+2
View File
@@ -139,6 +139,7 @@ void XRefPropertiesWidget::saveProperties(int index) {
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "right") xrp.setXrefPos(Qt::AlignRight);
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "alignment") xrp.setXrefPos(Qt::AlignBaseline);
xrp.setShowPowerContac(ui->m_show_power_cb->isChecked());
xrp.setShowTerminalName(ui->m_show_terminal_name_cb->isChecked());
xrp.setPrefix("power", ui->m_power_prefix_le->text());
xrp.setPrefix("delay", ui->m_delay_prefix_le->text());
xrp.setPrefix("switch", ui->m_switch_prefix_le->text());
@@ -190,6 +191,7 @@ void XRefPropertiesWidget::updateDisplay()
else if(xrp.getXrefPos() == Qt::AlignBaseline) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("alignment"));
else if(xrp.getXrefPos() == Qt::AlignBottom) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("bottom"));
ui->m_show_power_cb->setChecked(xrp.showPowerContact());
ui->m_show_terminal_name_cb->setChecked(xrp.showTerminalName());
ui->m_power_prefix_le-> setText(xrp.prefix("power"));
ui->m_delay_prefix_le-> setText(xrp.prefix("delay"));
ui->m_switch_prefix_le->setText(xrp.prefix("switch"));
+8
View File
@@ -108,6 +108,13 @@
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="m_show_terminal_name_cb">
<property name="text">
<string>Afficher les numéros de bornes dans les Xrefs</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="m_display_has_contacts_rb">
<property name="text">
@@ -283,6 +290,7 @@
<tabstop>m_master_le</tabstop>
<tabstop>m_slave_le</tabstop>
<tabstop>m_show_power_cb</tabstop>
<tabstop>m_show_terminal_name_cb</tabstop>
<tabstop>m_power_prefix_le</tabstop>
<tabstop>m_delay_prefix_le</tabstop>
<tabstop>m_switch_prefix_le</tabstop>
+160 -112
View File
@@ -5,9 +5,7 @@
#include <QTextStream>
#include <QDomDocument>
#include <QFile>
#include <QRegularExpression>
#include <QQueue>
#include <QSet>
#include <algorithm>
WiringListExport::WiringListExport(QETProject *project, QWidget *parent) :
QObject(parent),
@@ -45,8 +43,27 @@ QDomElement WiringListExport::climbToDiagram(QDomNode node) const
QMap<QString, ElementInfo> WiringListExport::collectElementsInfo(const QDomElement &root) const
{
QMap<QString, ElementInfo> infoMap;
QDomNodeList elements = root.elementsByTagName("element");
QSet<QString> placeholderTypes;
QDomElement collection = root.firstChildElement("collection");
if (!collection.isNull()) {
QDomNodeList defs = collection.elementsByTagName("definition");
for (int i = 0; i < defs.size(); ++i) {
QDomElement def = defs.at(i).toElement();
QString ltype = def.attribute("link_type");
if (ltype == "next_report" || ltype == "previous_report") {
QDomElement parentEl = def.parentNode().toElement();
if (parentEl.tagName().toLower() == "element") {
QString name = parentEl.attribute("name");
if (!name.isEmpty()) {
placeholderTypes.insert(name);
}
}
}
}
}
QDomNodeList elements = root.elementsByTagName("element");
for (int i = 0; i < elements.size(); ++i) {
QDomElement el = elements.at(i).toElement();
QString uuid = normalizeUuid(el.attribute("uuid", el.attribute("id", "")));
@@ -75,10 +92,13 @@ QMap<QString, ElementInfo> WiringListExport::collectElementsInfo(const QDomEleme
}
}
QString typeVal = el.attribute("type").toLower();
if (typeVal.contains("naechste") || typeVal.contains("vorherige") ||
typeVal.contains("next") || typeVal.contains("previous")) {
QString typeVal = el.attribute("type");
info.isPlaceholder = false;
for (const QString &ptype : placeholderTypes) {
if (typeVal.endsWith(ptype)) {
info.isPlaceholder = true;
break;
}
}
infoMap.insert(uuid, info);
@@ -102,7 +122,15 @@ QList<ConductorData> WiringListExport::collectConductors(const QDomElement &root
data.el2_uuid = normalizeUuid(cond.attribute("element2", cond.attribute("element2id", "")));
data.element1_label = cond.attribute("element1_label");
if (data.element1_label.isEmpty()) {
data.element1_label = cond.attribute("element1_linked");
}
data.element2_label = cond.attribute("element2_label");
if (data.element2_label.isEmpty()) {
data.element2_label = cond.attribute("element2_linked");
}
data.terminalname1 = cond.attribute("terminalname1");
data.terminalname2 = cond.attribute("terminalname2");
data.tension_protocol = cond.attribute("tension_protocol");
@@ -119,101 +147,6 @@ QList<ConductorData> WiringListExport::collectConductors(const QDomElement &root
return conductors;
}
void WiringListExport::resolveEndpoints(QList<ConductorData> &conductors, const QMap<QString, ElementInfo> &elementsInfo) const
{
QRegularExpression numericLabelRe("^\\d+(\\.\\d+)?$");
QMap<QString, QList<ConductorData>> el_to_cons;
for (const ConductorData &c : conductors) {
if (!c.el1_uuid.isEmpty()) el_to_cons[c.el1_uuid].append(c);
if (!c.el2_uuid.isEmpty()) el_to_cons[c.el2_uuid].append(c);
}
for (int i = 0; i < conductors.size(); ++i) {
ConductorData &c = conductors[i];
auto resolveSide = [&](const QString &startUuid, QString &outLabel, QString &outTerminal) {
if (startUuid.isEmpty() || !elementsInfo.contains(startUuid)) return;
const ElementInfo &startInfo = elementsInfo[startUuid];
if (!startInfo.links.isEmpty() || startInfo.isPlaceholder) {
QQueue<QString> q;
QSet<QString> visited;
q.enqueue(startUuid);
visited.insert(startUuid);
int depth = 0;
while (!q.isEmpty() && depth < 3) {
int levelSize = q.size();
for (int k = 0; k < levelSize; ++k) {
QString curr = q.dequeue();
if (elementsInfo.contains(curr)) {
const ElementInfo &currInfo = elementsInfo[curr];
if (!currInfo.isPlaceholder && !currInfo.label.isEmpty() && !numericLabelRe.match(currInfo.label).hasMatch()) {
outLabel = currInfo.label;
return;
}
for (const QString &lnk : currInfo.links) {
if (!visited.contains(lnk)) {
visited.insert(lnk);
q.enqueue(lnk);
}
}
}
for (const ConductorData &cond : el_to_cons.value(curr)) {
if (cond.index == c.index) continue;
QString other;
QString terminalHint;
if (cond.el1_uuid == curr) {
other = cond.el2_uuid;
terminalHint = cond.terminalname2;
} else {
other = cond.el1_uuid;
terminalHint = cond.terminalname1;
}
if (!other.isEmpty() && !visited.contains(other)) {
if (elementsInfo.contains(other)) {
const ElementInfo &oInfo = elementsInfo[other];
if (!oInfo.isPlaceholder && !oInfo.label.isEmpty() && !numericLabelRe.match(oInfo.label).hasMatch()) {
outLabel = oInfo.label;
if (outTerminal.isEmpty()) outTerminal = terminalHint;
return;
}
}
visited.insert(other);
q.enqueue(other);
}
}
}
depth++;
}
} else {
if (outLabel.isEmpty()) {
outLabel = startInfo.label.isEmpty() ? startInfo.name : startInfo.label;
}
}
};
bool p1 = elementsInfo.value(c.el1_uuid).isPlaceholder;
bool p2 = elementsInfo.value(c.el2_uuid).isPlaceholder;
if (c.element1_label.isEmpty() || p1) {
if (p1) c.element1_label = "";
resolveSide(c.el1_uuid, c.element1_label, c.terminalname1);
}
if (c.element2_label.isEmpty() || p2) {
if (p2) c.element2_label = "";
resolveSide(c.el2_uuid, c.element2_label, c.terminalname2);
}
}
}
void WiringListExport::toCsv()
{
if (!m_project) return;
@@ -243,25 +176,140 @@ void WiringListExport::toCsv()
QMap<QString, ElementInfo> elementsInfo = collectElementsInfo(doc.documentElement());
QList<ConductorData> conductors = collectConductors(doc.documentElement());
resolveEndpoints(conductors, elementsInfo);
QList<ConductorData> uniqueConductors;
QSet<QString> seenConnections;
QMap<QString, ConductorData> partialWires;
for (const ConductorData &c : conductors) {
if (c.element1_label.isEmpty() && c.element2_label.isEmpty()) continue;
auto normalizePartial = [](ConductorData c, const QString &ph_uuid) {
if (c.el1_uuid == ph_uuid) {
std::swap(c.el1_uuid, c.el2_uuid);
std::swap(c.element1_label, c.element2_label);
std::swap(c.terminalname1, c.terminalname2);
}
return c;
};
QString sideA = c.element1_label + ":" + c.terminalname1;
QString sideB = c.element2_label + ":" + c.terminalname2;
auto mergeField = [](const QString &a, const QString &b) {
QString at = a.trimmed();
QString bt = b.trimmed();
if (at.isEmpty()) return bt;
if (bt.isEmpty()) return at;
if (at == bt) return at;
return at + ", " + bt;
};
QString key = (sideA < sideB) ? (sideA + "||" + sideB) : (sideB + "||" + sideA);
for (int i = 0; i < conductors.size(); ++i) {
ConductorData c = conductors[i];
if (!seenConnections.contains(key)) {
seenConnections.insert(key);
if (c.element1_label.isEmpty() && elementsInfo.contains(c.el1_uuid)) {
c.element1_label = elementsInfo[c.el1_uuid].label;
if (c.element1_label.isEmpty()) c.element1_label = elementsInfo[c.el1_uuid].name;
}
if (c.element2_label.isEmpty() && elementsInfo.contains(c.el2_uuid)) {
c.element2_label = elementsInfo[c.el2_uuid].label;
if (c.element2_label.isEmpty()) c.element2_label = elementsInfo[c.el2_uuid].name;
}
bool el1_ph = elementsInfo.value(c.el1_uuid).isPlaceholder;
bool el2_ph = elementsInfo.value(c.el2_uuid).isPlaceholder;
if (!el1_ph && !el2_ph) {
uniqueConductors.append(c);
continue;
}
if (el1_ph && el2_ph) {
uniqueConductors.append(c);
continue;
}
QString ph_uuid = el1_ph ? c.el1_uuid : c.el2_uuid;
ConductorData normC = normalizePartial(c, ph_uuid);
QString matching_ph_uuid;
if (!elementsInfo[ph_uuid].links.isEmpty()) {
matching_ph_uuid = elementsInfo[ph_uuid].links.first();
}
if (!matching_ph_uuid.isEmpty() && partialWires.contains(matching_ph_uuid)) {
ConductorData otherHalf = partialWires.take(matching_ph_uuid);
ConductorData merged;
merged.folio = mergeField(otherHalf.folio, normC.folio);
merged.el1_uuid = otherHalf.el1_uuid;
merged.element1_label = otherHalf.element1_label;
merged.terminalname1 = otherHalf.terminalname1;
merged.el2_uuid = normC.el1_uuid;
merged.element2_label = normC.element1_label;
merged.terminalname2 = normC.terminalname1;
merged.tension_protocol = mergeField(otherHalf.tension_protocol, normC.tension_protocol);
merged.conductor_color = mergeField(otherHalf.conductor_color, normC.conductor_color);
merged.conductor_section = mergeField(otherHalf.conductor_section, normC.conductor_section);
merged.function = mergeField(otherHalf.function, normC.function);
uniqueConductors.append(merged);
} else {
partialWires.insert(ph_uuid, normC);
}
}
for (const ConductorData &leftover : partialWires.values()) {
uniqueConductors.append(leftover);
}
for (ConductorData &c : uniqueConductors) {
if (!c.element2_label.isEmpty() && (c.element1_label.isEmpty() || c.element2_label.toLower() < c.element1_label.toLower())) {
std::swap(c.element1_label, c.element2_label);
std::swap(c.terminalname1, c.terminalname2);
std::swap(c.el1_uuid, c.el2_uuid);
}
}
std::sort(uniqueConductors.begin(), uniqueConductors.end(), [](const ConductorData &a, const ConductorData &b) {
QStringList partsA = a.folio.split(',');
QStringList partsB = b.folio.split(',');
int minLen = std::min(partsA.size(), partsB.size());
int folioCmp = 0;
for (int i = 0; i < minLen; ++i) {
bool okA, okB;
int numA = partsA[i].trimmed().toInt(&okA);
int numB = partsB[i].trimmed().toInt(&okB);
if (okA && okB) {
if (numA != numB) {
folioCmp = (numA < numB) ? -1 : 1;
break;
}
} else {
int strCmp = partsA[i].trimmed().compare(partsB[i].trimmed(), Qt::CaseInsensitive);
if (strCmp != 0) {
folioCmp = strCmp;
break;
}
}
}
if (folioCmp == 0 && partsA.size() != partsB.size()) {
folioCmp = (partsA.size() < partsB.size()) ? -1 : 1;
}
if (folioCmp != 0) return folioCmp < 0;
int el1Cmp = a.element1_label.toLower().compare(b.element1_label.toLower());
if (el1Cmp != 0) return el1Cmp < 0;
int el2Cmp = a.element2_label.toLower().compare(b.element2_label.toLower());
if (el2Cmp != 0) return el2Cmp < 0;
int term1Cmp = a.terminalname1.compare(b.terminalname1);
if (term1Cmp != 0) return term1Cmp < 0;
return a.terminalname2 < b.terminalname2;
});
QTextStream out(&file);
out << tr("Page", "Wiring list CSV header") << ";"
<< tr("Composant 1", "Wiring list CSV header") << ";"
+1 -11
View File
@@ -12,7 +12,6 @@ class QWidget;
class QDomElement;
class QDomNode;
// Internal data structures for parsing the XML graph
struct ElementInfo {
QString folio;
QStringList links;
@@ -34,18 +33,11 @@ struct ConductorData {
QString conductor_section;
QString function;
QString folio;
// Resolved endpoints
QString chosen_a_uuid;
QString chosen_a_label;
QString chosen_b_uuid;
QString chosen_b_label;
};
/**
* @brief The WiringListExport class
* Handles the export of the wiring list (Verdrahtungsplan) to a CSV file.
* Automatically resolves links and placeholders to find physical endpoints.
* Exports the wiring diagram from QElectroTech as a CSV file.
*/
class WiringListExport : public QObject
{
@@ -64,8 +56,6 @@ private:
QMap<QString, ElementInfo> collectElementsInfo(const QDomElement &root) const;
QList<ConductorData> collectConductors(const QDomElement &root) const;
void resolveEndpoints(QList<ConductorData> &conductors, const QMap<QString, ElementInfo> &elementsInfo) const;
};
#endif // WIRINGLISTEXPORT_H