mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-06-08 11:13:15 +02:00
Compare commits
56 Commits
87f9f40e91
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c4711a8d0 | |||
| 8a8a338a2e | |||
| 407cc7a4c2 | |||
| 5cb8930732 | |||
| a24acfac24 | |||
| 8b0b1d10d4 | |||
| 57dfa28674 | |||
| 3848c7821a | |||
| 1572c23d51 | |||
| e234f063f8 | |||
| be21604ad0 | |||
| e1ccc1e568 | |||
| e202b5bc2b | |||
| 2b7e62f901 | |||
| 23e8258ae1 | |||
| 457d265f0a | |||
| cd76b6a1d6 | |||
| b522a94556 | |||
| 399bc0e897 | |||
| de9eeed542 | |||
| c071e92c58 | |||
| d22e4abf96 | |||
| 38b91e8083 | |||
| 19704cf5ca | |||
| 471d1f2538 | |||
| dc52105868 | |||
| 1b8dc5f410 | |||
| 26d5d019cc | |||
| 6dd7c2d926 | |||
| 47796e183a | |||
| ddf5ffcd89 | |||
| 7d718bb9a0 | |||
| d949e6eb8c | |||
| dbda958261 | |||
| d691489165 | |||
| 202ea38e40 | |||
| 86dafcb576 | |||
| f416c2a97e | |||
| 7426fedba3 | |||
| 027050c7e7 | |||
| fc948ad963 | |||
| 24d075b64c | |||
| f914f91e77 | |||
| fe03a0f643 | |||
| a7ad0278a6 | |||
| f517489421 | |||
| c8fa1c9fa4 | |||
| d85aff0c0f | |||
| 96b8e4b19c | |||
| 9760288db6 | |||
| eeaa059a77 | |||
| fa334d34a4 | |||
| 9664ff54ea | |||
| be0da461bd | |||
| 8c557a7f29 | |||
| 413e13a38c |
@@ -18,16 +18,19 @@ 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)
|
||||
# ----------------------------------------------------------------
|
||||
@@ -38,16 +41,12 @@ jobs:
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# 2. Download the portable artifact from the main build
|
||||
# Requires windows-build.yml to upload an artifact named
|
||||
# "qelectrotech-windows-portable" (fixed name)
|
||||
# ----------------------------------------------------------------
|
||||
- name: Download portable artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: qelectrotech-windows-portable
|
||||
path: artifact\files
|
||||
# workflow_run => use the triggering run's ID
|
||||
# workflow_dispatch => use input run_id if provided, otherwise current run
|
||||
run-id: ${{ github.event.workflow_run.id || github.event.inputs.run_id || github.run_id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: ${{ github.repository }}
|
||||
@@ -59,12 +58,10 @@ jobs:
|
||||
id: version
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Version from qetversion.cpp (same logic as windows-build.yml)
|
||||
$src = Get-Content "sources\qetversion.cpp" -Raw -ErrorAction SilentlyContinue
|
||||
if ($src -match 'return QVersionNumber\{([^}]+)\}') {
|
||||
$ver = $Matches[1] -replace '\s','' -replace ',','.'
|
||||
} else {
|
||||
# Fallback: CMakeLists.txt
|
||||
$cmake = Get-Content "CMakeLists.txt" -Raw
|
||||
if ($cmake -match 'project\s*\([^)]*VERSION\s+([\d]+\.[\d]+\.[\d]+)') {
|
||||
$ver = $Matches[1]
|
||||
@@ -72,43 +69,41 @@ jobs:
|
||||
$ver = "0.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
# Numeric MSI version: 4 digits required (e.g. 0.100.1.0)
|
||||
$verMsi = "$ver.0"
|
||||
|
||||
# Short SHA for the display version
|
||||
$sha = git rev-parse --short HEAD 2>$null
|
||||
if (-not $sha) { $sha = "unknown" }
|
||||
|
||||
# Cumulative revision number (same calculation as windows-build.yml)
|
||||
$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 "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
|
||||
# All done in one step: PATH is updated within the same step
|
||||
# so wix is immediately available for eula and extension commands
|
||||
# ----------------------------------------------------------------
|
||||
- name: Install WiX v7
|
||||
shell: pwsh
|
||||
run: |
|
||||
dotnet tool install --global wix --version 7.0.0
|
||||
# Update PATH immediately for the rest of this step
|
||||
$toolsPath = [System.IO.Path]::Combine($env:USERPROFILE, '.dotnet', 'tools')
|
||||
$env:PATH = "$toolsPath;$env:PATH"
|
||||
# Also export for subsequent steps
|
||||
echo $toolsPath >> $env:GITHUB_PATH
|
||||
# Accept OSMF EULA (official CI/CD method: writes a sentinel file)
|
||||
wix eula accept wix7
|
||||
# Install WixUI extension
|
||||
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."
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
@@ -120,8 +115,6 @@ jobs:
|
||||
$wxs = "build-aux\windows\QElectroTech.wxs"
|
||||
if (-not (Test-Path $wxs)) {
|
||||
Write-Error "WXS file not found: $wxs"
|
||||
Write-Host "Contents of build-aux\windows\ :"
|
||||
Get-ChildItem "build-aux\windows\" -ErrorAction SilentlyContinue
|
||||
exit 1
|
||||
}
|
||||
Write-Host "WXS found: $wxs"
|
||||
@@ -136,10 +129,8 @@ jobs:
|
||||
Get-ChildItem -Path "artifact\files" -Depth 2 |
|
||||
Select-Object FullName | Format-Table -AutoSize
|
||||
|
||||
# Search for qelectrotech.exe in the artifact
|
||||
$exe = Get-ChildItem -Path "artifact\files" -Filter "qelectrotech.exe" -Recurse | Select-Object -First 1
|
||||
if (-not $exe) {
|
||||
# Also try QElectroTech.exe (capital Q)
|
||||
$exe = Get-ChildItem -Path "artifact\files" -Filter "QElectroTech.exe" -Recurse | Select-Object -First 1
|
||||
}
|
||||
if (-not $exe) {
|
||||
@@ -147,69 +138,58 @@ jobs:
|
||||
exit 1
|
||||
}
|
||||
Write-Host "Executable: $($exe.FullName) ($([math]::Round($exe.Length/1MB,1)) MB)"
|
||||
|
||||
# FilesDir = folder containing bin\
|
||||
$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
|
||||
# RTF is the only format accepted by Windows Installer.
|
||||
# The conversion wraps plain text lines in basic RTF markup.
|
||||
# ----------------------------------------------------------------
|
||||
- name: Convert LICENSE to RTF
|
||||
shell: pwsh
|
||||
run: |
|
||||
$licSrc = "LICENSE"
|
||||
$licRtf = "$env:TEMP\License.rtf"
|
||||
|
||||
$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 header — Courier New, 9pt, black
|
||||
$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) {
|
||||
# Escape RTF special characters
|
||||
$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. Replace Lancer QET.bat with the MSI-specific version
|
||||
# The portable version uses relative paths suited for the zip.
|
||||
# The MSI version uses %~dp0 to resolve paths relative to
|
||||
# the installation directory in Program Files.
|
||||
# 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: Replace Lancer QET.bat for MSI
|
||||
- name: Remove Lancer QET.bat from artifact
|
||||
shell: pwsh
|
||||
run: |
|
||||
$bat = "$env:FILES_DIR\Lancer QET.bat"
|
||||
$content = "@echo off`r`nstart `"`" `"%~dp0bin\qelectrotech.exe`" --common-elements-dir=`"%~dp0elements/`" --common-tbt-dir=`"%~dp0titleblocks/`" --lang-dir=`"%~dp0lang/`" -style windowsvista`r`n"
|
||||
[System.IO.File]::WriteAllText($bat, $content, [System.Text.Encoding]::ASCII)
|
||||
Write-Host "Lancer QET.bat replaced for MSI installation."
|
||||
Write-Host "=== Content of new Lancer QET.bat ==="
|
||||
Get-Content $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
|
||||
@@ -227,32 +207,63 @@ jobs:
|
||||
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"
|
||||
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. Upload the MSI artifact
|
||||
# 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
|
||||
@@ -272,7 +283,7 @@ jobs:
|
||||
run: |
|
||||
gh release view nightly --repo "$REPO" --json assets \
|
||||
--jq '.assets[] | select(.name | test("\\.msi$")) | .name' \
|
||||
| while read -r name; do
|
||||
| while read -r name; do
|
||||
echo "Deleting old asset: $name"
|
||||
gh release delete-asset nightly "$name" --repo "$REPO" --yes
|
||||
done
|
||||
@@ -308,7 +319,6 @@ jobs:
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# 13. Generate and deploy the GitHub Pages download page
|
||||
# Toutes les URLs sont connues ici (exe, zip, msi).
|
||||
# ----------------------------------------------------------------
|
||||
- name: Checkout (for generate-page.py)
|
||||
uses: actions/checkout@v4
|
||||
@@ -325,29 +335,22 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
REPO="${{ github.repository }}"
|
||||
|
||||
# Fetch asset names from the nightly release (source of truth)
|
||||
ASSETS=$(gh release view nightly --repo "$REPO" --json assets --jq '.assets[].name')
|
||||
|
||||
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 "")
|
||||
|
||||
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
|
||||
|
||||
+101
-87
@@ -16,13 +16,13 @@
|
||||
|
||||
include(cmake/hoto_update_cmake_message.cmake)
|
||||
|
||||
cmake_minimum_required(VERSION 3.14...3.19 FATAL_ERROR)
|
||||
cmake_minimum_required(VERSION 3.5...4.2)
|
||||
|
||||
project(qelectrotech
|
||||
VERSION 0.9.0
|
||||
DESCRIPTION "QET is a CAD/CAE editor focusing on schematics drawing features."
|
||||
HOMEPAGE_URL "https://qelectrotech.org/"
|
||||
LANGUAGES CXX)
|
||||
VERSION 0.100.1
|
||||
DESCRIPTION "QET is a CAD/CAE editor focusing on schematics drawing features."
|
||||
HOMEPAGE_URL "https://qelectrotech.org/"
|
||||
LANGUAGES CXX)
|
||||
|
||||
include(cmake/copyright_message.cmake)
|
||||
|
||||
@@ -31,8 +31,8 @@ set(QET_DIR ${PROJECT_SOURCE_DIR})
|
||||
# Add sub directories
|
||||
option(PACKAGE_TESTS "Build the tests" ON)
|
||||
if(PACKAGE_TESTS)
|
||||
message("Add sub directory tests")
|
||||
add_subdirectory(tests)
|
||||
message("Add sub directory tests")
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
include(cmake/paths_compilation_installation.cmake)
|
||||
@@ -46,31 +46,31 @@ 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)
|
||||
|
||||
find_package(
|
||||
QT
|
||||
NAMES
|
||||
Qt5
|
||||
COMPONENTS
|
||||
${QET_COMPONENTS}
|
||||
REQUIRED
|
||||
)
|
||||
QT
|
||||
NAMES
|
||||
Qt5
|
||||
COMPONENTS
|
||||
${QET_COMPONENTS}
|
||||
REQUIRED
|
||||
)
|
||||
|
||||
find_package(
|
||||
Qt${QT_VERSION_MAJOR}
|
||||
COMPONENTS
|
||||
${QET_COMPONENTS}
|
||||
REQUIRED)
|
||||
Qt${QT_VERSION_MAJOR}
|
||||
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,78 +78,92 @@ qt5_add_translation(QM_FILES ${TS_FILES})
|
||||
# als laatse
|
||||
include(cmake/define_definitions.cmake)
|
||||
|
||||
add_executable(
|
||||
${PROJECT_NAME}
|
||||
${QET_RES_FILES}
|
||||
${QET_SRC_FILES}
|
||||
${QM_FILES}
|
||||
${QET_DIR}/qelectrotech.qrc
|
||||
)
|
||||
# On Windows, WIN32 sets /SUBSYSTEM:WINDOWS to suppress the console window.
|
||||
# Qt automatically links qtmain.lib which provides the WinMain entry point,
|
||||
# so no source code change is needed.
|
||||
if(WIN32)
|
||||
add_executable(
|
||||
${PROJECT_NAME}
|
||||
WIN32
|
||||
${QET_RES_FILES}
|
||||
${QET_SRC_FILES}
|
||||
${QM_FILES}
|
||||
${QET_DIR}/qelectrotech.qrc
|
||||
)
|
||||
else()
|
||||
add_executable(
|
||||
${PROJECT_NAME}
|
||||
${QET_RES_FILES}
|
||||
${QET_SRC_FILES}
|
||||
${QM_FILES}
|
||||
${QET_DIR}/qelectrotech.qrc
|
||||
)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
${PROJECT_NAME}
|
||||
PUBLIC
|
||||
PRIVATE
|
||||
pugixml::pugixml
|
||||
SingleApplication::SingleApplication
|
||||
SQLite::SQLite3
|
||||
${KF5_PRIVATE_LIBRARIES}
|
||||
${QET_PRIVATE_LIBRARIES}
|
||||
)
|
||||
${PROJECT_NAME}
|
||||
PUBLIC
|
||||
PRIVATE
|
||||
pugixml::pugixml
|
||||
SingleApplication::SingleApplication
|
||||
SQLite::SQLite3
|
||||
${KF5_PRIVATE_LIBRARIES}
|
||||
${QET_PRIVATE_LIBRARIES}
|
||||
)
|
||||
|
||||
target_include_directories(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${QET_DIR}/sources/titleblock
|
||||
${QET_DIR}/sources/ui
|
||||
${QET_DIR}/sources/qetgraphicsitem
|
||||
${QET_DIR}/sources/qetgraphicsitem/ViewItem
|
||||
${QET_DIR}/sources/qetgraphicsitem/ViewItem/ui
|
||||
${QET_DIR}/sources/richtext
|
||||
${QET_DIR}/sources/factory
|
||||
${QET_DIR}/sources/properties
|
||||
${QET_DIR}/sources/dvevent
|
||||
${QET_DIR}/sources/editor
|
||||
${QET_DIR}/sources/editor/esevent
|
||||
${QET_DIR}/sources/editor/graphicspart
|
||||
${QET_DIR}/sources/editor/ui
|
||||
${QET_DIR}/sources/editor/UndoCommand
|
||||
${QET_DIR}/sources/undocommand
|
||||
${QET_DIR}/sources/diagramevent
|
||||
${QET_DIR}/sources/ElementsCollection
|
||||
${QET_DIR}/sources/ElementsCollection/ui
|
||||
${QET_DIR}/sources/autoNum
|
||||
${QET_DIR}/sources/autoNum/ui
|
||||
${QET_DIR}/sources/ui/configpage
|
||||
${QET_DIR}/sources/SearchAndReplace
|
||||
${QET_DIR}/sources/SearchAndReplace/ui
|
||||
${QET_DIR}/sources/NameList
|
||||
${QET_DIR}/sources/NameList/ui
|
||||
${QET_DIR}/sources/utils
|
||||
${QET_DIR}/pugixml/src
|
||||
${QET_DIR}/sources/dataBase
|
||||
${QET_DIR}/sources/dataBase/ui
|
||||
${QET_DIR}/sources/factory/ui
|
||||
${QET_DIR}/sources/print
|
||||
)
|
||||
${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${QET_DIR}/sources/titleblock
|
||||
${QET_DIR}/sources/ui
|
||||
${QET_DIR}/sources/qetgraphicsitem
|
||||
${QET_DIR}/sources/qetgraphicsitem/ViewItem
|
||||
${QET_DIR}/sources/qetgraphicsitem/ViewItem/ui
|
||||
${QET_DIR}/sources/richtext
|
||||
${QET_DIR}/sources/factory
|
||||
${QET_DIR}/sources/properties
|
||||
${QET_DIR}/sources/dvevent
|
||||
${QET_DIR}/sources/editor
|
||||
${QET_DIR}/sources/editor/esevent
|
||||
${QET_DIR}/sources/editor/graphicspart
|
||||
${QET_DIR}/sources/editor/ui
|
||||
${QET_DIR}/sources/editor/UndoCommand
|
||||
${QET_DIR}/sources/undocommand
|
||||
${QET_DIR}/sources/diagramevent
|
||||
${QET_DIR}/sources/ElementsCollection
|
||||
${QET_DIR}/sources/ElementsCollection/ui
|
||||
${QET_DIR}/sources/autoNum
|
||||
${QET_DIR}/sources/autoNum/ui
|
||||
${QET_DIR}/sources/ui/configpage
|
||||
${QET_DIR}/sources/SearchAndReplace
|
||||
${QET_DIR}/sources/SearchAndReplace/ui
|
||||
${QET_DIR}/sources/NameList
|
||||
${QET_DIR}/sources/NameList/ui
|
||||
${QET_DIR}/sources/utils
|
||||
${QET_DIR}/pugixml/src
|
||||
${QET_DIR}/sources/dataBase
|
||||
${QET_DIR}/sources/dataBase/ui
|
||||
${QET_DIR}/sources/factory/ui
|
||||
${QET_DIR}/sources/print
|
||||
${QET_DIR}/sources/svg
|
||||
)
|
||||
|
||||
install(TARGETS ${PROJECT_NAME})
|
||||
|
||||
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})
|
||||
install(DIRECTORY ico/breeze-icons/48x48 DESTINATION ${QET_ICONS_PATH})
|
||||
install(DIRECTORY ico/breeze-icons/64x64 DESTINATION ${QET_ICONS_PATH})
|
||||
install(DIRECTORY ico/breeze-icons/128x128 DESTINATION ${QET_ICONS_PATH})
|
||||
install(DIRECTORY ico/breeze-icons/256x256 DESTINATION ${QET_ICONS_PATH})
|
||||
install(DIRECTORY elements DESTINATION share/qelectrotech)
|
||||
install(DIRECTORY examples DESTINATION share/qelectrotech)
|
||||
install(DIRECTORY titleblocks DESTINATION share/qelectrotech)
|
||||
install(FILES LICENSE ELEMENTS.LICENSE CREDIT README ChangeLog DESTINATION share/doc/qelectrotech)
|
||||
install(FILES misc/org.qelectrotech.qelectrotech.desktop DESTINATION share/applications)
|
||||
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})
|
||||
|
||||
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})
|
||||
install(DIRECTORY ico/breeze-icons/48x48 DESTINATION ${QET_ICONS_PATH})
|
||||
install(DIRECTORY ico/breeze-icons/64x64 DESTINATION ${QET_ICONS_PATH})
|
||||
install(DIRECTORY ico/breeze-icons/128x128 DESTINATION ${QET_ICONS_PATH})
|
||||
install(DIRECTORY ico/breeze-icons/256x256 DESTINATION ${QET_ICONS_PATH})
|
||||
install(DIRECTORY elements DESTINATION share/qelectrotech)
|
||||
install(DIRECTORY examples DESTINATION share/qelectrotech)
|
||||
install(DIRECTORY titleblocks DESTINATION share/qelectrotech)
|
||||
install(FILES LICENSE ELEMENTS.LICENSE CREDIT README ChangeLog DESTINATION share/doc/qelectrotech)
|
||||
install(FILES misc/org.qelectrotech.qelectrotech.desktop DESTINATION share/applications)
|
||||
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()
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
Name="QElectroTech"
|
||||
Manufacturer="QElectroTech Team"
|
||||
Version="$(var.Version)"
|
||||
ProductCode="$(var.ProductCode)"
|
||||
UpgradeCode="A1B2C3D4-E5F6-7890-ABCD-EF1234567890"
|
||||
Language="1033"
|
||||
Codepage="1252"
|
||||
@@ -28,6 +29,7 @@
|
||||
<!-- In-place upgrade: automatically uninstalls the previous version -->
|
||||
<MajorUpgrade
|
||||
DowngradeErrorMessage="A newer version of QElectroTech is already installed."
|
||||
AllowSameVersionUpgrades="yes"
|
||||
Schedule="afterInstallInitialize" />
|
||||
|
||||
<!-- Installation directory -->
|
||||
@@ -37,20 +39,26 @@
|
||||
|
||||
<!-- ============================================================
|
||||
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]Lancer QET.bat"
|
||||
Target="[INSTALLDIR]bin\qelectrotech.exe"
|
||||
Arguments="--common-elements-dir="[INSTALLDIR]elements/" --common-tbt-dir="[INSTALLDIR]titleblocks/" --lang-dir="[INSTALLDIR]lang/" -style windowsvista"
|
||||
Icon="qet.ico"
|
||||
WorkingDirectory="INSTALLDIR" />
|
||||
<RegistryValue Root="HKCU"
|
||||
@@ -59,11 +67,13 @@
|
||||
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]Lancer QET.bat"
|
||||
Target="[INSTALLDIR]bin\qelectrotech.exe"
|
||||
Arguments="--common-elements-dir="[INSTALLDIR]elements/" --common-tbt-dir="[INSTALLDIR]titleblocks/" --lang-dir="[INSTALLDIR]lang/" -style windowsvista"
|
||||
Icon="qet.ico"
|
||||
WorkingDirectory="INSTALLDIR" />
|
||||
<RegistryValue Root="HKCU"
|
||||
@@ -72,11 +82,12 @@
|
||||
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" />
|
||||
@@ -90,12 +101,44 @@
|
||||
|
||||
<!-- ============================================================
|
||||
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 "Get-ChildItem -LiteralPath '[INSTALLDIR]elements' -Recurse -File | ForEach-Object { `$_.IsReadOnly = `$true }"" />
|
||||
|
||||
<!-- 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" />
|
||||
@@ -110,9 +153,9 @@
|
||||
|
||||
<!-- Properties shown in Programs and Features -->
|
||||
<Property Id="ARPPRODUCTICON" Value="qet.ico" />
|
||||
<Property Id="ARPHELPLINK" Value="https://qelectrotech.org" />
|
||||
<Property Id="ARPHELPLINK" Value="https://qelectrotech.org" />
|
||||
<Property Id="ARPURLINFOABOUT" Value="https://qelectrotech.org" />
|
||||
<Property Id="ARPCOMMENTS" Value="Free electrical schematic editor" />
|
||||
<Property Id="ARPCOMMENTS" Value="Free electrical schematic editor" />
|
||||
|
||||
</Package>
|
||||
</Wix>
|
||||
|
||||
@@ -25,7 +25,7 @@ if(BUILD_PUGIXML)
|
||||
FetchContent_Declare(
|
||||
pugixml
|
||||
GIT_REPOSITORY https://github.com/zeux/pugixml.git
|
||||
GIT_TAG v1.11.4)
|
||||
GIT_TAG v1.15)
|
||||
|
||||
FetchContent_MakeAvailable(pugixml)
|
||||
else()
|
||||
|
||||
@@ -29,6 +29,7 @@ set(QET_COMPONENTS
|
||||
set(QET_PRIVATE_LIBRARIES
|
||||
Qt::PrintSupport
|
||||
Qt::Gui
|
||||
Qt::GuiPrivate # Required for QPdfEngine::drawHyperlink (PDF internal links)
|
||||
Qt::Xml
|
||||
Qt::Svg
|
||||
Qt::Sql
|
||||
@@ -499,6 +500,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/SearchAndReplace/ui/replacefoliowidget.h
|
||||
${QET_DIR}/sources/SearchAndReplace/ui/searchandreplacewidget.cpp
|
||||
${QET_DIR}/sources/SearchAndReplace/ui/searchandreplacewidget.h
|
||||
${QET_DIR}/sources/svg/qetsvg.cpp
|
||||
${QET_DIR}/sources/svg/qetsvg.h
|
||||
|
||||
${QET_DIR}/sources/titleblock/dimension.cpp
|
||||
${QET_DIR}/sources/titleblock/dimension.h
|
||||
@@ -713,6 +716,8 @@ set(QET_SRC_FILES
|
||||
|
||||
${QET_DIR}/sources/xml/terminalstripitemxml.cpp
|
||||
${QET_DIR}/sources/xml/terminalstripitemxml.h
|
||||
${QET_DIR}/sources/xml/terminalstriplayoutpatternxml.cpp
|
||||
${QET_DIR}/sources/xml/terminalstriplayoutpatternxml.h
|
||||
)
|
||||
|
||||
set(TS_FILES
|
||||
|
||||
+1
-1
Submodule elements updated: 9fd938d36c...3aab395fc4
+509
-443
File diff suppressed because it is too large
Load Diff
+509
-443
File diff suppressed because it is too large
Load Diff
+509
-443
File diff suppressed because it is too large
Load Diff
+509
-444
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+510
-444
File diff suppressed because it is too large
Load Diff
+509
-443
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+509
-443
File diff suppressed because it is too large
Load Diff
+509
-443
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+508
-442
File diff suppressed because it is too large
Load Diff
+508
-442
File diff suppressed because it is too large
Load Diff
+506
-440
File diff suppressed because it is too large
Load Diff
+510
-444
File diff suppressed because it is too large
Load Diff
+506
-440
File diff suppressed because it is too large
Load Diff
+509
-443
File diff suppressed because it is too large
Load Diff
+505
-439
File diff suppressed because it is too large
Load Diff
+506
-440
File diff suppressed because it is too large
Load Diff
+509
-443
File diff suppressed because it is too large
Load Diff
+508
-442
File diff suppressed because it is too large
Load Diff
+509
-443
File diff suppressed because it is too large
Load Diff
+512
-446
File diff suppressed because it is too large
Load Diff
+506
-440
File diff suppressed because it is too large
Load Diff
+512
-446
File diff suppressed because it is too large
Load Diff
+508
-442
File diff suppressed because it is too large
Load Diff
+509
-443
File diff suppressed because it is too large
Load Diff
+508
-442
File diff suppressed because it is too large
Load Diff
+508
-442
File diff suppressed because it is too large
Load Diff
+508
-442
File diff suppressed because it is too large
Load Diff
+506
-440
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+1311
-1202
File diff suppressed because it is too large
Load Diff
+506
-440
File diff suppressed because it is too large
Load Diff
+506
-440
File diff suppressed because it is too large
Load Diff
+5
-1
@@ -230,7 +230,11 @@ RESOURCES += qelectrotech.qrc
|
||||
TRANSLATIONS += lang/*.ts
|
||||
|
||||
# Modules Qt utilises par l'application
|
||||
QT += xml svg network sql widgets printsupport concurrent KWidgetsAddons KCoreAddons
|
||||
QT += xml svg network sql widgets printsupport concurrent KWidgetsAddons KCoreAddons gui-private
|
||||
|
||||
# Private Qt GUI headers (needed for QPdfEngine::drawHyperlink)
|
||||
# gui-private should add this automatically, but some distros need it explicit
|
||||
INCLUDEPATH += $$[QT_INSTALL_HEADERS]/QtGui/$$[QT_VERSION]/QtGui
|
||||
|
||||
# UI DESIGNER FILES AND GENERATION SOURCES FILES
|
||||
FORMS += $$files(sources/richtext/*.ui) \
|
||||
|
||||
@@ -383,7 +383,7 @@ void projectDataBase::createElementNomenclatureView()
|
||||
"ei.supplier_auxiliary4 AS supplier_auxiliary4,"
|
||||
"ei.quantity_auxiliary4 AS quantity_auxiliary4,"
|
||||
"ei.unity_auxiliary4 AS unity_auxiliary4,"
|
||||
|
||||
"ei.exclude_from_bom AS exclude_from_bom,"
|
||||
|
||||
"d.pos AS diagram_position,"
|
||||
"e.type AS element_type,"
|
||||
@@ -392,7 +392,7 @@ void projectDataBase::createElementNomenclatureView()
|
||||
"di.folio AS folio,"
|
||||
"e.pos AS position "
|
||||
" FROM element_info ei, diagram_info di, element e, diagram d"
|
||||
" WHERE ei.element_uuid = e.uuid AND e.diagram_uuid = d.uuid AND di.diagram_uuid = d.uuid");
|
||||
" WHERE ei.element_uuid = e.uuid AND e.diagram_uuid = d.uuid AND di.diagram_uuid = d.uuid AND (ei.exclude_from_bom IS NOT 'true')");
|
||||
|
||||
QSqlQuery query(m_data_base);
|
||||
if (!query.exec(create_view)) {
|
||||
|
||||
@@ -373,6 +373,11 @@ QString ElementQueryWidget::queryStr() const
|
||||
where.clear();
|
||||
}
|
||||
|
||||
QString exclude_condition = "(exclude_from_bom IS NULL OR exclude_from_bom != '1')";
|
||||
|
||||
filter_ += " AND " + exclude_condition;
|
||||
// -------------------------------------------------------------
|
||||
|
||||
if (where.isEmpty() && !filter_.isEmpty()) {
|
||||
filter_.remove(0, 4); //Remove the first " AND" of filter.
|
||||
filter_.prepend( " WHERE");
|
||||
@@ -456,7 +461,7 @@ void ElementQueryWidget::setUpItems()
|
||||
{
|
||||
for(QString key : QETInformation::elementInfoKeys())
|
||||
{
|
||||
if (key == "formula")
|
||||
if (key == "formula" || key == "exclude_from_bom")
|
||||
continue;
|
||||
|
||||
auto item = new QListWidgetItem(QETInformation::translatedInfoKey(key), ui->m_var_list);
|
||||
|
||||
+3
-2
@@ -142,10 +142,11 @@ class Diagram : public QGraphicsScene
|
||||
void wheelEvent (QGraphicsSceneWheelEvent *event) override;
|
||||
void keyPressEvent (QKeyEvent *event) override;
|
||||
void keyReleaseEvent (QKeyEvent *) override;
|
||||
void correctTextPos(Element* elmt);
|
||||
void restoreText(Element* elmt);
|
||||
|
||||
|
||||
public:
|
||||
void correctTextPos(Element* elmt);
|
||||
void restoreText(Element* elmt);
|
||||
QUuid uuid();
|
||||
void setEventInterface (DiagramEventInterface *event_interface);
|
||||
void clearEventInterface();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -736,40 +736,62 @@ bool QETElementEditor::checkElement()
|
||||
QList<QETWarning> warnings;
|
||||
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)
|
||||
// Warning #1: Element haven't got terminal
|
||||
// (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(
|
||||
"<br>En l'absence de borne, l'élément ne pourra être"
|
||||
" relié à d'autres éléments par l'intermédiaire de conducteurs.",
|
||||
"warning description"
|
||||
)
|
||||
);
|
||||
}
|
||||
tr("Absence de borne", "warning title"),
|
||||
tr(
|
||||
"<br>En l'absence de borne, l'élément ne pourra être"
|
||||
" relié à d'autres éléments par l'intermédiaire de conducteurs.",
|
||||
"warning description"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check folio report element
|
||||
if (m_elmt_scene->elementData().m_type & ElementData::AllReport)
|
||||
{
|
||||
int terminal =0;
|
||||
if (m_elmt_scene->elementData().m_type & ElementData::AllReport)
|
||||
{
|
||||
int terminal =0;
|
||||
|
||||
for(auto qgi : m_elmt_scene -> items()) {
|
||||
if (qgraphicsitem_cast<PartTerminal *>(qgi)) {
|
||||
terminal ++;
|
||||
for(auto qgi : m_elmt_scene -> items()) {
|
||||
if (qgraphicsitem_cast<PartTerminal *>(qgi)) {
|
||||
terminal ++;
|
||||
}
|
||||
}
|
||||
|
||||
//Error folio report must have only one terminal
|
||||
if (terminal != 1) {
|
||||
errors << qMakePair (tr("Absence de borne"),
|
||||
tr("<br><b>Erreur</b> :"
|
||||
"<br>Les reports de folio doivent posséder une seul borne."
|
||||
"<br><b>Solution</b> :"
|
||||
"<br>Verifier que l'élément ne possède qu'une seul borne"));
|
||||
}
|
||||
}
|
||||
|
||||
//Error folio report must have only one terminal
|
||||
if (terminal != 1) {
|
||||
errors << qMakePair (tr("Absence de borne"),
|
||||
tr("<br><b>Erreur</b> :"
|
||||
"<br>Les reports de folio doivent posséder une seul borne."
|
||||
"<br><b>Solution</b> :"
|
||||
"<br>Verifier que l'élément ne possède qu'une seul borne"));
|
||||
// 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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "elementspanelwidget.h"
|
||||
|
||||
#include "diagram.h"
|
||||
#include "editor/ui/qetelementeditor.h"
|
||||
#include "elementscategoryeditor.h"
|
||||
@@ -26,6 +25,7 @@
|
||||
#include "titleblock/templatedeleter.h"
|
||||
#include <QFileInfo>
|
||||
#include <QMessageBox>
|
||||
#include "qetgraphicsitem/element.h"
|
||||
|
||||
/*
|
||||
When the ENABLE_PANEL_WIDGET_DND_CHECKS flag is set, the panel
|
||||
@@ -59,7 +59,8 @@ ElementsPanelWidget::ElementsPanelWidget(QWidget *parent) : QWidget(parent) {
|
||||
prj_close = new QAction(QET::Icons::DocumentClose, tr("Fermer ce projet"), this);
|
||||
prj_edit_prop = new QAction(QET::Icons::DialogInformation, tr("Propriétés du projet"), this);
|
||||
prj_prop_diagram = new QAction(QET::Icons::DialogInformation, tr("Propriétés du folio"), this);
|
||||
prj_add_diagram = new QAction(QET::Icons::DiagramAdd, tr("Ajouter un folio"), this);
|
||||
prj_add_diagram = new QAction(QET::Icons::DiagramAdd, tr("Ajouter un folio"), this);
|
||||
prj_duplicate_diagram = new QAction(QET::Icons::IC_CopyFile, tr("Copier et coller"), this);
|
||||
prj_del_diagram = new QAction(QET::Icons::DiagramDelete, tr("Supprimer ce folio"), this);
|
||||
prj_move_diagram_up = new QAction(QET::Icons::GoUp, tr("Remonter ce folio"), this);
|
||||
prj_move_diagram_down = new QAction(QET::Icons::GoDown, tr("Abaisser ce folio"), this);
|
||||
@@ -100,6 +101,7 @@ ElementsPanelWidget::ElementsPanelWidget(QWidget *parent) : QWidget(parent) {
|
||||
connect(prj_prop_diagram, SIGNAL(triggered()), this, SLOT(editDiagramProperties()));
|
||||
connect(prj_add_diagram, SIGNAL(triggered()), this, SLOT(newDiagram()));
|
||||
connect(prj_del_diagram, SIGNAL(triggered()), this, SLOT(deleteDiagram()));
|
||||
connect(prj_duplicate_diagram, SIGNAL(triggered()), this, SLOT(duplicateDiagram()));
|
||||
connect(prj_move_diagram_up, SIGNAL(triggered()), this, SLOT(moveDiagramUp()));
|
||||
connect(prj_move_diagram_down, SIGNAL(triggered()), this, SLOT(moveDiagramDown()));
|
||||
connect(prj_move_diagram_top, SIGNAL(triggered()), this, SLOT(moveDiagramUpTop()));
|
||||
@@ -447,7 +449,8 @@ void ElementsPanelWidget::updateButtons()
|
||||
}
|
||||
|
||||
prj_del_diagram -> setEnabled(is_writable);
|
||||
prj_move_diagram_up -> setEnabled(is_writable && min_position > 0);
|
||||
prj_duplicate_diagram -> setEnabled(is_writable);
|
||||
prj_move_diagram_up -> setEnabled(is_writable && min_position > 0);
|
||||
prj_move_diagram_down -> setEnabled(is_writable && max_position < project_diagrams_count - 1);
|
||||
prj_move_diagram_top -> setEnabled(is_writable && min_position > 0);
|
||||
|
||||
@@ -501,6 +504,7 @@ void ElementsPanelWidget::handleContextMenu(const QPoint &pos) {
|
||||
case QET::Diagram:
|
||||
context_menu -> addAction(prj_prop_diagram);
|
||||
context_menu -> addAction(prj_del_diagram);
|
||||
context_menu -> addAction(prj_duplicate_diagram);
|
||||
context_menu -> addAction(prj_move_diagram_top);
|
||||
context_menu -> addAction(prj_move_diagram_upx10);
|
||||
context_menu -> addAction(prj_move_diagram_upx100);
|
||||
@@ -593,3 +597,56 @@ void ElementsPanelWidget::keyPressEvent(QKeyEvent *e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicates the selected folios (pages) along with their content
|
||||
* and properties, and cleanly resolves cross-references.
|
||||
*/
|
||||
void ElementsPanelWidget::duplicateDiagram()
|
||||
{
|
||||
QList<Diagram *> diagrams_to_duplicate = elements_panel->selectedDiagrams();
|
||||
if (diagrams_to_duplicate.isEmpty()) return;
|
||||
|
||||
QETProject *project = diagrams_to_duplicate.first()->project();
|
||||
if (!project || project->isReadOnly()) return;
|
||||
|
||||
for (Diagram *source_diagram : diagrams_to_duplicate) {
|
||||
|
||||
Diagram *new_diagram = project->addNewDiagram();
|
||||
if (!new_diagram) continue;
|
||||
|
||||
QString template_name = source_diagram->border_and_titleblock.titleBlockTemplateName();
|
||||
new_diagram->setTitleBlockTemplate(template_name);
|
||||
|
||||
TitleBlockProperties tbp = source_diagram->border_and_titleblock.exportTitleBlock();
|
||||
new_diagram->border_and_titleblock.importTitleBlock(tbp);
|
||||
|
||||
BorderProperties bp = source_diagram->border_and_titleblock.exportBorder();
|
||||
new_diagram->border_and_titleblock.importBorder(bp);
|
||||
|
||||
for (QGraphicsItem *item : source_diagram->items()) {
|
||||
if (Element *elmt = dynamic_cast<Element *>(item)) {
|
||||
source_diagram->correctTextPos(elmt);
|
||||
}
|
||||
}
|
||||
|
||||
QDomDocument doc = source_diagram->toXml();
|
||||
QDomElement diagram_elmt = doc.documentElement();
|
||||
|
||||
for (QGraphicsItem *item : source_diagram->items()) {
|
||||
if (Element *elmt = dynamic_cast<Element *>(item)) {
|
||||
source_diagram->restoreText(elmt);
|
||||
}
|
||||
}
|
||||
|
||||
new_diagram->fromXml(diagram_elmt, QPointF(0, 0), false, nullptr);
|
||||
|
||||
for (QGraphicsItem *item : new_diagram->items()) {
|
||||
if (Element *elmt = dynamic_cast<Element *>(item)) {
|
||||
new_diagram->restoreText(elmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
elements_panel->reload();
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ class ElementsPanelWidget : public QWidget {
|
||||
*prj_prop_diagram,
|
||||
*prj_add_diagram,
|
||||
*prj_del_diagram,
|
||||
*prj_duplicate_diagram,
|
||||
*prj_move_diagram_up,
|
||||
*prj_move_diagram_top,
|
||||
*prj_move_diagram_down,
|
||||
@@ -88,6 +89,7 @@ class ElementsPanelWidget : public QWidget {
|
||||
void editDiagramProperties();
|
||||
void newDiagram();
|
||||
void deleteDiagram();
|
||||
void duplicateDiagram();
|
||||
void moveDiagramUp();
|
||||
void moveDiagramDown();
|
||||
void moveDiagramUpTop();
|
||||
|
||||
@@ -21,9 +21,16 @@
|
||||
#include "../qeticons.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../qetversion.h"
|
||||
#include "../qetgraphicsitem/crossrefitem.h"
|
||||
#include "../qetgraphicsitem/dynamicelementtextitem.h"
|
||||
#include "../qetgraphicsitem/elementtextitemgroup.h"
|
||||
|
||||
#include "ui_projectprintwindow.h"
|
||||
|
||||
// Private Qt PDF engine for drawHyperlink() — not public API, stable since Qt4
|
||||
// Requires QT += gui-private in qelectrotech.pro
|
||||
#include <private/qpdf_p.h>
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
|
||||
# include <QDesktopWidget>
|
||||
#else
|
||||
@@ -37,6 +44,11 @@
|
||||
#include <QPrintDialog>
|
||||
#include <QPrintPreviewWidget>
|
||||
#include <QScreen>
|
||||
#include <QFile>
|
||||
#include <QRegularExpression>
|
||||
#include <QMap>
|
||||
#include <QTimer>
|
||||
#include <QVector>
|
||||
|
||||
/**
|
||||
* @brief ProjectPrintWindow::ProjectPrintWindow
|
||||
@@ -188,6 +200,241 @@ ProjectPrintWindow::~ProjectPrintWindow()
|
||||
* @brief ProjectPrintWindow::requestPaint
|
||||
* @param slot called when m_preview emit paintRequested
|
||||
*/
|
||||
/**
|
||||
* @brief ProjectPrintWindow::pdfConvertUriToGoTo
|
||||
* Post-processes a Qt-generated PDF to replace URI link annotations
|
||||
* (file:///path/to/file.pdf#page=N) with native PDF GoTo actions
|
||||
* ([pageObj 0 R /Fit]). This makes cross-reference links work in all
|
||||
* PDF viewers regardless of where the file is stored.
|
||||
*
|
||||
* The function:
|
||||
* 1. Reads the PDF as raw bytes.
|
||||
* 2. Collects page object numbers in document order by scanning for
|
||||
* objects that contain "/Type /Page" (but not "/Type /Pages").
|
||||
* 3. Replaces every annotation action block
|
||||
* /S /URI\n/URI (file://...#page=N)
|
||||
* with
|
||||
* /S /GoTo\n/D [<pageObj> 0 R /Fit]
|
||||
* 4. Rebuilds the cross-reference table (offsets change because the
|
||||
* replacement strings have different lengths).
|
||||
* 5. Writes the result back to the same file.
|
||||
*
|
||||
* The function is intentionally conservative: if any step fails (file
|
||||
* not found, malformed PDF, no URI annotations) it returns silently
|
||||
* without corrupting the file.
|
||||
*/
|
||||
static void pdfConvertUriToGoTo(const QString &pdfPath)
|
||||
{
|
||||
// --- 1. Read raw bytes ---
|
||||
QFile f(pdfPath);
|
||||
if (!f.open(QIODevice::ReadOnly)) return;
|
||||
QByteArray data = f.readAll();
|
||||
f.close();
|
||||
|
||||
// --- 2. Collect page object numbers in document order ---
|
||||
// Read them from the page tree (/Type /Pages -> /Kids [ N 0 R ... ]).
|
||||
// This is reliable; scanning raw bytes for "/Type /Page" is NOT: that
|
||||
// marker also occurs inside content streams, and a forward lookahead
|
||||
// wrongly tags neighbouring objects (it found 280 "pages" for a 137-page
|
||||
// document). Qt writes a single, flat /Kids array listing every page.
|
||||
QVector<int> pageObjs;
|
||||
{
|
||||
int pagesPos = data.indexOf("/Type /Pages");
|
||||
int kidsPos = (pagesPos == -1) ? -1 : data.indexOf("/Kids", pagesPos);
|
||||
int lb = (kidsPos == -1) ? -1 : data.indexOf('[', kidsPos);
|
||||
int rb = (lb == -1) ? -1 : data.indexOf(']', lb);
|
||||
if (lb != -1 && rb != -1 && rb > lb) {
|
||||
const QString kids =
|
||||
QString::fromLatin1(data.mid(lb + 1, rb - lb - 1));
|
||||
QRegularExpression re(QStringLiteral("(\\d+)\\s+\\d+\\s+R"));
|
||||
auto it = re.globalMatch(kids);
|
||||
while (it.hasNext()) {
|
||||
int objNum = it.next().captured(1).toInt();
|
||||
if (objNum > 0) pageObjs.append(objNum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pageObjs.isEmpty()) return; // nothing to do
|
||||
|
||||
// --- 3. Replace URI annotations with GoTo ---
|
||||
// Pattern (Qt always writes exactly this):
|
||||
// /S /URI\n/URI (file:///...<anything>#page=N)\n
|
||||
// or (older patches without file://):
|
||||
// /S /URI\n/URI (page=N)\n
|
||||
bool changed = false;
|
||||
{
|
||||
// We do a manual scan to handle variable-length replacements.
|
||||
QByteArray out;
|
||||
out.reserve(data.size());
|
||||
|
||||
const QByteArray sUri = "/S /URI\n/URI (";
|
||||
const QByteArray sGoTo = "/S /GoTo\n/D [";
|
||||
int pos = 0;
|
||||
|
||||
while (pos < data.size()) {
|
||||
int found = data.indexOf(sUri, pos);
|
||||
if (found == -1) {
|
||||
out.append(data.mid(pos));
|
||||
break;
|
||||
}
|
||||
|
||||
// Copy everything up to the match
|
||||
out.append(data.mid(pos, found - pos));
|
||||
|
||||
// Find closing ')' of the URI value
|
||||
int uriStart = found + sUri.size();
|
||||
int closeParen = data.indexOf(')', uriStart);
|
||||
if (closeParen == -1) {
|
||||
// Malformed — copy rest verbatim
|
||||
out.append(data.mid(found));
|
||||
pos = data.size();
|
||||
break;
|
||||
}
|
||||
|
||||
QByteArray uriVal = data.mid(uriStart, closeParen - uriStart);
|
||||
|
||||
// Extract page number: look for #page=N or bare page=N
|
||||
int pageNum = -1;
|
||||
int hashPos = uriVal.lastIndexOf("#page=");
|
||||
int digitStart = -1;
|
||||
if (hashPos != -1) {
|
||||
digitStart = hashPos + 6;
|
||||
} else if (uriVal.startsWith("page=")) {
|
||||
digitStart = 5;
|
||||
}
|
||||
if (digitStart != -1) {
|
||||
// Take only the leading digits: the fragment may carry extra
|
||||
// parameters after the page number (e.g. "22&fitr=15_489_..."),
|
||||
// and QByteArray::toInt() would fail on the whole remainder.
|
||||
int e = digitStart;
|
||||
while (e < uriVal.size()
|
||||
&& uriVal[e] >= '0' && uriVal[e] <= '9')
|
||||
++e;
|
||||
if (e > digitStart)
|
||||
pageNum = uriVal.mid(digitStart, e - digitStart).toInt();
|
||||
}
|
||||
|
||||
if (pageNum >= 1 && pageNum <= pageObjs.size()) {
|
||||
// Valid page reference — emit GoTo action.
|
||||
int pageObjNum = pageObjs[pageNum - 1];
|
||||
|
||||
// Optional precise destination: &fitr=Left_Bottom_Right_Top
|
||||
// (integer PDF points). If present -> /FitR (frame the element);
|
||||
// otherwise -> /Fit (whole page, top).
|
||||
QByteArray dest = " /Fit]";
|
||||
int fr = uriVal.indexOf("fitr=");
|
||||
if (fr != -1) {
|
||||
QByteArray rest = uriVal.mid(fr + 5);
|
||||
// stop at first char that is not part of the number list
|
||||
int end = 0;
|
||||
while (end < rest.size()
|
||||
&& ((rest[end] >= '0' && rest[end] <= '9')
|
||||
|| rest[end] == '_' || rest[end] == '-'))
|
||||
++end;
|
||||
QList<QByteArray> parts = rest.left(end).split('_');
|
||||
if (parts.size() == 4) {
|
||||
dest = " /FitR " + parts[0] + " " + parts[1] + " "
|
||||
+ parts[2] + " " + parts[3] + "]";
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray goTo = sGoTo
|
||||
+ QByteArray::number(pageObjNum)
|
||||
+ " 0 R" + dest;
|
||||
out.append(goTo);
|
||||
changed = true;
|
||||
} else {
|
||||
// Unknown page — keep original URI
|
||||
out.append(sUri);
|
||||
out.append(uriVal);
|
||||
out.append(')');
|
||||
}
|
||||
|
||||
pos = closeParen + 1; // skip past ')'
|
||||
}
|
||||
|
||||
if (!changed) return; // nothing was replaced
|
||||
data = out;
|
||||
}
|
||||
|
||||
// --- 4. Rebuild xref table ---
|
||||
// Find start of existing xref (last occurrence)
|
||||
int xrefStart = data.lastIndexOf("\nxref\n");
|
||||
if (xrefStart == -1) xrefStart = data.lastIndexOf("\nxref ");
|
||||
if (xrefStart == -1) return; // malformed PDF
|
||||
++xrefStart; // skip the leading '\n'
|
||||
|
||||
QByteArray body = data.left(xrefStart);
|
||||
|
||||
// Collect all object offsets from the body
|
||||
QMap<int, int> offsets; // objNum -> byte offset
|
||||
{
|
||||
const QByteArray objMarker = " 0 obj";
|
||||
int pos = 0;
|
||||
while ((pos = body.indexOf(objMarker, pos)) != -1) {
|
||||
int numStart = pos - 1;
|
||||
while (numStart > 0 && body[numStart-1] != '\n' && body[numStart-1] != '\r')
|
||||
--numStart;
|
||||
QByteArray numStr = body.mid(numStart, pos - numStart).trimmed();
|
||||
bool ok = false;
|
||||
int objNum = numStr.toInt(&ok);
|
||||
if (ok && objNum > 0)
|
||||
offsets[objNum] = numStart;
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
|
||||
if (offsets.isEmpty()) return;
|
||||
|
||||
int maxObj = offsets.lastKey();
|
||||
|
||||
// Build xref table
|
||||
QByteArray xref;
|
||||
xref += "xref\n";
|
||||
xref += "0 " + QByteArray::number(maxObj + 1) + "\n";
|
||||
xref += "0000000000 65535 f \n";
|
||||
for (int i = 1; i <= maxObj; ++i) {
|
||||
if (offsets.contains(i)) {
|
||||
xref += QByteArray::number(offsets[i]).rightJustified(10, '0')
|
||||
+ " 00000 n \n";
|
||||
} else {
|
||||
xref += "0000000000 65535 f \n";
|
||||
}
|
||||
}
|
||||
|
||||
// Find trailer dict from the original xref section
|
||||
int trailerPos = data.indexOf("trailer", xrefStart);
|
||||
int trailerEnd = -1;
|
||||
if (trailerPos != -1) {
|
||||
trailerEnd = data.indexOf("%%EOF", trailerPos);
|
||||
if (trailerEnd != -1) trailerEnd += 5;
|
||||
}
|
||||
|
||||
QByteArray trailer;
|
||||
if (trailerPos != -1 && trailerEnd != -1)
|
||||
trailer = data.mid(trailerPos, trailerEnd - trailerPos);
|
||||
else
|
||||
trailer = "trailer\n<<>>\n%%EOF";
|
||||
|
||||
int newXrefOffset = body.size();
|
||||
|
||||
QByteArray result;
|
||||
result.reserve(body.size() + xref.size() + trailer.size() + 30);
|
||||
result += body;
|
||||
result += xref;
|
||||
result += trailer;
|
||||
result += "\nstartxref\n";
|
||||
result += QByteArray::number(newXrefOffset);
|
||||
result += "\n%%EOF\n";
|
||||
|
||||
// --- 5. Write back ---
|
||||
QFile out(pdfPath);
|
||||
if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) return;
|
||||
out.write(result);
|
||||
out.close();
|
||||
}
|
||||
|
||||
void ProjectPrintWindow::requestPaint()
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
|
||||
@@ -214,13 +461,47 @@ void ProjectPrintWindow::requestPaint()
|
||||
return;
|
||||
}
|
||||
|
||||
// Build diagram -> first physical PDF page number map (1-based)
|
||||
// Must be done before the print loop since page numbers depend on order
|
||||
QMap<Diagram*, int> diagramPageMap;
|
||||
{
|
||||
int pageNum = 1;
|
||||
for (auto diagram : selectedDiagram()) {
|
||||
diagramPageMap.insert(diagram, pageNum);
|
||||
// Each diagram may span multiple pages if not fit_page
|
||||
if (!ui->m_fit_in_page_cb->isChecked()) {
|
||||
auto option = exportProperties();
|
||||
bool full_page = m_printer->fullPage();
|
||||
int h = horizontalPagesCount(diagram, option, full_page);
|
||||
int v = verticalPagesCount(diagram, option, full_page);
|
||||
pageNum += h * v;
|
||||
} else {
|
||||
pageNum += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool first = true;
|
||||
QPainter painter(m_printer);
|
||||
|
||||
// A real PDF export uses the QPdfEngine; the on-screen preview uses a
|
||||
// preview paint engine. We only post-process when actually writing a PDF.
|
||||
const bool pdfExport =
|
||||
(m_printer->outputFormat() == QPrinter::PdfFormat)
|
||||
&& (dynamic_cast<QPdfEngine*>(painter.paintEngine()) != nullptr);
|
||||
|
||||
for (auto diagram : selectedDiagram())
|
||||
{
|
||||
first ? first = false : m_printer->newPage();
|
||||
printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer);
|
||||
printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer, diagramPageMap);
|
||||
}
|
||||
|
||||
// Note: do NOT call painter.end() or pdfConvertUriToGoTo() here.
|
||||
// We are inside the paintRequested slot: the QPrintPreviewWidget still
|
||||
// owns the paint cycle. On macOS arm64 (Metal/CALayer compositor),
|
||||
// closing the QPainter manually inside this slot leaves the backing
|
||||
// store in an undefined state, producing a black screen after export.
|
||||
// pdfConvertUriToGoTo() is deferred to print() via QTimer::singleShot(0).
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,7 +511,7 @@ void ProjectPrintWindow::requestPaint()
|
||||
* @param fit_page
|
||||
* @param printer
|
||||
*/
|
||||
void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer)
|
||||
void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap<Diagram*, int> &diagramPageMap)
|
||||
{
|
||||
|
||||
////Prepare the print////
|
||||
@@ -317,6 +598,171 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter
|
||||
}
|
||||
}
|
||||
|
||||
////Inject PDF cross-reference links////
|
||||
if (printer->outputFormat() == QPrinter::PdfFormat && fit_page) {
|
||||
auto *pdfEngine = dynamic_cast<QPdfEngine*>(painter->paintEngine());
|
||||
if (pdfEngine) {
|
||||
|
||||
// QGraphicsScene::render() fait save()/restore() : worldTransform()
|
||||
// est revenu a l'identite ici. On reconstruit DONC explicitement la
|
||||
// transform appliquee par :
|
||||
// diagram->render(painter, QRectF(), diagram_rect, KeepAspectRatio)
|
||||
// cible vide => painter->viewport() ; source = diagram_rect ; centre.
|
||||
const QRectF target = QRectF(painter->viewport());
|
||||
const QRectF source = QRectF(diagram_rect); // meme source que render()
|
||||
|
||||
// render() ANCRE en haut-gauche (pas de centrage) :
|
||||
// translate(target.topLeft) . scale(s,s) . translate(-source.topLeft)
|
||||
// On reproduit EXACTEMENT ca — surtout PAS de (target-source*s)/2.
|
||||
const qreal s = qMin(target.width() / source.width(),
|
||||
target.height() / source.height());
|
||||
|
||||
QTransform fit;
|
||||
fit.translate(target.x(), target.y());
|
||||
fit.scale(s, s);
|
||||
fit.translate(-source.x(), -source.y()); // scene -> pixels device
|
||||
|
||||
// IMPORTANT : QPdfEngine::drawHyperlink() applique lui-meme
|
||||
// pageMatrix() (echelle 72/resolution + inversion de Y + marges).
|
||||
// On lui passe donc le rectangle en PIXELS DEVICE, sans aucune
|
||||
// conversion en points ni flip de notre cote.
|
||||
const QRectF pageBounds(0, 0, target.width(), target.height());
|
||||
|
||||
// ---- Device-pixels -> PDF points, replicating QPdfEnginePrivate::pageMatrix()
|
||||
// (same geometry for every page: same printer, page size and margins). ----
|
||||
const qreal pt_scale = 72.0 / printer->resolution();
|
||||
const qreal fullH_pt = printer->pageLayout().fullRectPoints().height();
|
||||
const bool fullPageMode =
|
||||
(printer->pageLayout().mode() == QPageLayout::FullPageMode);
|
||||
const QRect paintPx =
|
||||
printer->pageLayout().paintRectPixels(printer->resolution());
|
||||
auto devToPdf = [=](const QPointF &d) -> QPointF {
|
||||
qreal dx = d.x(), dy = d.y();
|
||||
if (!fullPageMode) { dx += paintPx.left(); dy += paintPx.top(); }
|
||||
return QPointF(pt_scale * dx, fullH_pt - pt_scale * dy);
|
||||
};
|
||||
|
||||
// Compute, in PDF points on its OWN page, the rectangle to frame for a
|
||||
// target element (used as a /FitR destination so the link zooms onto it).
|
||||
auto destRectPdf = [&](Element *tgt) -> QRectF {
|
||||
Diagram *dg = tgt ? tgt->diagram() : nullptr;
|
||||
if (!dg) return QRectF();
|
||||
const QRectF srcT = QRectF(diagramRect(dg, exportProperties()));
|
||||
if (srcT.width() <= 0.0 || srcT.height() <= 0.0) return QRectF();
|
||||
const qreal sT = qMin(target.width() / srcT.width(),
|
||||
target.height() / srcT.height());
|
||||
QTransform fitT;
|
||||
fitT.translate(target.x(), target.y());
|
||||
fitT.scale(sT, sT);
|
||||
fitT.translate(-srcT.x(), -srcT.y());
|
||||
|
||||
QRectF elemScene = tgt->mapRectToScene(tgt->boundingRect());
|
||||
// Frame the element with a little context, and enforce a minimum
|
||||
// framed size so tiny contacts don't zoom in extremely.
|
||||
const qreal pad = 25.0;
|
||||
elemScene.adjust(-pad, -pad, pad, pad);
|
||||
const qreal minSide = 160.0;
|
||||
if (elemScene.width() < minSide)
|
||||
elemScene.adjust(-(minSide - elemScene.width()) / 2.0, 0,
|
||||
(minSide - elemScene.width()) / 2.0, 0);
|
||||
if (elemScene.height() < minSide)
|
||||
elemScene.adjust(0, -(minSide - elemScene.height()) / 2.0,
|
||||
0, (minSide - elemScene.height()) / 2.0);
|
||||
|
||||
const QRectF devT = fitT.mapRect(elemScene);
|
||||
const QPointF a = devToPdf(devT.topLeft());
|
||||
const QPointF b = devToPdf(devT.bottomRight());
|
||||
return QRectF(QPointF(qMin(a.x(), b.x()), qMin(a.y(), b.y())),
|
||||
QPointF(qMax(a.x(), b.x()), qMax(a.y(), b.y())));
|
||||
};
|
||||
|
||||
auto injectLink = [&](const QRectF &sceneRect, Element *targetElmt) {
|
||||
if (!targetElmt || !targetElmt->diagram()) return;
|
||||
const int targetPage =
|
||||
diagramPageMap.value(targetElmt->diagram(), -1);
|
||||
if (targetPage < 1) return;
|
||||
const QRectF devRect = fit.mapRect(sceneRect);
|
||||
if (!devRect.isValid() || !pageBounds.intersects(devRect)) return;
|
||||
|
||||
QString frag = QString("page=%1").arg(targetPage);
|
||||
const QRectF d = destRectPdf(targetElmt); // /FitR L_B_R_T
|
||||
if (d.isValid())
|
||||
frag += QString("&fitr=%1_%2_%3_%4")
|
||||
.arg(qRound(d.left())).arg(qRound(d.top()))
|
||||
.arg(qRound(d.right())).arg(qRound(d.bottom()));
|
||||
|
||||
QUrl url = QUrl::fromLocalFile(printer->outputFileName());
|
||||
url.setFragment(frag);
|
||||
pdfEngine->drawHyperlink(devRect, url);
|
||||
};
|
||||
|
||||
for (auto *item : diagram->items()) {
|
||||
|
||||
// --- CrossRefItem links ---
|
||||
if (auto *xref = dynamic_cast<CrossRefItem*>(item)) {
|
||||
for (auto it = xref->hoveredContactsMap().begin();
|
||||
it != xref->hoveredContactsMap().end(); ++it)
|
||||
{
|
||||
Element *targetElmt = it.key();
|
||||
if (!targetElmt || !targetElmt->diagram()) continue;
|
||||
// it.value() est en coords LOCALES du CrossRefItem -> scene
|
||||
injectLink(xref->mapRectToScene(it.value()), targetElmt);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// --- Folio report links (DynamicElementTextItem) ---
|
||||
if (auto *deti = dynamic_cast<DynamicElementTextItem*>(item)) {
|
||||
Element *parent = deti->parentElement();
|
||||
if (!parent) continue;
|
||||
|
||||
// (a) Report element : label -> linked report on another folio
|
||||
if (parent->linkType() & Element::AllReport) {
|
||||
if (parent->linkedElements().isEmpty()) continue;
|
||||
|
||||
bool showsLabel =
|
||||
(deti->textFrom() == DynamicElementTextItem::ElementInfo
|
||||
&& deti->infoName() == QLatin1String("label")) ||
|
||||
(deti->textFrom() == DynamicElementTextItem::CompositeText
|
||||
&& deti->compositeText().contains(QStringLiteral("%{label}")));
|
||||
if (!showsLabel) continue;
|
||||
|
||||
Element *targetElmt = parent->linkedElements().first();
|
||||
if (!targetElmt || !targetElmt->diagram()) continue;
|
||||
|
||||
injectLink(deti->mapRectToScene(deti->boundingRect()), targetElmt);
|
||||
continue;
|
||||
}
|
||||
|
||||
// (b) Slave element : the "(folio-pos)" text -> master element
|
||||
if (parent->linkType() == Element::Slave) {
|
||||
QGraphicsTextItem *sx = deti->slaveXrefItem();
|
||||
Element *master = deti->masterElement();
|
||||
if (sx && master && master->diagram()) {
|
||||
injectLink(sx->mapRectToScene(sx->boundingRect()), master);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// --- Slave cross-reference carried by a grouped text ---
|
||||
if (auto *grp = dynamic_cast<ElementTextItemGroup*>(item)) {
|
||||
Element *parent = grp->parentElement();
|
||||
if (!parent || parent->linkType() != Element::Slave) continue;
|
||||
if (parent->linkedElements().isEmpty()) continue;
|
||||
QGraphicsTextItem *sx = grp->slaveXrefItem();
|
||||
if (!sx) continue;
|
||||
Element *master = parent->linkedElements().first();
|
||||
if (!master || !master->diagram()) continue;
|
||||
injectLink(sx->mapRectToScene(sx->boundingRect()), master);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
////PDF links end////
|
||||
|
||||
////Print is finished, restore diagram and graphics item properties
|
||||
for (auto view : diagram->views()) {
|
||||
view->setInteractive(true);
|
||||
@@ -772,9 +1218,29 @@ void ProjectPrintWindow::on_m_uncheck_all_clicked()
|
||||
|
||||
void ProjectPrintWindow::print()
|
||||
{
|
||||
m_preview->print();
|
||||
const bool isPdf = (m_printer->outputFormat() == QPrinter::PdfFormat);
|
||||
const QString pdfFile = isPdf ? m_printer->outputFileName() : QString();
|
||||
|
||||
m_preview->print(); // triggers requestPaint() synchronously; painter
|
||||
// is created/destroyed inside that call
|
||||
|
||||
savePageSetupForCurrentPrinter();
|
||||
this->close();
|
||||
|
||||
if (isPdf && !pdfFile.isEmpty()) {
|
||||
// Defer post-processing and window close to the next event-loop
|
||||
// iteration. This lets the macOS arm64 Metal compositor finish
|
||||
// compositing the backing store before the window is destroyed,
|
||||
// which prevents the black screen observed on Apple Silicon under
|
||||
// macOS Sequoia (QPrintPreviewWidget + CALayer timing issue).
|
||||
QTimer::singleShot(0, this, [this, pdfFile]() {
|
||||
// Convert URI link annotations into native internal GoTo/FitR
|
||||
// actions so cross-references jump inside the document.
|
||||
pdfConvertUriToGoTo(pdfFile);
|
||||
this->close();
|
||||
});
|
||||
} else {
|
||||
this->close();
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectPrintWindow::on_m_date_cb_userDateChanged(const QDate &date)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "../exportproperties.h"
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QMap>
|
||||
#include <QPrinter>
|
||||
|
||||
namespace Ui {
|
||||
@@ -79,7 +80,7 @@ class ProjectPrintWindow : public QMainWindow
|
||||
|
||||
private:
|
||||
void requestPaint();
|
||||
void printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer);
|
||||
void printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap<Diagram*, int> &diagramPageMap = {});
|
||||
QRect diagramRect(Diagram *diagram, const ExportProperties &option) const;
|
||||
int horizontalPagesCount(Diagram *diagram, const ExportProperties &option, bool full_page) const;
|
||||
int verticalPagesCount(Diagram *diagram, const ExportProperties &option, bool full_page) const;
|
||||
|
||||
@@ -45,14 +45,26 @@ QDomElement ElementData::toXml(QDomDocument &xml_element) const {
|
||||
bool ElementData::fromXml(const QDomElement &xml_element)
|
||||
{
|
||||
if(xml_element.tagName() != QLatin1String("definition") ||
|
||||
xml_element.attribute(QStringLiteral("type")) != QLatin1String("element")) {
|
||||
xml_element.attribute(QStringLiteral("type")) != QLatin1String("element")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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 ---
|
||||
|
||||
m_type = typeFromString(xml_element.attribute(QStringLiteral("link_type"), QStringLiteral("simple")));
|
||||
kindInfoFromXml(xml_element);
|
||||
m_informations.fromXml(xml_element.firstChildElement(QStringLiteral("elementInformations")),
|
||||
QStringLiteral("elementInformation"));
|
||||
QStringLiteral("elementInformation"));
|
||||
m_names_list.fromXml(xml_element);
|
||||
|
||||
auto xml_draw_info = xml_element.firstChildElement(QStringLiteral("informations"));
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
QPointF p1[3] = {
|
||||
QPointF(5, 0),
|
||||
QPointF(10, 3),
|
||||
QPointF(15, 3),
|
||||
};
|
||||
qp.drawPolyline(p1,3);
|
||||
qp.end();
|
||||
}
|
||||
//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(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),
|
||||
};
|
||||
painter.drawPolyline(p1, 3);
|
||||
|
||||
//draw the NC contact
|
||||
if (m_hdr_nc_ctc.isNull()) {
|
||||
qp.begin(&m_hdr_nc_ctc);
|
||||
qp.setPen(pen_);
|
||||
QPointF p2[3] = {
|
||||
QPointF(0, 3),
|
||||
QPointF(5, 3),
|
||||
QPointF(5, 0)
|
||||
};
|
||||
qp.drawPolyline(p2,3);
|
||||
QPointF p3[3] = {
|
||||
QPointF(4, 0),
|
||||
QPointF(10, 3),
|
||||
QPointF(15, 3),
|
||||
};
|
||||
qp.drawPolyline(p3,3);
|
||||
qp.end();
|
||||
}
|
||||
//draw the NC contact header symbol
|
||||
QPointF p2[3] = {
|
||||
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)
|
||||
};
|
||||
painter.drawPolyline(p2, 3);
|
||||
QPointF p3[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),
|
||||
};
|
||||
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);
|
||||
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));*/
|
||||
// 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);
|
||||
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))
|
||||
{
|
||||
m_hovered_contacts_map.insert(elmt, bounding_rect);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_hovered_contacts_map.insert(elmt, bounding_rect);
|
||||
}
|
||||
if (m_update_map)
|
||||
m_hovered_contacts_map.insert(elmt, text_rect);
|
||||
|
||||
++m_drawed_contacts;
|
||||
}
|
||||
@@ -768,6 +960,25 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt)
|
||||
};
|
||||
painter.drawPolyline(p2, 3);
|
||||
|
||||
// Draw terminal names for switch contact (3 terminals)
|
||||
// terminal_names[0] = NC (bottom-left)
|
||||
// terminal_names[1] = NO (top-left)
|
||||
// terminal_names[2] = Common (right)
|
||||
if (!terminal_names.isEmpty() && m_properties.showTerminalName()) {
|
||||
painter.setFont(QETApp::diagramTextsFont(4));
|
||||
// Storage order set above: [0]=NC, [1]=NO, [2]=Common
|
||||
if (terminal_names.size() >= 2)
|
||||
painter.drawText(QRectF(0, offset, 8, 8),
|
||||
Qt::AlignLeft|Qt::AlignTop, terminal_names[1]); // NO top-left
|
||||
if (terminal_names.size() >= 3)
|
||||
painter.drawText(QRectF(16, offset+4, 8, 6),
|
||||
Qt::AlignRight|Qt::AlignTop, terminal_names[2]); // Common right
|
||||
if (terminal_names.size() >= 1)
|
||||
painter.drawText(QRectF(0, offset+9, 8, 6),
|
||||
Qt::AlignLeft|Qt::AlignTop, terminal_names[0]); // NC bottom-left
|
||||
painter.setFont(QETApp::diagramTextsFont(5));
|
||||
}
|
||||
|
||||
//Draw the half ellipse off delay
|
||||
if (flags &Delay)
|
||||
{
|
||||
@@ -799,12 +1010,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)) {
|
||||
m_hovered_contacts_map.insert(elmt, bounding_rect);
|
||||
}
|
||||
else {
|
||||
m_hovered_contacts_map.insert(elmt, bounding_rect);
|
||||
}
|
||||
if (m_update_map)
|
||||
m_hovered_contacts_map.insert(elmt, text_rect);
|
||||
|
||||
//a switch contact take place of two normal contact
|
||||
m_drawed_contacts += 2;
|
||||
@@ -835,12 +1042,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)) {
|
||||
m_hovered_contacts_map.insert(elmt, bounding_rect);
|
||||
}
|
||||
else {
|
||||
m_hovered_contacts_map.insert(elmt, bounding_rect);
|
||||
}
|
||||
if (m_update_map)
|
||||
m_hovered_contacts_map.insert(elmt, text_rect);
|
||||
++m_drawed_contacts;
|
||||
}
|
||||
return bounding_rect;
|
||||
@@ -866,7 +1069,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 +1130,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 +1149,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 +1210,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();
|
||||
|
||||
@@ -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,16 +116,22 @@ 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;
|
||||
ElementTextItemGroup *m_group = nullptr;
|
||||
QList <QMetaObject::Connection> m_slave_connection;
|
||||
QList <QMetaObject::Connection> m_update_connection;
|
||||
|
||||
public:
|
||||
/// Returns the map of linked elements and their clickable rects (local coords).
|
||||
/// Used by the PDF export to inject hyperlink annotations.
|
||||
const QMultiMap<Element *, QRectF> &hoveredContactsMap() const
|
||||
{ return m_hovered_contacts_map; }
|
||||
};
|
||||
|
||||
#endif // CROSSREFITEM_H
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
#include "crossrefitem.h"
|
||||
#include "element.h"
|
||||
#include "elementtextitemgroup.h"
|
||||
|
||||
#include <QTimer>
|
||||
#include <QDomDocument>
|
||||
#include <QDomElement>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
@@ -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,28 +729,29 @@ 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.
|
||||
//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())
|
||||
{
|
||||
m_report_formula = m_parent_element.data()->diagram()->project()->defaultReportProperties();
|
||||
m_report_formula_con = connect(m_parent_element.data()->diagram()->project(), &QETProject::reportPropertiesChanged, this, &DynamicElementTextItem::reportFormulaChanged);
|
||||
}
|
||||
|
||||
//Add connection to keep up to date the status of the element linked to the parent folio report of this text.
|
||||
|
||||
//Add connection to keep up to date the status of the element linked to the parent folio report of this text.
|
||||
connect(m_parent_element.data(), &Element::linkedElementChanged, this, &DynamicElementTextItem::reportChanged);
|
||||
//The parent is already linked, we call reportChanged for init the connection
|
||||
//The parent is already linked, we call reportChanged for init the connection
|
||||
if(!m_parent_element.data()->linkedElements().isEmpty())
|
||||
reportChanged();
|
||||
|
||||
|
||||
if(m_parent_element.data()->terminals().size())
|
||||
{
|
||||
//Add connection to keep up date the conductors added or removed to the parent folio report element
|
||||
//Add connection to keep up date the conductors added or removed to the parent folio report element
|
||||
connect(m_parent_element.data()->terminals().first(), &Terminal::conductorWasAdded, this, &DynamicElementTextItem::conductorWasAdded);
|
||||
connect(m_parent_element.data()->terminals().first(), &Terminal::conductorWasRemoved, this, &DynamicElementTextItem::conductorWasRemoved);
|
||||
}
|
||||
//Get a conductor in the potential
|
||||
//Get a conductor in the potential
|
||||
setPotentialConductor();
|
||||
}
|
||||
else if(m_parent_element.data()->linkType() == Element::Master)
|
||||
@@ -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")
|
||||
@@ -1098,7 +1104,10 @@ void DynamicElementTextItem::updateLabel()
|
||||
void DynamicElementTextItem::conductorWasAdded(Conductor *conductor)
|
||||
{
|
||||
Q_UNUSED(conductor)
|
||||
setPotentialConductor();
|
||||
QTimer::singleShot(100, this, [this]() {
|
||||
setPotentialConductor();
|
||||
conductorPropertiesChanged();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1123,7 +1132,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 +1166,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 +1183,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)
|
||||
{
|
||||
@@ -1200,8 +1212,9 @@ void DynamicElementTextItem::conductorPropertiesChanged()
|
||||
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;
|
||||
|
||||
|
||||
@@ -86,6 +86,9 @@ class DynamicElementTextItem : public DiagramTextItem
|
||||
void fromXml(const QDomElement &dom_elmt) override;
|
||||
|
||||
Element *parentElement() const;
|
||||
/// PDF export: slave cross-reference text item ("(folio-pos)") and its master target.
|
||||
QGraphicsTextItem *slaveXrefItem() const { return m_slave_Xref_item; }
|
||||
Element *masterElement() const { return m_master_element.data(); }
|
||||
ElementTextItemGroup *parentGroup() const;
|
||||
Element *elementUseForInfo() const;
|
||||
void refreshLabelConnection();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,14 +52,15 @@ class Element : public QetGraphicsItem
|
||||
(master, slave, report ect...)
|
||||
*/
|
||||
enum kind {
|
||||
Simple = 1,
|
||||
NextReport = 2,
|
||||
PreviousReport = 4,
|
||||
AllReport = 6,
|
||||
Master = 8,
|
||||
Slave = 16,
|
||||
Terminale = 32,
|
||||
Thumbnail = 64};
|
||||
Simple = 1,
|
||||
NextReport = 2,
|
||||
PreviousReport = 4,
|
||||
AllReport = 6,
|
||||
Master = 8,
|
||||
Slave = 16,
|
||||
Terminale = 32,
|
||||
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;
|
||||
|
||||
@@ -76,6 +76,8 @@ class ElementTextItemGroup : public QObject, public QGraphicsItemGroup
|
||||
QList<DynamicElementTextItem *> texts() const;
|
||||
Diagram *diagram() const;
|
||||
Element *parentElement() const;
|
||||
/// PDF export: slave cross-reference text item of the group, if any.
|
||||
QGraphicsTextItem *slaveXrefItem() const { return m_slave_Xref_item; }
|
||||
|
||||
QDomElement toXml(QDomDocument &dom_document) const;
|
||||
void fromXml(QDomElement &dom_element);
|
||||
|
||||
@@ -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
|
||||
@@ -779,6 +788,12 @@ QList<Terminal *> relatedPotentialTerminal (
|
||||
// If terminal parent element is a Terminal element.
|
||||
else if (terminal -> parentElement() -> linkType() & Element::Terminale)
|
||||
{
|
||||
// English: Check if the user activated the potential isolation checkbox for this terminal
|
||||
if (terminal->parentElement()->elementInformations().value(QStringLiteral("potential_isolating")).toString() == QLatin1String("true")) {
|
||||
// English: Potential is isolated. Return an empty list so it does not propagate to the other side.
|
||||
return QList<Terminal *>();
|
||||
}
|
||||
|
||||
QList <Terminal *> terminals = terminal->parentElement()->terminals();
|
||||
terminals.removeAll(const_cast<Terminal *>(terminal));
|
||||
return terminals;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -189,7 +189,8 @@ QStringList QETInformation::elementInfoKeys()
|
||||
ELMT_MACHINE_MANUFACTURER_REF_AUX4,
|
||||
ELMT_SUPPLIER_AUX4,
|
||||
ELMT_QUANTITY_AUX4,
|
||||
ELMT_UNITY_AUX4, };
|
||||
ELMT_UNITY_AUX4,
|
||||
"exclude_from_bom" };
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "elementinfowidget.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include "../diagram.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
@@ -161,6 +161,13 @@ void ElementInfoWidget::enableLiveEdit()
|
||||
for (ElementInfoPartWidget *eipw : m_eipw_list)
|
||||
connect(eipw, &ElementInfoPartWidget::textChanged, this, &ElementInfoWidget::apply);
|
||||
connect(ui->m_auto_num_locked_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
|
||||
|
||||
if (m_potential_isolating_cb) {
|
||||
connect(m_potential_isolating_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
|
||||
}
|
||||
if (m_exclude_from_bom_cb) {
|
||||
connect(m_exclude_from_bom_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,6 +179,13 @@ void ElementInfoWidget::disableLiveEdit()
|
||||
for (ElementInfoPartWidget *eipw : m_eipw_list)
|
||||
disconnect(eipw, &ElementInfoPartWidget::textChanged, this, &ElementInfoWidget::apply);
|
||||
disconnect(ui->m_auto_num_locked_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
|
||||
|
||||
if (m_potential_isolating_cb) {
|
||||
disconnect(m_potential_isolating_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
|
||||
}
|
||||
if (m_exclude_from_bom_cb) {
|
||||
disconnect(m_exclude_from_bom_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,16 +207,34 @@ void ElementInfoWidget::buildInterface()
|
||||
ui->scroll_vlayout->addWidget(eipw);
|
||||
m_eipw_list << eipw;
|
||||
}
|
||||
|
||||
ui->scroll_vlayout->addStretch();
|
||||
|
||||
// Existing potential isolating checkbox
|
||||
m_potential_isolating_cb = new QCheckBox(tr("Séparation de potentiel"), this);
|
||||
m_potential_isolating_cb->setStyleSheet(QStringLiteral("margin: 5px; font-weight: bold;"));
|
||||
|
||||
// English: Initialize and style the BOM exclusion checkbox
|
||||
m_exclude_from_bom_cb = new QCheckBox(tr("Exclure de la nomenclature"), this);
|
||||
m_exclude_from_bom_cb->setStyleSheet(QStringLiteral("margin: 5px; font-weight: bold;"));
|
||||
|
||||
if (QVBoxLayout *mainLayout = qobject_cast<QVBoxLayout*>(this->layout())) {
|
||||
mainLayout->insertWidget(1, m_potential_isolating_cb);
|
||||
// English: Insert the new checkbox into the main vertical layout
|
||||
mainLayout->insertWidget(2, m_exclude_from_bom_cb);
|
||||
}
|
||||
|
||||
// English: BOM exclusion applies to all elements, so it's always visible
|
||||
m_exclude_from_bom_cb->setVisible(true);
|
||||
|
||||
// Show checkbox only if the element is a terminal
|
||||
if (m_element.data()->elementData().m_type == ElementData::Terminal) {
|
||||
ui->m_auto_num_locked_cb->setVisible(true);
|
||||
m_potential_isolating_cb->setVisible(true);
|
||||
} else {
|
||||
ui->m_auto_num_locked_cb->setVisible(false);
|
||||
m_potential_isolating_cb->setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoWidget::infoPartWidgetForKey
|
||||
@param key
|
||||
@@ -241,8 +273,13 @@ void ElementInfoWidget::updateUi()
|
||||
}
|
||||
// Load the lock status for auto numbering
|
||||
if (m_element->elementData().m_type == ElementData::Terminal) {
|
||||
QString lock_value = element_info.value(QStringLiteral("auto_num_locked")).toString();
|
||||
ui->m_auto_num_locked_cb->setChecked(lock_value == QLatin1String("true"));
|
||||
// ... (bestehender Terminal-Code für auto_num_locked und potential_isolating) ...
|
||||
}
|
||||
|
||||
// English: Load the BOM exclusion status from the element information mapping
|
||||
if (m_exclude_from_bom_cb) {
|
||||
QString exclude_bom_value = element_info.value(QStringLiteral("exclude_from_bom")).toString();
|
||||
m_exclude_from_bom_cb->setChecked(exclude_bom_value == QLatin1String("true"));
|
||||
}
|
||||
|
||||
if (m_live_edit) {
|
||||
@@ -274,11 +311,14 @@ DiagramContext ElementInfoWidget::currentInfo() const
|
||||
|
||||
// Save the auto numbering lock status
|
||||
if (m_element->elementData().m_type == ElementData::Terminal) {
|
||||
info_.addValue(QStringLiteral("auto_num_locked"), ui->m_auto_num_locked_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
|
||||
}
|
||||
|
||||
if (m_exclude_from_bom_cb) {
|
||||
info_.addValue(QStringLiteral("exclude_from_bom"), m_exclude_from_bom_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
|
||||
}
|
||||
|
||||
return info_;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoWidget::firstActivated
|
||||
Slot activated when this widget is show.
|
||||
|
||||
@@ -27,6 +27,7 @@ class Element;
|
||||
class QUndoCommand;
|
||||
class ElementInfoPartWidget;
|
||||
class ChangeElementInformationCommand;
|
||||
class QCheckBox;
|
||||
|
||||
namespace Ui {
|
||||
class ElementInfoWidget;
|
||||
@@ -71,6 +72,8 @@ class ElementInfoWidget : public AbstractElementPropertiesEditorWidget
|
||||
private:
|
||||
Ui::ElementInfoWidget *ui;
|
||||
QList <ElementInfoPartWidget *> m_eipw_list;
|
||||
QCheckBox *m_potential_isolating_cb = nullptr;
|
||||
QCheckBox *m_exclude_from_bom_cb = nullptr;
|
||||
bool m_first_activation;
|
||||
bool m_ui_builded = false;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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>
|
||||
|
||||
+205
-114
@@ -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,13 +92,16 @@ 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")) {
|
||||
info.isPlaceholder = true;
|
||||
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);
|
||||
infoMap.insert(uuid, info);
|
||||
}
|
||||
return infoMap;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -225,6 +158,45 @@ void WiringListExport::toCsv()
|
||||
return;
|
||||
}
|
||||
|
||||
QSet<QString> conductorDefinitionTypes;
|
||||
QDomElement rootElem = doc.documentElement();
|
||||
QDomElement collection = rootElem.firstChildElement("collection");
|
||||
if (!collection.isNull()) {
|
||||
QDomNodeList defs = collection.elementsByTagName("definition");
|
||||
for (int i = 0; i < defs.size(); ++i) {
|
||||
QDomElement def = defs.at(i).toElement();
|
||||
if (def.attribute("link_type") == "conductor_definition") {
|
||||
QDomElement parentEl = def.parentNode().toElement();
|
||||
if (parentEl.tagName().toLower() == "element") {
|
||||
QString name = parentEl.attribute("name");
|
||||
if (!name.isEmpty()) {
|
||||
conductorDefinitionTypes.insert(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QSet<QString> conductorDefinitionUuids;
|
||||
QDomNodeList projectElements = rootElem.elementsByTagName("element");
|
||||
for (int i = 0; i < projectElements.size(); ++i) {
|
||||
QDomElement el = projectElements.at(i).toElement();
|
||||
QString typeVal = el.attribute("type");
|
||||
bool isCondDef = false;
|
||||
for (const QString &cType : conductorDefinitionTypes) {
|
||||
if (typeVal.endsWith(cType)) {
|
||||
isCondDef = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isCondDef) {
|
||||
QString uuid = normalizeUuid(el.attribute("uuid", el.attribute("id", "")));
|
||||
if (!uuid.isEmpty()) {
|
||||
conductorDefinitionUuids.insert(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QFileDialog dialog(m_parent);
|
||||
dialog.setAcceptMode(QFileDialog::AcceptSave);
|
||||
dialog.setWindowTitle(tr("Exporter le plan de câblage"));
|
||||
@@ -243,25 +215,144 @@ 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 (conductorDefinitionUuids.contains(c.el1_uuid) || conductorDefinitionUuids.contains(c.el2_uuid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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") << ";"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user