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

|
|
||||||
|
|
||||||
### Compilateurs
|
|
||||||
- Cliquer sur _ajouter_ puis choisir _MinGW_.
|
|
||||||
- Renseigner _Emplacement du compilateur C_ (exemple C:\\msys64\\ucrt64\\bin\\g++.exe).
|
|
||||||
- Dans le champ _Nom :_ ajouter (msys2).
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Débogueurs
|
|
||||||
- Cliquer sur _ajouter_
|
|
||||||
- Renseigner _Chemin :_ (exemple C:\\msys64\\ucrt64\\bin\\gdb.exe).
|
|
||||||
- Dans le champ _Nom :_ ajouter (msys2).
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### cmake
|
|
||||||
- Outils -> _Ajouter_
|
|
||||||
- Renseigner _Chemin :_ (exemple C:\\msys64\\ucrt64\\bin\\cmake.exe).
|
|
||||||
- Dans le champ _Nom :_ ajouter (msys2).
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### KIT
|
|
||||||
Maintenant que tous les prérequis sont fait nous allons crée un kit utilisant les outils fournis par MSYS2. Cliquer sur _Ajouter_, un nouveau kit _manuel_ apparaît, nommer celui-ci par exemple _Qt6 msys2_ puis renseigner le compilateur, le débogueur, la version de Qt et Outils CMake en choisissant à chaque fois ceux que nous venons de créer.
|
|
||||||
puis cliquer sur _appliquer_.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Bravo 🥳🥳 vous avez terminé l'installation de la totalité des outils de développement.
|
|
||||||
|
|
||||||
# Clonez le dépôts de QElectrotech
|
|
||||||
Clonez le dépôt de QElectroTech comme vous le faite habituellement, sinon utilisez les commandes suivante dans power shell.
|
|
||||||
|
|
||||||
Crée et/ou se rendre dans le dossier dans lequel vous voulez clonez le dépôt (dans l'exemple nous allons crée un dossier QElectroTech dans C:)
|
|
||||||
|
|
||||||
```
|
|
||||||
mkdir C:\QElectroTech
|
|
||||||
cd C:\QElectroTech
|
|
||||||
|
|
||||||
git clone --recursive https://github.com/qelectrotech/qelectrotech-source-mirror.git
|
|
||||||
```
|
|
||||||
|
|
||||||
Une fois le dépôt cloné lancer Qt creator puis choisir d'ouvrir un projet existant, en choisissant le _CMakeLists.txt_ se trouvant à la racine du projet QElectroTech, enfin dans l'assistant de création de projet choisir comme kit le kit que nous avons créer précédemment.
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
Compiler QElectroTech sous microsoft Windows 10 et 11
|
|
||||||
================================
|
|
||||||
Compiler QElectroTech pour et/ou sous Windows peut être effectué avec plusieurs méthode différente.
|
|
||||||
Ce document énumère uniquement les différentes méthode possible
|
|
||||||
|
|
||||||
N'est mentionné que les étapes nécessaire afin de compilé QElectroTech sous Windows avec Qt6 et cmake. Ce document ne traite pas la compilation avec Qt5 et qmake.
|
|
||||||
|
|
||||||
>QElectroTech 0.100 est la dernière version à utiliser Qt5. Les version suivante sont développé avec Qt6 et utilise cmake au lieu de qmake.
|
|
||||||
|
|
||||||
Il existe deux méthodes pour cela :
|
|
||||||
|
|
||||||
1. [Utiliser msys2 (méthode recommandé)](fr_window_build_msys2.md)
|
|
||||||
2. Télécharger et compiler l'ensemble des dépendances (non rédigé)
|
|
||||||
@@ -230,7 +230,11 @@ RESOURCES += qelectrotech.qrc
|
|||||||
TRANSLATIONS += lang/*.ts
|
TRANSLATIONS += lang/*.ts
|
||||||
|
|
||||||
# Modules Qt utilises par l'application
|
# Modules Qt utilises par l'application
|
||||||
QT += xml svg network sql widgets printsupport concurrent KWidgetsAddons KCoreAddons
|
QT += xml svg network sql widgets printsupport concurrent KWidgetsAddons KCoreAddons gui-private
|
||||||
|
|
||||||
|
# Private Qt GUI headers (needed for QPdfEngine::drawHyperlink)
|
||||||
|
# gui-private should add this automatically, but some distros need it explicit
|
||||||
|
INCLUDEPATH += $$[QT_INSTALL_HEADERS]/QtGui/$$[QT_VERSION]/QtGui
|
||||||
|
|
||||||
# UI DESIGNER FILES AND GENERATION SOURCES FILES
|
# UI DESIGNER FILES AND GENERATION SOURCES FILES
|
||||||
FORMS += $$files(sources/richtext/*.ui) \
|
FORMS += $$files(sources/richtext/*.ui) \
|
||||||
|
|||||||
@@ -26,7 +26,12 @@
|
|||||||
#include "xmlprojectelementcollectionitem.h"
|
#include "xmlprojectelementcollectionitem.h"
|
||||||
|
|
||||||
#include <QFutureWatcher>
|
#include <QFutureWatcher>
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
|
||||||
#include <QtConcurrentMap>
|
#include <QtConcurrentMap>
|
||||||
|
#else
|
||||||
|
#include <QtConcurrentRun>
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief ElementsCollectionModel::ElementsCollectionModel
|
@brief ElementsCollectionModel::ElementsCollectionModel
|
||||||
Constructor
|
Constructor
|
||||||
@@ -289,14 +294,14 @@ void ElementsCollectionModel::loadCollections(bool common_collection,
|
|||||||
connect(watcher, &QFutureWatcher<void>::progressRangeChanged,
|
connect(watcher, &QFutureWatcher<void>::progressRangeChanged,
|
||||||
this, &ElementsCollectionModel::loadingProgressRangeChanged);
|
this, &ElementsCollectionModel::loadingProgressRangeChanged);
|
||||||
connect(watcher, &QFutureWatcher<void>::finished,
|
connect(watcher, &QFutureWatcher<void>::finished,
|
||||||
this, &ElementsCollectionModel::loadingFinished);
|
this, &ElementsCollectionModel::loadingFinished);
|
||||||
connect(
|
connect(watcher, &QFutureWatcher<void>::finished, watcher, &QFutureWatcher<void>::deleteLater);
|
||||||
watcher,
|
|
||||||
&QFutureWatcher<void>::finished,
|
|
||||||
watcher,
|
|
||||||
&QFutureWatcher<void>::deleteLater);
|
|
||||||
|
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||||
m_future = QtConcurrent::map(m_items_list_to_setUp, setUpData);
|
m_future = QtConcurrent::map(m_items_list_to_setUp, setUpData);
|
||||||
|
#else
|
||||||
|
qDebug() << "Help code for QT 6 or later";
|
||||||
|
#endif
|
||||||
watcher->setFuture(m_future);
|
watcher->setFuture(m_future);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -835,8 +835,14 @@ void ElementsCollectionWidget::search()
|
|||||||
}
|
}
|
||||||
|
|
||||||
hideCollection(true);
|
hideCollection(true);
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
||||||
|
const QStringList text_list = text.split("+", QString::SkipEmptyParts);
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 5.14 or later")
|
||||||
|
#endif
|
||||||
const QStringList text_list = text.split("+", Qt::SkipEmptyParts);
|
const QStringList text_list = text.split("+", Qt::SkipEmptyParts);
|
||||||
|
#endif
|
||||||
QModelIndexList match_index;
|
QModelIndexList match_index;
|
||||||
for (QString txt : text_list) {
|
for (QString txt : text_list) {
|
||||||
match_index << m_model->match(m_showed_index.isValid()
|
match_index << m_model->match(m_showed_index.isValid()
|
||||||
|
|||||||
@@ -803,13 +803,13 @@ bool ElementsLocation::setXml(const QDomDocument &xml_document) const
|
|||||||
QString path_ = collectionPath(false);
|
QString path_ = collectionPath(false);
|
||||||
QRegularExpression rx("^(.*)/(.*\\.elmt)$");
|
QRegularExpression rx("^(.*)/(.*\\.elmt)$");
|
||||||
|
|
||||||
if (auto regex_match = rx.match(path_); regex_match.hasMatch())
|
if (rx.exactMatch(path_))
|
||||||
{
|
{
|
||||||
return project()
|
return project()
|
||||||
->embeddedElementCollection()
|
->embeddedElementCollection()
|
||||||
->addElementDefinition(
|
->addElementDefinition(
|
||||||
regex_match.captured(1),
|
rx.cap(1),
|
||||||
regex_match.captured(2),
|
rx.cap(2),
|
||||||
xml_document.documentElement());
|
xml_document.documentElement());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
#include "../NameList/nameslist.h"
|
#include "../NameList/nameslist.h"
|
||||||
#include "../diagramcontext.h"
|
#include "../diagramcontext.h"
|
||||||
#include "pugixml.hpp"
|
#include "pugixml/src/pugixml.hpp"
|
||||||
|
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|||||||
@@ -87,7 +87,11 @@ void ElementsTreeView::startElementDrag(const ElementsLocation &location)
|
|||||||
{
|
{
|
||||||
if (! location.exist()) return;
|
if (! location.exist()) return;
|
||||||
|
|
||||||
auto drag = new QDrag{this};
|
#if QT_VERSION < QT_VERSION_CHECK(6, 2, 0)
|
||||||
|
QDrag* drag = new QDrag(this);
|
||||||
|
#else
|
||||||
|
QScopedPointer<QDrag> drag(new QDrag(this));
|
||||||
|
#endif
|
||||||
|
|
||||||
QString location_str = location.toString();
|
QString location_str = location.toString();
|
||||||
QMimeData *mime_data = new QMimeData();
|
QMimeData *mime_data = new QMimeData();
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ void FileElementCollectionItem::setUpIcon()
|
|||||||
setIcon(QET::Icons::Folder);
|
setIcon(QET::Icons::Folder);
|
||||||
} else {
|
} else {
|
||||||
if (m_path.endsWith(".qetmak")) {
|
if (m_path.endsWith(".qetmak")) {
|
||||||
setIcon(QET::Icons::PartRectangle);
|
setIcon(QIcon());
|
||||||
} else {
|
} else {
|
||||||
ElementsLocation loc(collectionPath());
|
ElementsLocation loc(collectionPath());
|
||||||
setIcon(loc.icon());
|
setIcon(loc.icon());
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
#ifndef NAMES_LIST_H
|
#ifndef NAMES_LIST_H
|
||||||
#define NAMES_LIST_H
|
#define NAMES_LIST_H
|
||||||
#include "pugixml.hpp"
|
#include "pugixml/src/pugixml.hpp"
|
||||||
|
|
||||||
#include <QtXml>
|
#include <QtXml>
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
#include "terminalstripdrawer.h"
|
#include "terminalstripdrawer.h"
|
||||||
|
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QHash>
|
|
||||||
|
|
||||||
namespace TerminalStripDrawer {
|
namespace TerminalStripDrawer {
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,12 @@ bool PhysicalTerminal::setLevelOf(const QSharedPointer<RealTerminal> &terminal,
|
|||||||
const int i = m_real_terminal.indexOf(terminal);
|
const int i = m_real_terminal.indexOf(terminal);
|
||||||
if (i >= 0)
|
if (i >= 0)
|
||||||
{
|
{
|
||||||
m_real_terminal.swapItemsAt(i, std::min(static_cast<qsizetype>(level), m_real_terminal.size()-1));
|
#if QT_VERSION >= QT_VERSION_CHECK(5,14,0)
|
||||||
|
m_real_terminal.swapItemsAt(i, std::min(level, m_real_terminal.size()-1));
|
||||||
|
#else
|
||||||
|
auto j = std::min(level, m_real_terminal.size()-1);
|
||||||
|
std::swap(m_real_terminal.begin()[i], m_real_terminal.begin()[j]);
|
||||||
|
#endif
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -64,8 +64,11 @@ bool TerminalStripData::fromXml(const QDomElement &xml_element)
|
|||||||
"due to wrong tag name. Expected " << this->xmlTagName() << " used " << xml_element.tagName();
|
"due to wrong tag name. Expected " << this->xmlTagName() << " used " << xml_element.tagName();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
||||||
m_uuid = QUuid::fromString(xml_element.attribute(QStringLiteral("uuid")));
|
m_uuid = QUuid::fromString(xml_element.attribute(QStringLiteral("uuid")));
|
||||||
|
#else
|
||||||
|
m_uuid = QUuid(xml_element.attribute(QStringLiteral("uuid")));
|
||||||
|
#endif
|
||||||
|
|
||||||
for (auto &xml_info :
|
for (auto &xml_info :
|
||||||
QETXML::findInDomElement(xml_element.firstChildElement(QStringLiteral("informations")),
|
QETXML::findInDomElement(xml_element.firstChildElement(QStringLiteral("informations")),
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ TerminalStripTreeDockWidget::TerminalStripTreeDockWidget(QETProject *project, QW
|
|||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
setProject(project);
|
setProject(project);
|
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
|
||||||
ui->m_tree_view->expandRecursively(ui->m_tree_view->rootIndex());
|
ui->m_tree_view->expandRecursively(ui->m_tree_view->rootIndex());
|
||||||
|
#else
|
||||||
|
ui->m_tree_view->expandAll();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
TerminalStripTreeDockWidget::~TerminalStripTreeDockWidget()
|
TerminalStripTreeDockWidget::~TerminalStripTreeDockWidget()
|
||||||
@@ -89,7 +93,11 @@ void TerminalStripTreeDockWidget::reload()
|
|||||||
|
|
||||||
buildTree();
|
buildTree();
|
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
|
||||||
ui->m_tree_view->expandRecursively(ui->m_tree_view->rootIndex());
|
ui->m_tree_view->expandRecursively(ui->m_tree_view->rootIndex());
|
||||||
|
#else
|
||||||
|
ui->m_tree_view->expandAll();
|
||||||
|
#endif
|
||||||
|
|
||||||
//Reselect the tree widget item of the current edited strip
|
//Reselect the tree widget item of the current edited strip
|
||||||
auto item = m_item_strip_H.key(current_);
|
auto item = m_item_strip_H.key(current_);
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ BorderTitleBlock::BorderTitleBlock(QObject *parent) :
|
|||||||
m_titleblock_template_renderer = new TitleBlockTemplateRenderer(this);
|
m_titleblock_template_renderer = new TitleBlockTemplateRenderer(this);
|
||||||
m_titleblock_template_renderer -> setTitleBlockTemplate(QETApp::defaultTitleBlockTemplate());
|
m_titleblock_template_renderer -> setTitleBlockTemplate(QETApp::defaultTitleBlockTemplate());
|
||||||
|
|
||||||
|
// disable the QPicture-based cache from Qt 4.8 to avoid rendering errors and crashes
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(4, 8, 0) // ### Qt 6: remove
|
||||||
|
#else
|
||||||
m_titleblock_template_renderer -> setUseCache(false);
|
m_titleblock_template_renderer -> setUseCache(false);
|
||||||
|
#endif
|
||||||
|
|
||||||
// dimensions par defaut du schema
|
// dimensions par defaut du schema
|
||||||
importBorder(BorderProperties());
|
importBorder(BorderProperties());
|
||||||
|
|||||||
@@ -0,0 +1,825 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2006-2025 The QElectroTech Team
|
||||||
|
This file is part of QElectroTech.
|
||||||
|
|
||||||
|
QElectroTech is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
QElectroTech is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#include "cli_export.h"
|
||||||
|
|
||||||
|
#include "bordertitleblock.h"
|
||||||
|
#include "conductornumexport.h"
|
||||||
|
#include "conductorproperties.h"
|
||||||
|
#include "dataBase/projectdatabase.h"
|
||||||
|
#include "diagram.h"
|
||||||
|
#include "diagramcontext.h"
|
||||||
|
#include "pdf_links.h"
|
||||||
|
#include "qetgraphicsitem/conductor.h"
|
||||||
|
#include "qetgraphicsitem/element.h"
|
||||||
|
#include "qetgraphicsitem/terminal.h"
|
||||||
|
#include "qetproject.h"
|
||||||
|
#include "titleblockproperties.h"
|
||||||
|
#include "wiringlistexport.h"
|
||||||
|
|
||||||
|
// Private Qt PDF engine for drawHyperlink() — see pdf_links / projectprintwindow.
|
||||||
|
#include <private/qpdf_p.h>
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QDirIterator>
|
||||||
|
#include <QDomDocument>
|
||||||
|
#include <QDate>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QPageLayout>
|
||||||
|
#include <QPair>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPdfWriter>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QSqlError>
|
||||||
|
#include <QSqlQuery>
|
||||||
|
#include <QSvgGenerator>
|
||||||
|
#include <QTextStream>
|
||||||
|
#include <QTransform>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
QTextStream out(stdout);
|
||||||
|
QTextStream err(stderr);
|
||||||
|
|
||||||
|
/// All CLI option flags, mapped to a short format name.
|
||||||
|
const QHash<QString, QString> &exportFlags()
|
||||||
|
{
|
||||||
|
static const QHash<QString, QString> flags {
|
||||||
|
{"--export-pdf", "pdf"},
|
||||||
|
{"--export-png", "png"},
|
||||||
|
{"--export-svg", "svg"},
|
||||||
|
{"--export-cables", "cables"},
|
||||||
|
{"--export-wires", "wires"},
|
||||||
|
{"--export-bom", "bom"},
|
||||||
|
{"--export-nets", "nets"},
|
||||||
|
{"--export-links", "links"},
|
||||||
|
{"--info", "info"},
|
||||||
|
{"--check-elements", "check"},
|
||||||
|
{"--resave", "resave"},
|
||||||
|
{"--set-titleblock", "settb"},
|
||||||
|
};
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Device tag of an element ("K1", "Q55"), falling back to its name.
|
||||||
|
QString elementLabel(Element *element)
|
||||||
|
{
|
||||||
|
const QString label = element->elementInformations()["label"].toString();
|
||||||
|
return label.isEmpty() ? element->name() : label;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pixel rect of a diagram's border + title block (the printable page area).
|
||||||
|
QRect diagramRect(Diagram *diagram)
|
||||||
|
{
|
||||||
|
QRectF r = diagram->border_and_titleblock.borderAndTitleBlockRect();
|
||||||
|
r.adjust(0, 0, 1, 1); // include the 1px border line
|
||||||
|
return r.toAlignedRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A filesystem-safe per-diagram file stem: "01_Title".
|
||||||
|
QString diagramStem(Diagram *diagram, int index)
|
||||||
|
{
|
||||||
|
QString title = diagram->title();
|
||||||
|
title.replace(QRegularExpression("[^\\w \\-]"), "_");
|
||||||
|
title = title.simplified();
|
||||||
|
if (title.isEmpty())
|
||||||
|
title = "diagram";
|
||||||
|
return QStringLiteral("%1_%2")
|
||||||
|
.arg(index, 2, 10, QChar('0'))
|
||||||
|
.arg(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render @p diagram into @p painter, fitting @p target to the page rect.
|
||||||
|
void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target)
|
||||||
|
{
|
||||||
|
const QRect source = diagramRect(diagram);
|
||||||
|
// Export without the editor grid: drawBackground() only paints it when
|
||||||
|
// draw_grid_ is set (default true), so toggle it off around the render
|
||||||
|
// and restore it afterwards.
|
||||||
|
const bool was_drawing_grid = diagram->displayGrid();
|
||||||
|
diagram->setDisplayGrid(false);
|
||||||
|
diagram->render(&painter, target, source, Qt::KeepAspectRatio);
|
||||||
|
diagram->setDisplayGrid(was_drawing_grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
int exportPdf(QETProject &project, const QString &output)
|
||||||
|
{
|
||||||
|
const QList<Diagram *> diagrams = project.diagrams();
|
||||||
|
if (diagrams.isEmpty()) {
|
||||||
|
err << "No diagrams to export.\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page numbers (1-based) for cross-reference hyperlink targets: each
|
||||||
|
// diagram is exactly one page in the CLI export (no tiling).
|
||||||
|
QMap<Diagram *, int> pageMap;
|
||||||
|
for (int i = 0; i < diagrams.size(); ++i)
|
||||||
|
pageMap.insert(diagrams.at(i), i + 1);
|
||||||
|
|
||||||
|
QPdfWriter writer(output);
|
||||||
|
writer.setCreator("QElectroTech");
|
||||||
|
writer.setResolution(96);
|
||||||
|
|
||||||
|
QPainter painter;
|
||||||
|
bool first = true;
|
||||||
|
for (Diagram *diagram : diagrams) {
|
||||||
|
const QRect r = diagramRect(diagram);
|
||||||
|
// Match the page to the diagram (in points: 1px @ 96dpi = 0.75pt).
|
||||||
|
const QPageSize page(QSizeF(r.width() * 72.0 / 96.0,
|
||||||
|
r.height() * 72.0 / 96.0),
|
||||||
|
QPageSize::Point);
|
||||||
|
writer.setPageSize(page);
|
||||||
|
writer.setPageMargins(QMarginsF(0, 0, 0, 0));
|
||||||
|
|
||||||
|
if (first) {
|
||||||
|
if (!painter.begin(&writer)) {
|
||||||
|
err << "Cannot open '" << output << "' for writing.\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
writer.newPage();
|
||||||
|
}
|
||||||
|
const QRectF target(0, 0,
|
||||||
|
writer.width(), writer.height());
|
||||||
|
renderDiagram(diagram, painter, target);
|
||||||
|
|
||||||
|
// Inject clickable cross-reference / folio-report hyperlinks for this
|
||||||
|
// page. The geometry is rebuilt from the QPdfWriter (not a QPrinter):
|
||||||
|
// render() anchors the diagram top-left with KeepAspectRatio, and the
|
||||||
|
// page is sized to the diagram so the scale is ~1.
|
||||||
|
if (auto *engine = dynamic_cast<QPdfEngine *>(painter.paintEngine())) {
|
||||||
|
const QRectF source(r);
|
||||||
|
const qreal s = qMin(target.width() / source.width(),
|
||||||
|
target.height() / source.height());
|
||||||
|
QTransform fit;
|
||||||
|
fit.translate(target.x(), target.y());
|
||||||
|
fit.scale(s, s);
|
||||||
|
fit.translate(-source.x(), -source.y());
|
||||||
|
|
||||||
|
// Device pixels -> PDF points, replicating the engine's page matrix
|
||||||
|
// (72/resolution scale + Y flip; zero margins -> no paint offset).
|
||||||
|
const qreal pt_scale = 72.0 / writer.resolution();
|
||||||
|
const qreal fullH_pt = writer.pageLayout().fullRectPoints().height();
|
||||||
|
const bool fullPageMode =
|
||||||
|
(writer.pageLayout().mode() == QPageLayout::FullPageMode);
|
||||||
|
const QRect paintPx =
|
||||||
|
writer.pageLayout().paintRectPixels(writer.resolution());
|
||||||
|
|
||||||
|
PdfLinks::PageGeometry geom;
|
||||||
|
geom.sceneToDevice = fit;
|
||||||
|
geom.target = target;
|
||||||
|
geom.pageBounds = QRectF(0, 0, target.width(), target.height());
|
||||||
|
geom.devToPdf = [=](const QPointF &d) -> QPointF {
|
||||||
|
qreal dx = d.x(), dy = d.y();
|
||||||
|
if (!fullPageMode) { dx += paintPx.left(); dy += paintPx.top(); }
|
||||||
|
return QPointF(pt_scale * dx, fullH_pt - pt_scale * dy);
|
||||||
|
};
|
||||||
|
geom.sourceRectOf = [](Diagram *dg) {
|
||||||
|
return QRectF(diagramRect(dg));
|
||||||
|
};
|
||||||
|
PdfLinks::injectCrossRefLinks(engine, diagram, geom, pageMap, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
painter.end();
|
||||||
|
|
||||||
|
// Rewrite the URI link annotations into native internal GoTo actions, so
|
||||||
|
// the cross-references jump inside the document in any PDF viewer.
|
||||||
|
PdfLinks::convertUriToGoTo(output);
|
||||||
|
|
||||||
|
out << "Exported " << diagrams.size() << " page(s) -> " << output << "\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int exportImages(QETProject &project, const QString &format,
|
||||||
|
const QString &out_dir)
|
||||||
|
{
|
||||||
|
const QList<Diagram *> diagrams = project.diagrams();
|
||||||
|
if (diagrams.isEmpty()) {
|
||||||
|
err << "No diagrams to export.\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
QDir().mkpath(out_dir);
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
for (Diagram *diagram : diagrams) {
|
||||||
|
++index;
|
||||||
|
const QRect r = diagramRect(diagram);
|
||||||
|
const QString path = QDir(out_dir).filePath(
|
||||||
|
diagramStem(diagram, index) + "." + format);
|
||||||
|
|
||||||
|
if (format == "svg") {
|
||||||
|
QSvgGenerator gen;
|
||||||
|
gen.setFileName(path);
|
||||||
|
gen.setSize(r.size());
|
||||||
|
gen.setViewBox(QRect(0, 0, r.width(), r.height()));
|
||||||
|
gen.setTitle(diagram->title());
|
||||||
|
QPainter painter(&gen);
|
||||||
|
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()));
|
||||||
|
painter.end();
|
||||||
|
} else { // png
|
||||||
|
QImage image(r.size(), QImage::Format_ARGB32);
|
||||||
|
image.fill(Qt::white);
|
||||||
|
QPainter painter(&image);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()));
|
||||||
|
painter.end();
|
||||||
|
if (!image.save(path)) {
|
||||||
|
err << "Failed to write '" << path << "'.\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out << " " << path << "\n";
|
||||||
|
}
|
||||||
|
out << "Exported " << diagrams.size() << " diagram(s) -> " << out_dir << "\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int exportCsv(QETProject &project, const QString &format, const QString &output)
|
||||||
|
{
|
||||||
|
QString csv;
|
||||||
|
if (format == "cables") {
|
||||||
|
WiringListExport wle(&project, nullptr);
|
||||||
|
csv = wle.toCsvString();
|
||||||
|
} else { // wires
|
||||||
|
ConductorNumExport cne(&project, nullptr);
|
||||||
|
csv = cne.wiresNum();
|
||||||
|
}
|
||||||
|
if (csv.isEmpty()) {
|
||||||
|
err << "Nothing to export (empty list).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile file(output);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||||
|
err << "Cannot open '" << output << "' for writing.\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
QTextStream fout(&file);
|
||||||
|
fout << csv;
|
||||||
|
file.close();
|
||||||
|
out << "Exported " << format << " list -> " << output << "\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Quote a field for CSV output (RFC-4180 style, ';' delimiter).
|
||||||
|
QString csvField(const QString &value)
|
||||||
|
{
|
||||||
|
if (value.contains(';') || value.contains('"')
|
||||||
|
|| value.contains('\n') || value.contains('\r')) {
|
||||||
|
QString v = value;
|
||||||
|
v.replace('"', "\"\"");
|
||||||
|
return '"' % v % '"';
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bill of materials: one row per element, key component-data fields.
|
||||||
|
/// Pulls from QET's own project database (the same source as the GUI BOM
|
||||||
|
/// export), so the output matches what the editor produces.
|
||||||
|
int exportBom(QETProject &project, const QString &output)
|
||||||
|
{
|
||||||
|
// The project database is built lazily; force a (re)build before querying.
|
||||||
|
project.dataBase()->updateDB();
|
||||||
|
|
||||||
|
static const QStringList columns {
|
||||||
|
"label", "designation", "manufacturer", "manufacturer_reference",
|
||||||
|
"quantity", "location", "function", "title", "folio"
|
||||||
|
};
|
||||||
|
|
||||||
|
QSqlQuery query = project.dataBase()->newQuery(
|
||||||
|
"SELECT " % columns.join(", ") %
|
||||||
|
" FROM element_nomenclature_view ORDER BY label");
|
||||||
|
if (!query.exec()) {
|
||||||
|
err << "BOM query failed: " << query.lastError().text() << "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString csv = columns.join(";") % "\n";
|
||||||
|
int rows = 0;
|
||||||
|
while (query.next()) {
|
||||||
|
QStringList values;
|
||||||
|
for (int i = 0; i < columns.size(); ++i)
|
||||||
|
values << csvField(query.value(i).toString());
|
||||||
|
csv += values.join(";") % "\n";
|
||||||
|
++rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile file(output);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||||
|
err << "Cannot open '" << output << "' for writing.\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
QTextStream fout(&file);
|
||||||
|
fout << csv;
|
||||||
|
file.close();
|
||||||
|
out << "Exported " << rows << " component(s) -> " << output << "\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Count terminals on @p element that no conductor connects to.
|
||||||
|
int freeTerminals(Element *element)
|
||||||
|
{
|
||||||
|
int free = 0;
|
||||||
|
const QList<Terminal *> terminals = element->terminals();
|
||||||
|
for (Terminal *t : terminals)
|
||||||
|
if (t->conductorsCount() == 0)
|
||||||
|
++free;
|
||||||
|
return free;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Structural ground-truth dump of a project, as JSON, to stdout (or a file).
|
||||||
|
/// Uses QET's own loaded model, so it reports what the editor actually sees:
|
||||||
|
/// per-page element / conductor counts and unconnected terminals.
|
||||||
|
int exportInfo(QETProject &project, const QString &output)
|
||||||
|
{
|
||||||
|
const QList<Diagram *> diagrams = project.diagrams();
|
||||||
|
|
||||||
|
int total_elements = 0, total_conductors = 0, total_free = 0;
|
||||||
|
QJsonArray pages;
|
||||||
|
int index = 0;
|
||||||
|
for (Diagram *diagram : diagrams) {
|
||||||
|
++index;
|
||||||
|
const QList<Element *> elements = diagram->elements();
|
||||||
|
const int conductors = diagram->conductors().size();
|
||||||
|
int page_free = 0;
|
||||||
|
for (Element *e : elements)
|
||||||
|
page_free += freeTerminals(e);
|
||||||
|
|
||||||
|
const QRect r = diagramRect(diagram);
|
||||||
|
QJsonObject page;
|
||||||
|
page["index"] = index;
|
||||||
|
page["title"] = diagram->title();
|
||||||
|
page["folio"] = QStringLiteral("%1 of %2")
|
||||||
|
.arg(index).arg(diagrams.size());
|
||||||
|
page["width_px"] = r.width();
|
||||||
|
page["height_px"] = r.height();
|
||||||
|
page["elements"] = elements.size();
|
||||||
|
page["conductors"] = conductors;
|
||||||
|
page["free_terminals"] = page_free;
|
||||||
|
pages.append(page);
|
||||||
|
|
||||||
|
total_elements += elements.size();
|
||||||
|
total_conductors += conductors;
|
||||||
|
total_free += page_free;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject root;
|
||||||
|
root["project"] = project.title();
|
||||||
|
root["diagrams"] = diagrams.size();
|
||||||
|
root["elements"] = total_elements;
|
||||||
|
root["conductors"] = total_conductors;
|
||||||
|
root["free_terminals"] = total_free;
|
||||||
|
root["pages"] = pages;
|
||||||
|
|
||||||
|
const QByteArray json =
|
||||||
|
QJsonDocument(root).toJson(QJsonDocument::Indented);
|
||||||
|
|
||||||
|
if (output.isEmpty()) {
|
||||||
|
out << QString::fromUtf8(json);
|
||||||
|
} else {
|
||||||
|
QFile file(output);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||||
|
err << "Cannot open '" << output << "' for writing.\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
file.write(json);
|
||||||
|
file.close();
|
||||||
|
out << "Wrote project info -> " << output << "\n";
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate one .elmt file against QET's element schema.
|
||||||
|
/// @return 0 = OK, 1 = warning (loads but suspicious), 2 = failure.
|
||||||
|
int checkOneElement(const QString &path)
|
||||||
|
{
|
||||||
|
QFile file(path);
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
out << "FAIL " << path << " (cannot open)\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
QDomDocument doc;
|
||||||
|
QString error;
|
||||||
|
int line = 0;
|
||||||
|
if (!doc.setContent(&file, &error, &line)) {
|
||||||
|
file.close();
|
||||||
|
out << "FAIL " << path << " (XML error line "
|
||||||
|
<< line << ": " << error << ")\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
const QDomElement root = doc.documentElement();
|
||||||
|
if (root.tagName() != "definition" || root.attribute("type") != "element") {
|
||||||
|
out << "FAIL " << path << " (root is not <definition type=\"element\">)\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool w_ok = false, h_ok = false;
|
||||||
|
const double w = root.attribute("width").toDouble(&w_ok);
|
||||||
|
const double h = root.attribute("height").toDouble(&h_ok);
|
||||||
|
if (!w_ok || !h_ok || w == 0 || h == 0) {
|
||||||
|
out << "FAIL " << path << " (missing/zero bounding box "
|
||||||
|
<< root.attribute("width") << "x"
|
||||||
|
<< root.attribute("height") << ")\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int terminals = root.elementsByTagName("terminal").count();
|
||||||
|
|
||||||
|
// Negative dimensions are malformed but QET still loads them; surface as a
|
||||||
|
// warning rather than a failure so this agrees with QET's own loader.
|
||||||
|
if (w < 0 || h < 0) {
|
||||||
|
out << "WARN " << path << " (negative bounding box "
|
||||||
|
<< w << "x" << h << ", " << terminals << " terminals)\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (terminals == 0) {
|
||||||
|
out << "WARN " << path << " (loads, but 0 terminals)\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
out << "OK " << path << " (" << terminals << " terminals)\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate a single .elmt file or every .elmt under a directory.
|
||||||
|
int checkElements(const QString &path)
|
||||||
|
{
|
||||||
|
QStringList files;
|
||||||
|
const QFileInfo info(path);
|
||||||
|
if (info.isDir()) {
|
||||||
|
QDirIterator it(path, {"*.elmt"}, QDir::Files,
|
||||||
|
QDirIterator::Subdirectories);
|
||||||
|
while (it.hasNext())
|
||||||
|
files << it.next();
|
||||||
|
files.sort();
|
||||||
|
} else if (info.isFile()) {
|
||||||
|
files << path;
|
||||||
|
} else {
|
||||||
|
err << "Not found: " << path << "\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.isEmpty()) {
|
||||||
|
err << "No .elmt files found under: " << path << "\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
int warnings = 0, failures = 0;
|
||||||
|
for (const QString &f : files) {
|
||||||
|
const int r = checkOneElement(f);
|
||||||
|
if (r == 1) ++warnings;
|
||||||
|
else if (r == 2) ++failures;
|
||||||
|
}
|
||||||
|
out << files.size() << " file(s), " << warnings
|
||||||
|
<< " warning(s), " << failures << " failure(s)\n";
|
||||||
|
return failures > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map every element in the project to its 1-based folio (page) position.
|
||||||
|
QHash<Element *, int> folioIndex(QETProject &project)
|
||||||
|
{
|
||||||
|
QHash<Element *, int> folio;
|
||||||
|
int index = 0;
|
||||||
|
const QList<Diagram *> diagrams = project.diagrams();
|
||||||
|
for (Diagram *diagram : diagrams) {
|
||||||
|
++index;
|
||||||
|
const QList<Element *> elements = diagram->elements();
|
||||||
|
for (Element *e : elements)
|
||||||
|
folio.insert(e, index);
|
||||||
|
}
|
||||||
|
return folio;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Electrical nets: groups of terminals joined into one potential.
|
||||||
|
/// Walks QET's own potential graph, so each net is a connected component
|
||||||
|
/// of terminals across all folios. The ground truth for connectivity.
|
||||||
|
int exportNets(QETProject &project, const QString &output)
|
||||||
|
{
|
||||||
|
const QHash<Element *, int> folio = folioIndex(project);
|
||||||
|
|
||||||
|
QList<Conductor *> all_conductors;
|
||||||
|
const QList<Diagram *> diagrams = project.diagrams();
|
||||||
|
for (Diagram *diagram : diagrams)
|
||||||
|
all_conductors << diagram->conductors();
|
||||||
|
|
||||||
|
QSet<Conductor *> visited;
|
||||||
|
QJsonArray nets;
|
||||||
|
int net_no = 0;
|
||||||
|
for (Conductor *c : all_conductors) {
|
||||||
|
if (visited.contains(c))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// The whole potential this conductor belongs to. relatedPotential-
|
||||||
|
// Conductors() also fills t_list with every terminal in the net
|
||||||
|
// (following folio reports and terminal blocks too).
|
||||||
|
QList<Terminal *> t_list;
|
||||||
|
QSet<Conductor *> group = c->relatedPotentialConductors(true, &t_list);
|
||||||
|
group.insert(c);
|
||||||
|
for (Conductor *g : group)
|
||||||
|
visited.insert(g);
|
||||||
|
if (c->terminal1) t_list << c->terminal1;
|
||||||
|
if (c->terminal2) t_list << c->terminal2;
|
||||||
|
|
||||||
|
// Wire number: smallest non-empty conductor text (deterministic).
|
||||||
|
QStringList wire_nos;
|
||||||
|
for (Conductor *g : group)
|
||||||
|
if (!g->properties().text.isEmpty())
|
||||||
|
wire_nos << g->properties().text;
|
||||||
|
wire_nos.sort();
|
||||||
|
|
||||||
|
++net_no;
|
||||||
|
QJsonArray terminals;
|
||||||
|
QSet<Terminal *> seen;
|
||||||
|
for (Terminal *t : t_list) {
|
||||||
|
if (!t || seen.contains(t))
|
||||||
|
continue;
|
||||||
|
seen.insert(t);
|
||||||
|
Element *pe = t->parentElement();
|
||||||
|
QJsonObject to;
|
||||||
|
to["element"] = pe ? elementLabel(pe) : QString();
|
||||||
|
to["terminal"] = t->name();
|
||||||
|
to["folio"] = pe ? folio.value(pe, 0) : 0;
|
||||||
|
terminals.append(to);
|
||||||
|
}
|
||||||
|
QJsonObject net;
|
||||||
|
net["net"] = net_no;
|
||||||
|
net["wire_no"] = wire_nos.value(0);
|
||||||
|
net["terminals"] = terminals;
|
||||||
|
nets.append(net);
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject root;
|
||||||
|
root["project"] = project.title();
|
||||||
|
root["nets"] = nets.size();
|
||||||
|
root["list"] = nets;
|
||||||
|
|
||||||
|
QFile file(output);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||||
|
err << "Cannot open '" << output << "' for writing.\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
file.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
|
||||||
|
file.close();
|
||||||
|
out << "Exported " << nets.size() << " net(s) -> " << output << "\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cross-references: each linkable element (coil / contact / report) and the
|
||||||
|
/// elements it links to, flagging masters/slaves with no link as unresolved.
|
||||||
|
int exportLinks(QETProject &project, const QString &output)
|
||||||
|
{
|
||||||
|
const QHash<Element *, int> folio = folioIndex(project);
|
||||||
|
|
||||||
|
QString csv("element;link_type;linked_to;folio;status\n");
|
||||||
|
int linkable = 0, unresolved = 0;
|
||||||
|
|
||||||
|
const QList<Diagram *> diagrams = project.diagrams();
|
||||||
|
for (Diagram *diagram : diagrams) {
|
||||||
|
const QList<Element *> elements = diagram->elements();
|
||||||
|
for (Element *e : elements) {
|
||||||
|
if (e->linkType() == Element::Simple)
|
||||||
|
continue;
|
||||||
|
++linkable;
|
||||||
|
|
||||||
|
const QList<Element *> linked = e->linkedElements();
|
||||||
|
QStringList names;
|
||||||
|
for (Element *le : linked)
|
||||||
|
names << elementLabel(le) % "(f"
|
||||||
|
% QString::number(folio.value(le, 0)) % ")";
|
||||||
|
|
||||||
|
QString status = "linked";
|
||||||
|
if ((e->linkType() == Element::Master
|
||||||
|
|| e->linkType() == Element::Slave)
|
||||||
|
&& linked.isEmpty()) {
|
||||||
|
status = "UNRESOLVED";
|
||||||
|
++unresolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
csv += csvField(elementLabel(e)) % ";"
|
||||||
|
% e->linkTypeToString() % ";"
|
||||||
|
% csvField(names.join(", ")) % ";"
|
||||||
|
% QString::number(folio.value(e, 0)) % ";"
|
||||||
|
% status % "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile file(output);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||||
|
err << "Cannot open '" << output << "' for writing.\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
QTextStream fout(&file);
|
||||||
|
fout << csv;
|
||||||
|
file.close();
|
||||||
|
out << "Exported " << linkable << " linkable element(s), "
|
||||||
|
<< unresolved << " unresolved -> " << output << "\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Round-trip: load the project and write its XML back out, so an external
|
||||||
|
/// diff can reveal markup QET silently normalises (tolerated-but-invalid XML).
|
||||||
|
int resaveProject(QETProject &project, const QString &output)
|
||||||
|
{
|
||||||
|
const QDomDocument doc = project.toXml();
|
||||||
|
QFile file(output);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||||
|
err << "Cannot open '" << output << "' for writing.\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
QTextStream fout(&file);
|
||||||
|
fout << doc.toString(4);
|
||||||
|
file.close();
|
||||||
|
out << "Re-saved project -> " << output << "\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stamp title-block fields onto every folio (and the project default), then
|
||||||
|
/// save. Each assignment is "key=value". Standard keys map to the documented
|
||||||
|
/// title-block fields; "date=today" uses the current date; any other key is
|
||||||
|
/// stored as a custom title-block field. Aimed at CI/revision workflows
|
||||||
|
/// (e.g. set revision + date before exporting a new revision).
|
||||||
|
int setTitleBlock(QETProject &project, const QString &output,
|
||||||
|
const QStringList &assignments)
|
||||||
|
{
|
||||||
|
if (assignments.isEmpty()) {
|
||||||
|
err << "No field assignments given (expected key=value).\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse "key=value" assignments up front so a bad one fails before writing.
|
||||||
|
QList<QPair<QString, QString>> fields;
|
||||||
|
for (const QString &a : assignments) {
|
||||||
|
const int eq = a.indexOf('=');
|
||||||
|
if (eq <= 0) {
|
||||||
|
err << "Bad assignment '" << a << "' (expected key=value).\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
const QString key = a.left(eq);
|
||||||
|
const QString val = a.mid(eq + 1);
|
||||||
|
if (key.compare("date", Qt::CaseInsensitive) == 0
|
||||||
|
&& val.compare("today", Qt::CaseInsensitive) != 0
|
||||||
|
&& !QDate::fromString(val, Qt::ISODate).isValid()) {
|
||||||
|
err << "Bad date '" << val << "' (expected YYYY-MM-DD or 'today').\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
fields << qMakePair(key, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto apply = [&](TitleBlockProperties &p) {
|
||||||
|
for (const auto &f : fields) {
|
||||||
|
const QString k = f.first.toLower();
|
||||||
|
const QString &v = f.second;
|
||||||
|
if (k == "title") p.title = v;
|
||||||
|
else if (k == "author") p.author = v;
|
||||||
|
else if (k == "filename") p.filename = v;
|
||||||
|
else if (k == "plant") p.plant = v;
|
||||||
|
else if (k == "location") p.locmach = v;
|
||||||
|
else if (k == "revision") p.indexrev = v;
|
||||||
|
else if (k == "version") p.version = v;
|
||||||
|
else if (k == "date") {
|
||||||
|
p.date = (v.compare("today", Qt::CaseInsensitive) == 0)
|
||||||
|
? QDate::currentDate()
|
||||||
|
: QDate::fromString(v, Qt::ISODate);
|
||||||
|
// An explicit date is only honoured when the folio is in
|
||||||
|
// "use the date value" mode (not "now"/"null").
|
||||||
|
p.useDate = TitleBlockProperties::UseDateValue;
|
||||||
|
}
|
||||||
|
else // unknown key -> custom title-block field
|
||||||
|
p.context.addValue(f.first, v);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Project default (the template applied to new folios).
|
||||||
|
TitleBlockProperties def = project.defaultTitleBlockProperties();
|
||||||
|
apply(def);
|
||||||
|
project.setDefaultTitleBlockProperties(def);
|
||||||
|
|
||||||
|
// Every existing folio's own title block.
|
||||||
|
int folios = 0;
|
||||||
|
const QList<Diagram *> diagrams = project.diagrams();
|
||||||
|
for (Diagram *diagram : diagrams) {
|
||||||
|
TitleBlockProperties p =
|
||||||
|
diagram->border_and_titleblock.exportTitleBlock();
|
||||||
|
apply(p);
|
||||||
|
diagram->border_and_titleblock.importTitleBlock(p);
|
||||||
|
++folios;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QDomDocument doc = project.toXml();
|
||||||
|
QFile file(output);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||||
|
err << "Cannot open '" << output << "' for writing.\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
QTextStream fout(&file);
|
||||||
|
fout << doc.toString(4);
|
||||||
|
file.close();
|
||||||
|
out << "Stamped " << fields.size() << " field(s) on "
|
||||||
|
<< folios << " folio(s) -> " << output << "\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
|
namespace CLIExport {
|
||||||
|
|
||||||
|
bool isExportRequest(const QStringList &args)
|
||||||
|
{
|
||||||
|
for (const QString &a : args)
|
||||||
|
if (exportFlags().contains(a))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int run(const QStringList &args)
|
||||||
|
{
|
||||||
|
QString flag;
|
||||||
|
QStringList rest;
|
||||||
|
for (int i = 0; i < args.size(); ++i) {
|
||||||
|
if (exportFlags().contains(args.at(i))) {
|
||||||
|
flag = args.at(i);
|
||||||
|
for (int j = i + 1; j < args.size(); ++j)
|
||||||
|
rest << args.at(j);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const QString format = exportFlags().value(flag);
|
||||||
|
|
||||||
|
// --check-elements operates on an element file/directory, not a project.
|
||||||
|
if (format == "check") {
|
||||||
|
if (rest.isEmpty()) {
|
||||||
|
err << "Usage: qelectrotech --check-elements "
|
||||||
|
"<element.elmt | directory>\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return checkElements(rest.at(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString input = rest.value(0);
|
||||||
|
if (input.isEmpty()) {
|
||||||
|
err << "Usage: qelectrotech " << flag << " <project.qet> <output>\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (!QFileInfo::exists(input)) {
|
||||||
|
err << "Project not found: " << input << "\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
QETProject project(input);
|
||||||
|
if (project.state() != QETProject::Ok) {
|
||||||
|
err << "Failed to open project: " << input
|
||||||
|
<< " (state " << project.state() << ")\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --info writes JSON to stdout, or to an optional output file.
|
||||||
|
if (format == "info")
|
||||||
|
return exportInfo(project, rest.value(1));
|
||||||
|
|
||||||
|
const QString output = rest.value(1);
|
||||||
|
if (output.isEmpty()) {
|
||||||
|
err << "Usage: qelectrotech " << flag
|
||||||
|
<< " <project.qet> <output>\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (format == "pdf")
|
||||||
|
return exportPdf(project, output);
|
||||||
|
if (format == "cables" || format == "wires")
|
||||||
|
return exportCsv(project, format, output);
|
||||||
|
if (format == "bom")
|
||||||
|
return exportBom(project, output);
|
||||||
|
if (format == "nets")
|
||||||
|
return exportNets(project, output);
|
||||||
|
if (format == "links")
|
||||||
|
return exportLinks(project, output);
|
||||||
|
if (format == "resave")
|
||||||
|
return resaveProject(project, output);
|
||||||
|
if (format == "settb")
|
||||||
|
return setTitleBlock(project, output, rest.mid(2));
|
||||||
|
return exportImages(project, format, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace CLIExport
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2006-2025 The QElectroTech Team
|
||||||
|
This file is part of QElectroTech.
|
||||||
|
|
||||||
|
QElectroTech is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
QElectroTech is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#ifndef CLI_EXPORT_H
|
||||||
|
#define CLI_EXPORT_H
|
||||||
|
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Headless command-line export.
|
||||||
|
|
||||||
|
Implements the long-requested batch/headless export
|
||||||
|
(qelectrotech.org bugtracker #171, GitHub #309): render a project's
|
||||||
|
diagrams to files without opening the GUI.
|
||||||
|
|
||||||
|
Detected and handled in main() before the GUI is created.
|
||||||
|
*/
|
||||||
|
namespace CLIExport {
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief True if @p args request a CLI export
|
||||||
|
(i.e. contain one of the export options).
|
||||||
|
*/
|
||||||
|
bool isExportRequest(const QStringList &args);
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Run the CLI export described by @p args.
|
||||||
|
@return process exit code (0 on success).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
qelectrotech --export-pdf <project.qet> <output.pdf>
|
||||||
|
qelectrotech --export-png <project.qet> <output_dir>
|
||||||
|
qelectrotech --export-svg <project.qet> <output_dir>
|
||||||
|
qelectrotech --export-cables <project.qet> <output.csv>
|
||||||
|
qelectrotech --export-wires <project.qet> <output.csv>
|
||||||
|
qelectrotech --export-bom <project.qet> <output.csv>
|
||||||
|
qelectrotech --export-nets <project.qet> <output.json>
|
||||||
|
qelectrotech --export-links <project.qet> <output.csv>
|
||||||
|
qelectrotech --info <project.qet> [output.json]
|
||||||
|
qelectrotech --check-elements <element.elmt | directory>
|
||||||
|
qelectrotech --resave <project.qet> <output.qet>
|
||||||
|
qelectrotech --set-titleblock <project.qet> <output.qet> key=value...
|
||||||
|
|
||||||
|
PDF: one multi-page document (one diagram per page).
|
||||||
|
PNG/SVG: one file per diagram, named <output_dir>/<NN>_<title>.<ext>.
|
||||||
|
cables: wiring list (one row per conductor) as CSV.
|
||||||
|
wires: list of distinct wire numbers as CSV.
|
||||||
|
bom: bill of materials (one row per element) as CSV.
|
||||||
|
nets: electrical nets (connected-terminal groups) as JSON.
|
||||||
|
links: element cross-references (coil/contact) as CSV, with
|
||||||
|
unresolved links flagged.
|
||||||
|
info: structural project summary as JSON (stdout, or a file) —
|
||||||
|
per-page element / conductor counts and unconnected terminals.
|
||||||
|
check-elements: validate .elmt file(s) against the element schema.
|
||||||
|
resave: load and rewrite the project XML (round-trip integrity).
|
||||||
|
set-titleblock: stamp title-block fields onto every folio, then save.
|
||||||
|
Keys: title, author, date (or date=today), plant, location,
|
||||||
|
revision, version, filename; any other key becomes a custom
|
||||||
|
field. E.g. --set-titleblock in.qet out.qet revision=B date=today
|
||||||
|
*/
|
||||||
|
int run(const QStringList &args);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // CLI_EXPORT_H
|
||||||
@@ -72,7 +72,14 @@ bool ConductorNumExport::toCsv()
|
|||||||
if (file.open(QIODevice::WriteOnly | QIODevice::Text))
|
if (file.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||||
{
|
{
|
||||||
QTextStream stream(&file);
|
QTextStream stream(&file);
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // ### Qt 6: remove
|
||||||
|
stream << wiresNum() << endl;
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 5.15 or later")
|
||||||
|
#endif
|
||||||
stream << wiresNum() << &Qt::endl(stream);
|
stream << wiresNum() << &Qt::endl(stream);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -811,7 +811,14 @@ void ConductorProperties::readStyle(const QString &style_string) {
|
|||||||
if (style_string.isEmpty()) return;
|
if (style_string.isEmpty()) return;
|
||||||
|
|
||||||
// recupere la liste des couples style / valeur
|
// recupere la liste des couples style / valeur
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
||||||
|
QStringList styles = style_string.split(";", QString::SkipEmptyParts);
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code QString::SkipEmptyParts for QT 5.14 or later")
|
||||||
|
#endif
|
||||||
QStringList styles = style_string.split(";", Qt::SkipEmptyParts);
|
QStringList styles = style_string.split(";", Qt::SkipEmptyParts);
|
||||||
|
#endif
|
||||||
|
|
||||||
QRegularExpression Rx("^(?<name>[a-z-]+): (?<value>[a-z-]+)$");
|
QRegularExpression Rx("^(?<name>[a-z-]+): (?<value>[a-z-]+)$");
|
||||||
if (!Rx.isValid())
|
if (!Rx.isValid())
|
||||||
|
|||||||
@@ -383,7 +383,7 @@ void projectDataBase::createElementNomenclatureView()
|
|||||||
"ei.supplier_auxiliary4 AS supplier_auxiliary4,"
|
"ei.supplier_auxiliary4 AS supplier_auxiliary4,"
|
||||||
"ei.quantity_auxiliary4 AS quantity_auxiliary4,"
|
"ei.quantity_auxiliary4 AS quantity_auxiliary4,"
|
||||||
"ei.unity_auxiliary4 AS unity_auxiliary4,"
|
"ei.unity_auxiliary4 AS unity_auxiliary4,"
|
||||||
|
"ei.exclude_from_bom AS exclude_from_bom,"
|
||||||
|
|
||||||
"d.pos AS diagram_position,"
|
"d.pos AS diagram_position,"
|
||||||
"e.type AS element_type,"
|
"e.type AS element_type,"
|
||||||
@@ -392,7 +392,7 @@ void projectDataBase::createElementNomenclatureView()
|
|||||||
"di.folio AS folio,"
|
"di.folio AS folio,"
|
||||||
"e.pos AS position "
|
"e.pos AS position "
|
||||||
" FROM element_info ei, diagram_info di, element e, diagram d"
|
" FROM element_info ei, diagram_info di, element e, diagram d"
|
||||||
" WHERE ei.element_uuid = e.uuid AND e.diagram_uuid = d.uuid AND di.diagram_uuid = d.uuid");
|
" WHERE ei.element_uuid = e.uuid AND e.diagram_uuid = d.uuid AND di.diagram_uuid = d.uuid AND (ei.exclude_from_bom IS NOT 'true')");
|
||||||
|
|
||||||
QSqlQuery query(m_data_base);
|
QSqlQuery query(m_data_base);
|
||||||
if (!query.exec(create_view)) {
|
if (!query.exec(create_view)) {
|
||||||
|
|||||||
@@ -47,8 +47,14 @@ ElementQueryWidget::ElementQueryWidget(QWidget *parent) :
|
|||||||
m_button_group.addButton(ui->m_coil_cb, 4);
|
m_button_group.addButton(ui->m_coil_cb, 4);
|
||||||
m_button_group.addButton(ui->m_protection_cb, 5);
|
m_button_group.addButton(ui->m_protection_cb, 5);
|
||||||
m_button_group.addButton(ui->m_thumbnail_cb, 6);
|
m_button_group.addButton(ui->m_thumbnail_cb, 6);
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // ### Qt 6: remove
|
||||||
|
connect(&m_button_group, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), [this](int id)
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 5.15 or later")
|
||||||
|
#endif
|
||||||
connect(&m_button_group, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::idClicked), [this](int id)
|
connect(&m_button_group, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::idClicked), [this](int id)
|
||||||
|
#endif
|
||||||
{
|
{
|
||||||
auto check_box = static_cast<QCheckBox *>(m_button_group.button(0));
|
auto check_box = static_cast<QCheckBox *>(m_button_group.button(0));
|
||||||
if (id == 0)
|
if (id == 0)
|
||||||
@@ -367,6 +373,11 @@ QString ElementQueryWidget::queryStr() const
|
|||||||
where.clear();
|
where.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString exclude_condition = "(exclude_from_bom IS NULL OR exclude_from_bom != '1')";
|
||||||
|
|
||||||
|
filter_ += " AND " + exclude_condition;
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
|
||||||
if (where.isEmpty() && !filter_.isEmpty()) {
|
if (where.isEmpty() && !filter_.isEmpty()) {
|
||||||
filter_.remove(0, 4); //Remove the first " AND" of filter.
|
filter_.remove(0, 4); //Remove the first " AND" of filter.
|
||||||
filter_.prepend( " WHERE");
|
filter_.prepend( " WHERE");
|
||||||
@@ -450,7 +461,7 @@ void ElementQueryWidget::setUpItems()
|
|||||||
{
|
{
|
||||||
for(QString key : QETInformation::elementInfoKeys())
|
for(QString key : QETInformation::elementInfoKeys())
|
||||||
{
|
{
|
||||||
if (key == "formula")
|
if (key == "formula" || key == "exclude_from_bom")
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
auto item = new QListWidgetItem(QETInformation::translatedInfoKey(key), ui->m_var_list);
|
auto item = new QListWidgetItem(QETInformation::translatedInfoKey(key), ui->m_var_list);
|
||||||
|
|||||||
@@ -1514,6 +1514,14 @@ bool Diagram::fromXml(QDomElement &document,
|
|||||||
if (content_ptr) {
|
if (content_ptr) {
|
||||||
content_ptr -> m_elements = added_elements;
|
content_ptr -> m_elements = added_elements;
|
||||||
content_ptr -> m_conductors_to_move = added_conductors;
|
content_ptr -> m_conductors_to_move = added_conductors;
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
||||||
|
content_ptr -> m_text_fields = added_texts.toSet();
|
||||||
|
content_ptr -> m_images = added_images.toSet();
|
||||||
|
content_ptr -> m_shapes = added_shapes.toSet();
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 5.14 or later")
|
||||||
|
#endif
|
||||||
content_ptr -> m_text_fields = QSet<IndependentTextItem *>(
|
content_ptr -> m_text_fields = QSet<IndependentTextItem *>(
|
||||||
added_texts.begin(),
|
added_texts.begin(),
|
||||||
added_texts.end());
|
added_texts.end());
|
||||||
@@ -1524,6 +1532,7 @@ bool Diagram::fromXml(QDomElement &document,
|
|||||||
added_shapes.begin(),
|
added_shapes.begin(),
|
||||||
added_shapes.end());
|
added_shapes.end());
|
||||||
content_ptr->m_terminal_strip.swap(added_strips);
|
content_ptr->m_terminal_strip.swap(added_strips);
|
||||||
|
#endif
|
||||||
content_ptr->m_tables.swap(added_tables);
|
content_ptr->m_tables.swap(added_tables);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -142,10 +142,11 @@ class Diagram : public QGraphicsScene
|
|||||||
void wheelEvent (QGraphicsSceneWheelEvent *event) override;
|
void wheelEvent (QGraphicsSceneWheelEvent *event) override;
|
||||||
void keyPressEvent (QKeyEvent *event) override;
|
void keyPressEvent (QKeyEvent *event) override;
|
||||||
void keyReleaseEvent (QKeyEvent *) override;
|
void keyReleaseEvent (QKeyEvent *) override;
|
||||||
void correctTextPos(Element* elmt);
|
|
||||||
void restoreText(Element* elmt);
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
void correctTextPos(Element* elmt);
|
||||||
|
void restoreText(Element* elmt);
|
||||||
QUuid uuid();
|
QUuid uuid();
|
||||||
void setEventInterface (DiagramEventInterface *event_interface);
|
void setEventInterface (DiagramEventInterface *event_interface);
|
||||||
void clearEventInterface();
|
void clearEventInterface();
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
#ifndef DIAGRAM_CONTEXT_H
|
#ifndef DIAGRAM_CONTEXT_H
|
||||||
#define DIAGRAM_CONTEXT_H
|
#define DIAGRAM_CONTEXT_H
|
||||||
#include "pugixml.hpp"
|
#include "pugixml/src/pugixml.hpp"
|
||||||
|
|
||||||
#include <QDomElement>
|
#include <QDomElement>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
|
|||||||
@@ -210,10 +210,17 @@ void DiagramView::handleElementDrop(QDropEvent *event)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QPointF drop_pos;
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
|
||||||
|
drop_pos = mapToScene(event->pos());
|
||||||
|
#else
|
||||||
|
drop_pos = event->position();
|
||||||
|
#endif
|
||||||
|
|
||||||
if (location.path().endsWith(".qetmak")) {
|
if (location.path().endsWith(".qetmak")) {
|
||||||
diagram()->setEventInterface(new DiagramEventAddMacro(location, diagram(), event->position()));
|
diagram()->setEventInterface(new DiagramEventAddMacro(location, diagram(), drop_pos));
|
||||||
} else {
|
} else {
|
||||||
diagram()->setEventInterface(new DiagramEventAddElement(location, diagram(), event->position()));
|
diagram()->setEventInterface(new DiagramEventAddElement(location, diagram(), drop_pos));
|
||||||
}
|
}
|
||||||
|
|
||||||
//Set focus to the view to get event
|
//Set focus to the view to get event
|
||||||
@@ -283,8 +290,17 @@ void DiagramView::handleTextDrop(QDropEvent *e) {
|
|||||||
iti -> setHtml (e -> mimeData() -> text());
|
iti -> setHtml (e -> mimeData() -> text());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
|
||||||
|
|
||||||
|
m_diagram->undoStack().push(new AddGraphicsObjectCommand(
|
||||||
|
iti, m_diagram, mapToScene(e->pos())));
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 6 or later")
|
||||||
|
#endif
|
||||||
m_diagram->undoStack().push(new AddGraphicsObjectCommand(
|
m_diagram->undoStack().push(new AddGraphicsObjectCommand(
|
||||||
iti, m_diagram, e->position()));
|
iti, m_diagram, e->position()));
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -442,7 +458,14 @@ void DiagramView::mousePressEvent(QMouseEvent *e)
|
|||||||
if (m_event_interface && m_event_interface->mousePressEvent(e)) return;
|
if (m_event_interface && m_event_interface->mousePressEvent(e)) return;
|
||||||
|
|
||||||
//Start drag view when hold the middle button
|
//Start drag view when hold the middle button
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||||
|
if (e->button() == Qt::MidButton)
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 6 or later")
|
||||||
|
#endif
|
||||||
if (e->button() == Qt::MiddleButton)
|
if (e->button() == Qt::MiddleButton)
|
||||||
|
#endif
|
||||||
{
|
{
|
||||||
m_drag_last_pos = e->pos();
|
m_drag_last_pos = e->pos();
|
||||||
viewport()->setCursor(Qt::ClosedHandCursor);
|
viewport()->setCursor(Qt::ClosedHandCursor);
|
||||||
@@ -492,7 +515,14 @@ void DiagramView::mouseMoveEvent(QMouseEvent *e)
|
|||||||
if (m_event_interface && m_event_interface->mouseMoveEvent(e)) return;
|
if (m_event_interface && m_event_interface->mouseMoveEvent(e)) return;
|
||||||
|
|
||||||
// Drag the view
|
// Drag the view
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||||
|
if (e->buttons() == Qt::MidButton)
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 6 or later")
|
||||||
|
#endif
|
||||||
if (e->buttons() == Qt::MiddleButton)
|
if (e->buttons() == Qt::MiddleButton)
|
||||||
|
#endif
|
||||||
{
|
{
|
||||||
QScrollBar *h = horizontalScrollBar();
|
QScrollBar *h = horizontalScrollBar();
|
||||||
QScrollBar *v = verticalScrollBar();
|
QScrollBar *v = verticalScrollBar();
|
||||||
@@ -553,7 +583,14 @@ void DiagramView::mouseReleaseEvent(QMouseEvent *e)
|
|||||||
if (m_event_interface && m_event_interface->mouseReleaseEvent(e)) return;
|
if (m_event_interface && m_event_interface->mouseReleaseEvent(e)) return;
|
||||||
|
|
||||||
// Stop drag view
|
// Stop drag view
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||||
|
if (e->button() == Qt::MidButton)
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 6 or later")
|
||||||
|
#endif
|
||||||
if (e->button() == Qt::MiddleButton)
|
if (e->button() == Qt::MiddleButton)
|
||||||
|
#endif
|
||||||
{
|
{
|
||||||
viewport()->setCursor(Qt::ArrowCursor);
|
viewport()->setCursor(Qt::ArrowCursor);
|
||||||
}
|
}
|
||||||
@@ -587,7 +624,14 @@ void DiagramView::mouseReleaseEvent(QMouseEvent *e)
|
|||||||
QMenu *menu = new QMenu(this);
|
QMenu *menu = new QMenu(this);
|
||||||
menu->addAction(act);
|
menu->addAction(act);
|
||||||
|
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
|
||||||
|
menu->popup(e->globalPos());
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 6 or later")
|
||||||
|
#endif
|
||||||
menu->popup(e->pos());
|
menu->popup(e->pos());
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
m_free_rubberbanding = false;
|
m_free_rubberbanding = false;
|
||||||
@@ -1311,6 +1355,7 @@ void DiagramView::createTemplateFromSelection()
|
|||||||
QFile file(full_path);
|
QFile file(full_path);
|
||||||
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||||
QTextStream out(&file);
|
QTextStream out(&file);
|
||||||
|
out.setCodec("UTF-8");
|
||||||
out << macro_doc.toString(4);
|
out << macro_doc.toString(4);
|
||||||
file.close();
|
file.close();
|
||||||
qDebug() << "Template successfully saved to:" << full_path;
|
qDebug() << "Template successfully saved to:" << full_path;
|
||||||
|
|||||||
@@ -276,7 +276,14 @@ void ChangeZValueCommand::applyRaise(const QList<QGraphicsItem *> &items_list) {
|
|||||||
for (int i = my_items_list.count() - 2 ; i >= 0 ; -- i) {
|
for (int i = my_items_list.count() - 2 ; i >= 0 ; -- i) {
|
||||||
if (my_items_list[i] -> isSelected()) {
|
if (my_items_list[i] -> isSelected()) {
|
||||||
if (!my_items_list[i +1] -> isSelected()) {
|
if (!my_items_list[i +1] -> isSelected()) {
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) // ### Qt 6: remove
|
||||||
|
my_items_list.swap(i, i + 1);
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 5.13 or later")
|
||||||
|
#endif
|
||||||
my_items_list.swapItemsAt(i, i + 1);
|
my_items_list.swapItemsAt(i, i + 1);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,7 +301,14 @@ void ChangeZValueCommand::applyLower(const QList<QGraphicsItem *> &items_list) {
|
|||||||
for (int i = 1 ; i < my_items_list.count() ; ++ i) {
|
for (int i = 1 ; i < my_items_list.count() ; ++ i) {
|
||||||
if (my_items_list[i] -> isSelected()) {
|
if (my_items_list[i] -> isSelected()) {
|
||||||
if (!my_items_list[i - 1] -> isSelected()) {
|
if (!my_items_list[i - 1] -> isSelected()) {
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) // ### Qt 6: remove
|
||||||
|
my_items_list.swap(i, i - 1);
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 5.13 or later")
|
||||||
|
#endif
|
||||||
my_items_list.swapItemsAt(i, i - 1);
|
my_items_list.swapItemsAt(i, i - 1);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -372,7 +372,14 @@ ElementContent ElementView::pasteWithOffset(const QDomDocument &xml_document) {
|
|||||||
*/
|
*/
|
||||||
void ElementView::mousePressEvent(QMouseEvent* e)
|
void ElementView::mousePressEvent(QMouseEvent* e)
|
||||||
{
|
{
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||||
|
if (e->button() == Qt::MidButton)
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 6 or later")
|
||||||
|
#endif
|
||||||
if (e->button() == Qt::MiddleButton)
|
if (e->button() == Qt::MiddleButton)
|
||||||
|
#endif
|
||||||
{
|
{
|
||||||
setCursor( (Qt::ClosedHandCursor));
|
setCursor( (Qt::ClosedHandCursor));
|
||||||
reference_view_ = e->pos();
|
reference_view_ = e->pos();
|
||||||
@@ -387,7 +394,14 @@ void ElementView::mousePressEvent(QMouseEvent* e)
|
|||||||
*/
|
*/
|
||||||
void ElementView::mouseMoveEvent(QMouseEvent* e)
|
void ElementView::mouseMoveEvent(QMouseEvent* e)
|
||||||
{
|
{
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||||
|
if (e->buttons() == Qt::MidButton)
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 6 or later")
|
||||||
|
#endif
|
||||||
if (e->buttons() == Qt::MiddleButton)
|
if (e->buttons() == Qt::MiddleButton)
|
||||||
|
#endif
|
||||||
{
|
{
|
||||||
QScrollBar *h = horizontalScrollBar();
|
QScrollBar *h = horizontalScrollBar();
|
||||||
QScrollBar *v = verticalScrollBar();
|
QScrollBar *v = verticalScrollBar();
|
||||||
@@ -406,7 +420,14 @@ void ElementView::mouseMoveEvent(QMouseEvent* e)
|
|||||||
*/
|
*/
|
||||||
void ElementView::mouseReleaseEvent(QMouseEvent* e)
|
void ElementView::mouseReleaseEvent(QMouseEvent* e)
|
||||||
{
|
{
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||||
|
if (e->button() == Qt::MidButton)
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 6 or later")
|
||||||
|
#endif
|
||||||
if (e->button() == Qt::MiddleButton)
|
if (e->button() == Qt::MiddleButton)
|
||||||
|
#endif
|
||||||
{
|
{
|
||||||
setCursor(Qt::ArrowCursor);
|
setCursor(Qt::ArrowCursor);
|
||||||
adjustSceneRect();
|
adjustSceneRect();
|
||||||
|
|||||||
@@ -519,7 +519,14 @@ void CustomElementGraphicPart::stylesFromXml(const QDomElement &qde)
|
|||||||
resetStyles();
|
resetStyles();
|
||||||
|
|
||||||
//Get the list of pair style/value
|
//Get the list of pair style/value
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
||||||
|
QStringList styles = qde.attribute("style").split(";", QString::SkipEmptyParts);
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 5.14 or later")
|
||||||
|
#endif
|
||||||
QStringList styles = qde.attribute("style").split(";", Qt::SkipEmptyParts);
|
QStringList styles = qde.attribute("style").split(";", Qt::SkipEmptyParts);
|
||||||
|
#endif
|
||||||
|
|
||||||
//Check each pair of style
|
//Check each pair of style
|
||||||
QRegularExpression rx("^\\s*([a-z-]+)\\s*:\\s*([a-zA-Z-]+)\\s*$");
|
QRegularExpression rx("^\\s*([a-z-]+)\\s*:\\s*([a-zA-Z-]+)\\s*$");
|
||||||
|
|||||||
@@ -313,6 +313,12 @@ void PartText::setPlainText(const QString &text) {
|
|||||||
void PartText::setFont(const QFont &font) {
|
void PartText::setFont(const QFont &font) {
|
||||||
if (font != this -> font()) {
|
if (font != this -> font()) {
|
||||||
QGraphicsTextItem::setFont(font);
|
QGraphicsTextItem::setFont(font);
|
||||||
|
// Re-anchor: the item's position transform is -margin(), and margin()
|
||||||
|
// depends on the font ascent. Without re-running this on a font change,
|
||||||
|
// the transform keeps the previous font's ascent — so the text renders
|
||||||
|
// at a different spot after save/reopen (the position recomputes from
|
||||||
|
// the saved font on load). See #158.
|
||||||
|
adjustItemPosition();
|
||||||
emit fontChanged(font);
|
emit fontChanged(font);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,9 +291,11 @@ void ElementPropertiesEditorWidget::on_m_base_type_cb_currentIndexChanged(int in
|
|||||||
ui->m_master_gb->setVisible(master);
|
ui->m_master_gb->setVisible(master);
|
||||||
ui->m_terminal_gb->setVisible(terminal);
|
ui->m_terminal_gb->setVisible(terminal);
|
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
|
||||||
ui->tabWidget->setTabVisible(1,
|
ui->tabWidget->setTabVisible(1,
|
||||||
(type_ == ElementData::Simple ||
|
(type_ == ElementData::Simple ||
|
||||||
type_ == ElementData::Master));
|
type_ == ElementData::Master));
|
||||||
|
#endif
|
||||||
|
|
||||||
updateTree();
|
updateTree();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,17 @@ void ElementDialog::setUpWidget()
|
|||||||
} else if (m_mode == SaveTemplate) {
|
} else if (m_mode == SaveTemplate) {
|
||||||
m_text_field->setPlaceholderText(tr("Nom du nouveau template"));
|
m_text_field->setPlaceholderText(tr("Nom du nouveau template"));
|
||||||
} else {
|
} else {
|
||||||
m_text_field->setPlaceholderText(tr("Nom du nouvel élément"));
|
// This is the element's file name, not its display name: the field
|
||||||
|
// only accepts file-name characters (QFileNameEdit). The visible
|
||||||
|
// element name is edited separately in the element properties.
|
||||||
|
m_text_field->setPlaceholderText(
|
||||||
|
tr("Nom de fichier de l'élément",
|
||||||
|
"placeholder: the element's file name, not its display name"));
|
||||||
|
m_text_field->setToolTip(
|
||||||
|
tr("Nom de fichier de l'élément : chiffres, minuscules, « - », "
|
||||||
|
"« _ » et « . » uniquement.\nLe nom affiché de l'élément se "
|
||||||
|
"modifie séparément dans les propriétés de l'élément.",
|
||||||
|
"tooltip for the element file-name field"));
|
||||||
}
|
}
|
||||||
|
|
||||||
layout->addWidget(m_text_field);
|
layout->addWidget(m_text_field);
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
#include "elementspanelwidget.h"
|
#include "elementspanelwidget.h"
|
||||||
|
|
||||||
#include "diagram.h"
|
#include "diagram.h"
|
||||||
#include "editor/ui/qetelementeditor.h"
|
#include "editor/ui/qetelementeditor.h"
|
||||||
#include "elementscategoryeditor.h"
|
#include "elementscategoryeditor.h"
|
||||||
@@ -26,6 +25,7 @@
|
|||||||
#include "titleblock/templatedeleter.h"
|
#include "titleblock/templatedeleter.h"
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
#include "qetgraphicsitem/element.h"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
When the ENABLE_PANEL_WIDGET_DND_CHECKS flag is set, the panel
|
When the ENABLE_PANEL_WIDGET_DND_CHECKS flag is set, the panel
|
||||||
@@ -59,7 +59,8 @@ ElementsPanelWidget::ElementsPanelWidget(QWidget *parent) : QWidget(parent) {
|
|||||||
prj_close = new QAction(QET::Icons::DocumentClose, tr("Fermer ce projet"), this);
|
prj_close = new QAction(QET::Icons::DocumentClose, tr("Fermer ce projet"), this);
|
||||||
prj_edit_prop = new QAction(QET::Icons::DialogInformation, tr("Propriétés du projet"), this);
|
prj_edit_prop = new QAction(QET::Icons::DialogInformation, tr("Propriétés du projet"), this);
|
||||||
prj_prop_diagram = new QAction(QET::Icons::DialogInformation, tr("Propriétés du folio"), this);
|
prj_prop_diagram = new QAction(QET::Icons::DialogInformation, tr("Propriétés du folio"), this);
|
||||||
prj_add_diagram = new QAction(QET::Icons::DiagramAdd, tr("Ajouter un folio"), this);
|
prj_add_diagram = new QAction(QET::Icons::DiagramAdd, tr("Ajouter un folio"), this);
|
||||||
|
prj_duplicate_diagram = new QAction(QET::Icons::IC_CopyFile, tr("Copier et coller"), this);
|
||||||
prj_del_diagram = new QAction(QET::Icons::DiagramDelete, tr("Supprimer ce folio"), this);
|
prj_del_diagram = new QAction(QET::Icons::DiagramDelete, tr("Supprimer ce folio"), this);
|
||||||
prj_move_diagram_up = new QAction(QET::Icons::GoUp, tr("Remonter ce folio"), this);
|
prj_move_diagram_up = new QAction(QET::Icons::GoUp, tr("Remonter ce folio"), this);
|
||||||
prj_move_diagram_down = new QAction(QET::Icons::GoDown, tr("Abaisser ce folio"), this);
|
prj_move_diagram_down = new QAction(QET::Icons::GoDown, tr("Abaisser ce folio"), this);
|
||||||
@@ -100,6 +101,7 @@ ElementsPanelWidget::ElementsPanelWidget(QWidget *parent) : QWidget(parent) {
|
|||||||
connect(prj_prop_diagram, SIGNAL(triggered()), this, SLOT(editDiagramProperties()));
|
connect(prj_prop_diagram, SIGNAL(triggered()), this, SLOT(editDiagramProperties()));
|
||||||
connect(prj_add_diagram, SIGNAL(triggered()), this, SLOT(newDiagram()));
|
connect(prj_add_diagram, SIGNAL(triggered()), this, SLOT(newDiagram()));
|
||||||
connect(prj_del_diagram, SIGNAL(triggered()), this, SLOT(deleteDiagram()));
|
connect(prj_del_diagram, SIGNAL(triggered()), this, SLOT(deleteDiagram()));
|
||||||
|
connect(prj_duplicate_diagram, SIGNAL(triggered()), this, SLOT(duplicateDiagram()));
|
||||||
connect(prj_move_diagram_up, SIGNAL(triggered()), this, SLOT(moveDiagramUp()));
|
connect(prj_move_diagram_up, SIGNAL(triggered()), this, SLOT(moveDiagramUp()));
|
||||||
connect(prj_move_diagram_down, SIGNAL(triggered()), this, SLOT(moveDiagramDown()));
|
connect(prj_move_diagram_down, SIGNAL(triggered()), this, SLOT(moveDiagramDown()));
|
||||||
connect(prj_move_diagram_top, SIGNAL(triggered()), this, SLOT(moveDiagramUpTop()));
|
connect(prj_move_diagram_top, SIGNAL(triggered()), this, SLOT(moveDiagramUpTop()));
|
||||||
@@ -447,7 +449,8 @@ void ElementsPanelWidget::updateButtons()
|
|||||||
}
|
}
|
||||||
|
|
||||||
prj_del_diagram -> setEnabled(is_writable);
|
prj_del_diagram -> setEnabled(is_writable);
|
||||||
prj_move_diagram_up -> setEnabled(is_writable && min_position > 0);
|
prj_duplicate_diagram -> setEnabled(is_writable);
|
||||||
|
prj_move_diagram_up -> setEnabled(is_writable && min_position > 0);
|
||||||
prj_move_diagram_down -> setEnabled(is_writable && max_position < project_diagrams_count - 1);
|
prj_move_diagram_down -> setEnabled(is_writable && max_position < project_diagrams_count - 1);
|
||||||
prj_move_diagram_top -> setEnabled(is_writable && min_position > 0);
|
prj_move_diagram_top -> setEnabled(is_writable && min_position > 0);
|
||||||
|
|
||||||
@@ -501,6 +504,7 @@ void ElementsPanelWidget::handleContextMenu(const QPoint &pos) {
|
|||||||
case QET::Diagram:
|
case QET::Diagram:
|
||||||
context_menu -> addAction(prj_prop_diagram);
|
context_menu -> addAction(prj_prop_diagram);
|
||||||
context_menu -> addAction(prj_del_diagram);
|
context_menu -> addAction(prj_del_diagram);
|
||||||
|
context_menu -> addAction(prj_duplicate_diagram);
|
||||||
context_menu -> addAction(prj_move_diagram_top);
|
context_menu -> addAction(prj_move_diagram_top);
|
||||||
context_menu -> addAction(prj_move_diagram_upx10);
|
context_menu -> addAction(prj_move_diagram_upx10);
|
||||||
context_menu -> addAction(prj_move_diagram_upx100);
|
context_menu -> addAction(prj_move_diagram_upx100);
|
||||||
@@ -593,3 +597,56 @@ void ElementsPanelWidget::keyPressEvent(QKeyEvent *e) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicates the selected folios (pages) along with their content
|
||||||
|
* and properties, and cleanly resolves cross-references.
|
||||||
|
*/
|
||||||
|
void ElementsPanelWidget::duplicateDiagram()
|
||||||
|
{
|
||||||
|
QList<Diagram *> diagrams_to_duplicate = elements_panel->selectedDiagrams();
|
||||||
|
if (diagrams_to_duplicate.isEmpty()) return;
|
||||||
|
|
||||||
|
QETProject *project = diagrams_to_duplicate.first()->project();
|
||||||
|
if (!project || project->isReadOnly()) return;
|
||||||
|
|
||||||
|
for (Diagram *source_diagram : diagrams_to_duplicate) {
|
||||||
|
|
||||||
|
Diagram *new_diagram = project->addNewDiagram();
|
||||||
|
if (!new_diagram) continue;
|
||||||
|
|
||||||
|
QString template_name = source_diagram->border_and_titleblock.titleBlockTemplateName();
|
||||||
|
new_diagram->setTitleBlockTemplate(template_name);
|
||||||
|
|
||||||
|
TitleBlockProperties tbp = source_diagram->border_and_titleblock.exportTitleBlock();
|
||||||
|
new_diagram->border_and_titleblock.importTitleBlock(tbp);
|
||||||
|
|
||||||
|
BorderProperties bp = source_diagram->border_and_titleblock.exportBorder();
|
||||||
|
new_diagram->border_and_titleblock.importBorder(bp);
|
||||||
|
|
||||||
|
for (QGraphicsItem *item : source_diagram->items()) {
|
||||||
|
if (Element *elmt = dynamic_cast<Element *>(item)) {
|
||||||
|
source_diagram->correctTextPos(elmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QDomDocument doc = source_diagram->toXml();
|
||||||
|
QDomElement diagram_elmt = doc.documentElement();
|
||||||
|
|
||||||
|
for (QGraphicsItem *item : source_diagram->items()) {
|
||||||
|
if (Element *elmt = dynamic_cast<Element *>(item)) {
|
||||||
|
source_diagram->restoreText(elmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_diagram->fromXml(diagram_elmt, QPointF(0, 0), false, nullptr);
|
||||||
|
|
||||||
|
for (QGraphicsItem *item : new_diagram->items()) {
|
||||||
|
if (Element *elmt = dynamic_cast<Element *>(item)) {
|
||||||
|
new_diagram->restoreText(elmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elements_panel->reload();
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class ElementsPanelWidget : public QWidget {
|
|||||||
*prj_prop_diagram,
|
*prj_prop_diagram,
|
||||||
*prj_add_diagram,
|
*prj_add_diagram,
|
||||||
*prj_del_diagram,
|
*prj_del_diagram,
|
||||||
|
*prj_duplicate_diagram,
|
||||||
*prj_move_diagram_up,
|
*prj_move_diagram_up,
|
||||||
*prj_move_diagram_top,
|
*prj_move_diagram_top,
|
||||||
*prj_move_diagram_down,
|
*prj_move_diagram_down,
|
||||||
@@ -88,6 +89,7 @@ class ElementsPanelWidget : public QWidget {
|
|||||||
void editDiagramProperties();
|
void editDiagramProperties();
|
||||||
void newDiagram();
|
void newDiagram();
|
||||||
void deleteDiagram();
|
void deleteDiagram();
|
||||||
|
void duplicateDiagram();
|
||||||
void moveDiagramUp();
|
void moveDiagramUp();
|
||||||
void moveDiagramDown();
|
void moveDiagramDown();
|
||||||
void moveDiagramUpTop();
|
void moveDiagramUpTop();
|
||||||
|
|||||||
@@ -567,7 +567,14 @@ void ElementPictureFactory::setPainterStyle(const QDomElement &dom, QPainter &pa
|
|||||||
pen.setCapStyle(Qt::SquareCap);
|
pen.setCapStyle(Qt::SquareCap);
|
||||||
|
|
||||||
//Get the couples style/value
|
//Get the couples style/value
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
||||||
|
const QStringList styles = dom.attribute("style").split(";", QString::SkipEmptyParts);
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 5.14 or later")
|
||||||
|
#endif
|
||||||
const QStringList styles = dom.attribute("style").split(";", Qt::SkipEmptyParts);
|
const QStringList styles = dom.attribute("style").split(";", Qt::SkipEmptyParts);
|
||||||
|
#endif
|
||||||
|
|
||||||
QRegularExpression rx("^(?<name>[a-z-]+):(?<value>[a-zA-Z-]+)$");
|
QRegularExpression rx("^(?<name>[a-z-]+):(?<value>[a-zA-Z-]+)$");
|
||||||
if (!rx.isValid())
|
if (!rx.isValid())
|
||||||
|
|||||||
@@ -188,8 +188,10 @@ void MachineInfo::send_info_to_debug()
|
|||||||
QDirIterator it1(QETApp::commonElementsDir().toLatin1(),nameFilters, QDir::Files, QDirIterator::Subdirectories);
|
QDirIterator it1(QETApp::commonElementsDir().toLatin1(),nameFilters, QDir::Files, QDirIterator::Subdirectories);
|
||||||
while (it1.hasNext())
|
while (it1.hasNext())
|
||||||
{
|
{
|
||||||
it1.next();
|
if(it1.next() > 0 )
|
||||||
|
{
|
||||||
commomElementsDir ++;
|
commomElementsDir ++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
qInfo()<< " Common Elements count:"<< commomElementsDir << "Elements";
|
qInfo()<< " Common Elements count:"<< commomElementsDir << "Elements";
|
||||||
|
|
||||||
@@ -198,8 +200,10 @@ void MachineInfo::send_info_to_debug()
|
|||||||
QDirIterator it2(QETApp::customElementsDir().toLatin1(), nameFilters, QDir::Files, QDirIterator::Subdirectories);
|
QDirIterator it2(QETApp::customElementsDir().toLatin1(), nameFilters, QDir::Files, QDirIterator::Subdirectories);
|
||||||
while (it2.hasNext())
|
while (it2.hasNext())
|
||||||
{
|
{
|
||||||
it2.next();
|
if(it2.next() > 0 )
|
||||||
|
{
|
||||||
customElementsDir ++;
|
customElementsDir ++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
qInfo()<< " Custom Elements count:"<< customElementsDir << "Elements";
|
qInfo()<< " Custom Elements count:"<< customElementsDir << "Elements";
|
||||||
|
|
||||||
@@ -207,8 +211,10 @@ void MachineInfo::send_info_to_debug()
|
|||||||
QDirIterator it3(QETApp::companyElementsDir().toLatin1(), nameFilters, QDir::Files, QDirIterator::Subdirectories);
|
QDirIterator it3(QETApp::companyElementsDir().toLatin1(), nameFilters, QDir::Files, QDirIterator::Subdirectories);
|
||||||
while (it3.hasNext())
|
while (it3.hasNext())
|
||||||
{
|
{
|
||||||
it3.next();
|
if(it3.next() > 0 )
|
||||||
|
{
|
||||||
companyElementsDir ++;
|
companyElementsDir ++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
qInfo()<< " Company Elements count:"<< companyElementsDir << "Elements";
|
qInfo()<< " Company Elements count:"<< companyElementsDir << "Elements";
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,17 @@
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
#include "cli_export.h"
|
||||||
#include "machine_info.h"
|
#include "machine_info.h"
|
||||||
#include "qet.h"
|
#include "qet.h"
|
||||||
#include "qetapp.h"
|
#include "qetapp.h"
|
||||||
|
#include "qetproject.h"
|
||||||
#include "singleapplication.h"
|
#include "singleapplication.h"
|
||||||
#include "utils/macosxopenevent.h"
|
#include "utils/macosxopenevent.h"
|
||||||
#include "utils/qetsettings.h"
|
#include "utils/qetsettings.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
|
||||||
#include <QStyleFactory>
|
#include <QStyleFactory>
|
||||||
#include <QtConcurrentRun>
|
#include <QtConcurrentRun>
|
||||||
|
|
||||||
@@ -174,10 +178,42 @@ int main(int argc, char **argv)
|
|||||||
QCoreApplication::setApplicationName("QElectroTech");
|
QCoreApplication::setApplicationName("QElectroTech");
|
||||||
//Creation and execution of the application
|
//Creation and execution of the application
|
||||||
//HighDPI
|
//HighDPI
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
||||||
|
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 6 or later")
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
qputenv("QT_ENABLE_HIGHDPI_SCALING", "1");
|
|
||||||
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFactorRoundingPolicy());
|
|
||||||
|
|
||||||
|
#if QT_VERSION > QT_VERSION_CHECK(5, 7, 0) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
|
||||||
|
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||||
|
qputenv("QT_ENABLE_HIGHDPI_SCALING", "1");
|
||||||
|
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFactorRoundingPolicy());
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
// Headless command-line export: render a project to PDF/PNG/SVG without
|
||||||
|
// opening the GUI, then exit. Must be handled before SingleApplication
|
||||||
|
// (which would forward the args to an already-running instance).
|
||||||
|
{
|
||||||
|
QStringList raw_args;
|
||||||
|
for (int i = 0; i < argc; ++i)
|
||||||
|
raw_args << QString::fromLocal8Bit(argv[i]);
|
||||||
|
if (CLIExport::isExportRequest(raw_args)) {
|
||||||
|
QApplication export_app(argc, argv);
|
||||||
|
// No crash-recovery backups in one-shot CLI mode: the backup write
|
||||||
|
// runs on a background thread referencing the project and races the
|
||||||
|
// process exit (intermittent segfault in QET::writeToFile).
|
||||||
|
QETProject::setBackupEnabled(false);
|
||||||
|
return CLIExport::run(export_app.arguments());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SingleApplication app(argc, argv, true);
|
SingleApplication app(argc, argv, true);
|
||||||
#ifdef Q_OS_MACOS
|
#ifdef Q_OS_MACOS
|
||||||
|
|||||||
@@ -0,0 +1,382 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2006-2025 The QElectroTech Team
|
||||||
|
This file is part of QElectroTech.
|
||||||
|
|
||||||
|
QElectroTech is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
QElectroTech is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#include "pdf_links.h"
|
||||||
|
|
||||||
|
#include "diagram.h"
|
||||||
|
#include "qetgraphicsitem/crossrefitem.h"
|
||||||
|
#include "qetgraphicsitem/dynamicelementtextitem.h"
|
||||||
|
#include "qetgraphicsitem/element.h"
|
||||||
|
#include "qetgraphicsitem/elementtextitemgroup.h"
|
||||||
|
|
||||||
|
// Private Qt PDF engine for drawHyperlink() — not public API, stable since Qt4.
|
||||||
|
// Requires QT += gui-private in qelectrotech.pro / gui-private in CMake.
|
||||||
|
#include <private/qpdf_p.h>
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QGraphicsTextItem>
|
||||||
|
#include <QList>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
namespace PdfLinks {
|
||||||
|
|
||||||
|
void injectCrossRefLinks(QPdfEngine *engine, Diagram *diagram,
|
||||||
|
const PageGeometry &geom,
|
||||||
|
const QMap<Diagram *, int> &pageMap,
|
||||||
|
const QString &outputFileName)
|
||||||
|
{
|
||||||
|
if (!engine || !diagram)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QTransform &fit = geom.sceneToDevice;
|
||||||
|
const QRectF &target = geom.target;
|
||||||
|
const QRectF &pageBounds = geom.pageBounds;
|
||||||
|
|
||||||
|
// Compute, in PDF points on its OWN page, the rectangle to frame for a
|
||||||
|
// target element (used as a /FitR destination so the link zooms onto it).
|
||||||
|
auto destRectPdf = [&](Element *tgt) -> QRectF {
|
||||||
|
Diagram *dg = tgt ? tgt->diagram() : nullptr;
|
||||||
|
if (!dg) return QRectF();
|
||||||
|
const QRectF srcT = geom.sourceRectOf(dg);
|
||||||
|
if (srcT.width() <= 0.0 || srcT.height() <= 0.0) return QRectF();
|
||||||
|
const qreal sT = qMin(target.width() / srcT.width(),
|
||||||
|
target.height() / srcT.height());
|
||||||
|
QTransform fitT;
|
||||||
|
fitT.translate(target.x(), target.y());
|
||||||
|
fitT.scale(sT, sT);
|
||||||
|
fitT.translate(-srcT.x(), -srcT.y());
|
||||||
|
|
||||||
|
QRectF elemScene = tgt->mapRectToScene(tgt->boundingRect());
|
||||||
|
// Frame the element with a little context, and enforce a minimum
|
||||||
|
// framed size so tiny contacts don't zoom in extremely.
|
||||||
|
const qreal pad = 25.0;
|
||||||
|
elemScene.adjust(-pad, -pad, pad, pad);
|
||||||
|
const qreal minSide = 160.0;
|
||||||
|
if (elemScene.width() < minSide)
|
||||||
|
elemScene.adjust(-(minSide - elemScene.width()) / 2.0, 0,
|
||||||
|
(minSide - elemScene.width()) / 2.0, 0);
|
||||||
|
if (elemScene.height() < minSide)
|
||||||
|
elemScene.adjust(0, -(minSide - elemScene.height()) / 2.0,
|
||||||
|
0, (minSide - elemScene.height()) / 2.0);
|
||||||
|
|
||||||
|
const QRectF devT = fitT.mapRect(elemScene);
|
||||||
|
const QPointF a = geom.devToPdf(devT.topLeft());
|
||||||
|
const QPointF b = geom.devToPdf(devT.bottomRight());
|
||||||
|
return QRectF(QPointF(qMin(a.x(), b.x()), qMin(a.y(), b.y())),
|
||||||
|
QPointF(qMax(a.x(), b.x()), qMax(a.y(), b.y())));
|
||||||
|
};
|
||||||
|
|
||||||
|
auto injectLink = [&](const QRectF &sceneRect, Element *targetElmt) {
|
||||||
|
if (!targetElmt || !targetElmt->diagram()) return;
|
||||||
|
const int targetPage = pageMap.value(targetElmt->diagram(), -1);
|
||||||
|
if (targetPage < 1) return;
|
||||||
|
const QRectF devRect = fit.mapRect(sceneRect);
|
||||||
|
if (!devRect.isValid() || !pageBounds.intersects(devRect)) return;
|
||||||
|
|
||||||
|
QString frag = QString("page=%1").arg(targetPage);
|
||||||
|
const QRectF d = destRectPdf(targetElmt); // /FitR L_B_R_T
|
||||||
|
if (d.isValid())
|
||||||
|
frag += QString("&fitr=%1_%2_%3_%4")
|
||||||
|
.arg(qRound(d.left())).arg(qRound(d.top()))
|
||||||
|
.arg(qRound(d.right())).arg(qRound(d.bottom()));
|
||||||
|
|
||||||
|
QUrl url = QUrl::fromLocalFile(outputFileName);
|
||||||
|
url.setFragment(frag);
|
||||||
|
engine->drawHyperlink(devRect, url);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto *item : diagram->items()) {
|
||||||
|
|
||||||
|
// --- CrossRefItem links ---
|
||||||
|
if (auto *xref = dynamic_cast<CrossRefItem*>(item)) {
|
||||||
|
for (auto it = xref->hoveredContactsMap().begin();
|
||||||
|
it != xref->hoveredContactsMap().end(); ++it)
|
||||||
|
{
|
||||||
|
Element *targetElmt = it.key();
|
||||||
|
if (!targetElmt || !targetElmt->diagram()) continue;
|
||||||
|
// it.value() is in the CrossRefItem's LOCAL coords -> scene
|
||||||
|
injectLink(xref->mapRectToScene(it.value()), targetElmt);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Folio report links (DynamicElementTextItem) ---
|
||||||
|
if (auto *deti = dynamic_cast<DynamicElementTextItem*>(item)) {
|
||||||
|
Element *parent = deti->parentElement();
|
||||||
|
if (!parent) continue;
|
||||||
|
|
||||||
|
// (a) Report element : label -> linked report on another folio
|
||||||
|
if (parent->linkType() & Element::AllReport) {
|
||||||
|
if (parent->linkedElements().isEmpty()) continue;
|
||||||
|
|
||||||
|
bool showsLabel =
|
||||||
|
(deti->textFrom() == DynamicElementTextItem::ElementInfo
|
||||||
|
&& deti->infoName() == QLatin1String("label")) ||
|
||||||
|
(deti->textFrom() == DynamicElementTextItem::CompositeText
|
||||||
|
&& deti->compositeText().contains(QStringLiteral("%{label}")));
|
||||||
|
if (!showsLabel) continue;
|
||||||
|
|
||||||
|
Element *targetElmt = parent->linkedElements().first();
|
||||||
|
if (!targetElmt || !targetElmt->diagram()) continue;
|
||||||
|
|
||||||
|
injectLink(deti->mapRectToScene(deti->boundingRect()), targetElmt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// (b) Slave element : the "(folio-pos)" text -> master element
|
||||||
|
if (parent->linkType() == Element::Slave) {
|
||||||
|
QGraphicsTextItem *sx = deti->slaveXrefItem();
|
||||||
|
Element *master = deti->masterElement();
|
||||||
|
if (sx && master && master->diagram()) {
|
||||||
|
injectLink(sx->mapRectToScene(sx->boundingRect()), master);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Slave cross-reference carried by a grouped text ---
|
||||||
|
if (auto *grp = dynamic_cast<ElementTextItemGroup*>(item)) {
|
||||||
|
Element *parent = grp->parentElement();
|
||||||
|
if (!parent || parent->linkType() != Element::Slave) continue;
|
||||||
|
if (parent->linkedElements().isEmpty()) continue;
|
||||||
|
QGraphicsTextItem *sx = grp->slaveXrefItem();
|
||||||
|
if (!sx) continue;
|
||||||
|
Element *master = parent->linkedElements().first();
|
||||||
|
if (!master || !master->diagram()) continue;
|
||||||
|
injectLink(sx->mapRectToScene(sx->boundingRect()), master);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void convertUriToGoTo(const QString &pdfPath)
|
||||||
|
{
|
||||||
|
// --- 1. Read raw bytes ---
|
||||||
|
QFile f(pdfPath);
|
||||||
|
if (!f.open(QIODevice::ReadOnly)) return;
|
||||||
|
QByteArray data = f.readAll();
|
||||||
|
f.close();
|
||||||
|
|
||||||
|
// --- 2. Collect page object numbers in document order ---
|
||||||
|
// Read them from the page tree (/Type /Pages -> /Kids [ N 0 R ... ]).
|
||||||
|
// This is reliable; scanning raw bytes for "/Type /Page" is NOT: that
|
||||||
|
// marker also occurs inside content streams, and a forward lookahead
|
||||||
|
// wrongly tags neighbouring objects (it found 280 "pages" for a 137-page
|
||||||
|
// document). Qt writes a single, flat /Kids array listing every page.
|
||||||
|
QVector<int> pageObjs;
|
||||||
|
{
|
||||||
|
int pagesPos = data.indexOf("/Type /Pages");
|
||||||
|
int kidsPos = (pagesPos == -1) ? -1 : data.indexOf("/Kids", pagesPos);
|
||||||
|
int lb = (kidsPos == -1) ? -1 : data.indexOf('[', kidsPos);
|
||||||
|
int rb = (lb == -1) ? -1 : data.indexOf(']', lb);
|
||||||
|
if (lb != -1 && rb != -1 && rb > lb) {
|
||||||
|
const QString kids =
|
||||||
|
QString::fromLatin1(data.mid(lb + 1, rb - lb - 1));
|
||||||
|
QRegularExpression re(QStringLiteral("(\\d+)\\s+\\d+\\s+R"));
|
||||||
|
auto it = re.globalMatch(kids);
|
||||||
|
while (it.hasNext()) {
|
||||||
|
int objNum = it.next().captured(1).toInt();
|
||||||
|
if (objNum > 0) pageObjs.append(objNum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageObjs.isEmpty()) return; // nothing to do
|
||||||
|
|
||||||
|
// --- 3. Replace URI annotations with GoTo ---
|
||||||
|
// Pattern (Qt always writes exactly this):
|
||||||
|
// /S /URI\n/URI (file:///...<anything>#page=N)\n
|
||||||
|
// or (older patches without file://):
|
||||||
|
// /S /URI\n/URI (page=N)\n
|
||||||
|
bool changed = false;
|
||||||
|
{
|
||||||
|
// We do a manual scan to handle variable-length replacements.
|
||||||
|
QByteArray out;
|
||||||
|
out.reserve(data.size());
|
||||||
|
|
||||||
|
const QByteArray sUri = "/S /URI\n/URI (";
|
||||||
|
const QByteArray sGoTo = "/S /GoTo\n/D [";
|
||||||
|
int pos = 0;
|
||||||
|
|
||||||
|
while (pos < data.size()) {
|
||||||
|
int found = data.indexOf(sUri, pos);
|
||||||
|
if (found == -1) {
|
||||||
|
out.append(data.mid(pos));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy everything up to the match
|
||||||
|
out.append(data.mid(pos, found - pos));
|
||||||
|
|
||||||
|
// Find closing ')' of the URI value
|
||||||
|
int uriStart = found + sUri.size();
|
||||||
|
int closeParen = data.indexOf(')', uriStart);
|
||||||
|
if (closeParen == -1) {
|
||||||
|
// Malformed — copy rest verbatim
|
||||||
|
out.append(data.mid(found));
|
||||||
|
pos = data.size();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray uriVal = data.mid(uriStart, closeParen - uriStart);
|
||||||
|
|
||||||
|
// Extract page number: look for #page=N or bare page=N
|
||||||
|
int pageNum = -1;
|
||||||
|
int hashPos = uriVal.lastIndexOf("#page=");
|
||||||
|
int digitStart = -1;
|
||||||
|
if (hashPos != -1) {
|
||||||
|
digitStart = hashPos + 6;
|
||||||
|
} else if (uriVal.startsWith("page=")) {
|
||||||
|
digitStart = 5;
|
||||||
|
}
|
||||||
|
if (digitStart != -1) {
|
||||||
|
// Take only the leading digits: the fragment may carry extra
|
||||||
|
// parameters after the page number (e.g. "22&fitr=15_489_..."),
|
||||||
|
// and QByteArray::toInt() would fail on the whole remainder.
|
||||||
|
int e = digitStart;
|
||||||
|
while (e < uriVal.size()
|
||||||
|
&& uriVal[e] >= '0' && uriVal[e] <= '9')
|
||||||
|
++e;
|
||||||
|
if (e > digitStart)
|
||||||
|
pageNum = uriVal.mid(digitStart, e - digitStart).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageNum >= 1 && pageNum <= pageObjs.size()) {
|
||||||
|
// Valid page reference — emit GoTo action.
|
||||||
|
int pageObjNum = pageObjs[pageNum - 1];
|
||||||
|
|
||||||
|
// Optional precise destination: &fitr=Left_Bottom_Right_Top
|
||||||
|
// (integer PDF points). If present -> /FitR (frame the element);
|
||||||
|
// otherwise -> /Fit (whole page, top).
|
||||||
|
QByteArray dest = " /Fit]";
|
||||||
|
int fr = uriVal.indexOf("fitr=");
|
||||||
|
if (fr != -1) {
|
||||||
|
QByteArray rest = uriVal.mid(fr + 5);
|
||||||
|
// stop at first char that is not part of the number list
|
||||||
|
int end = 0;
|
||||||
|
while (end < rest.size()
|
||||||
|
&& ((rest[end] >= '0' && rest[end] <= '9')
|
||||||
|
|| rest[end] == '_' || rest[end] == '-'))
|
||||||
|
++end;
|
||||||
|
QList<QByteArray> parts = rest.left(end).split('_');
|
||||||
|
if (parts.size() == 4) {
|
||||||
|
dest = " /FitR " + parts[0] + " " + parts[1] + " "
|
||||||
|
+ parts[2] + " " + parts[3] + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray goTo = sGoTo
|
||||||
|
+ QByteArray::number(pageObjNum)
|
||||||
|
+ " 0 R" + dest;
|
||||||
|
out.append(goTo);
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
// Unknown page — keep original URI
|
||||||
|
out.append(sUri);
|
||||||
|
out.append(uriVal);
|
||||||
|
out.append(')');
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = closeParen + 1; // skip past ')'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) return; // nothing was replaced
|
||||||
|
data = out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 4. Rebuild xref table ---
|
||||||
|
// Find start of existing xref (last occurrence)
|
||||||
|
int xrefStart = data.lastIndexOf("\nxref\n");
|
||||||
|
if (xrefStart == -1) xrefStart = data.lastIndexOf("\nxref ");
|
||||||
|
if (xrefStart == -1) return; // malformed PDF
|
||||||
|
++xrefStart; // skip the leading '\n'
|
||||||
|
|
||||||
|
QByteArray body = data.left(xrefStart);
|
||||||
|
|
||||||
|
// Collect all object offsets from the body
|
||||||
|
QMap<int, int> offsets; // objNum -> byte offset
|
||||||
|
{
|
||||||
|
const QByteArray objMarker = " 0 obj";
|
||||||
|
int pos = 0;
|
||||||
|
while ((pos = body.indexOf(objMarker, pos)) != -1) {
|
||||||
|
int numStart = pos - 1;
|
||||||
|
while (numStart > 0 && body[numStart-1] != '\n' && body[numStart-1] != '\r')
|
||||||
|
--numStart;
|
||||||
|
QByteArray numStr = body.mid(numStart, pos - numStart).trimmed();
|
||||||
|
bool ok = false;
|
||||||
|
int objNum = numStr.toInt(&ok);
|
||||||
|
if (ok && objNum > 0)
|
||||||
|
offsets[objNum] = numStart;
|
||||||
|
++pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offsets.isEmpty()) return;
|
||||||
|
|
||||||
|
int maxObj = offsets.lastKey();
|
||||||
|
|
||||||
|
// Build xref table
|
||||||
|
QByteArray xref;
|
||||||
|
xref += "xref\n";
|
||||||
|
xref += "0 " + QByteArray::number(maxObj + 1) + "\n";
|
||||||
|
xref += "0000000000 65535 f \n";
|
||||||
|
for (int i = 1; i <= maxObj; ++i) {
|
||||||
|
if (offsets.contains(i)) {
|
||||||
|
xref += QByteArray::number(offsets[i]).rightJustified(10, '0')
|
||||||
|
+ " 00000 n \n";
|
||||||
|
} else {
|
||||||
|
xref += "0000000000 65535 f \n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find trailer dict from the original xref section
|
||||||
|
int trailerPos = data.indexOf("trailer", xrefStart);
|
||||||
|
int trailerEnd = -1;
|
||||||
|
if (trailerPos != -1) {
|
||||||
|
trailerEnd = data.indexOf("%%EOF", trailerPos);
|
||||||
|
if (trailerEnd != -1) trailerEnd += 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray trailer;
|
||||||
|
if (trailerPos != -1 && trailerEnd != -1)
|
||||||
|
trailer = data.mid(trailerPos, trailerEnd - trailerPos);
|
||||||
|
else
|
||||||
|
trailer = "trailer\n<<>>\n%%EOF";
|
||||||
|
|
||||||
|
int newXrefOffset = body.size();
|
||||||
|
|
||||||
|
QByteArray result;
|
||||||
|
result.reserve(body.size() + xref.size() + trailer.size() + 30);
|
||||||
|
result += body;
|
||||||
|
result += xref;
|
||||||
|
result += trailer;
|
||||||
|
result += "\nstartxref\n";
|
||||||
|
result += QByteArray::number(newXrefOffset);
|
||||||
|
result += "\n%%EOF\n";
|
||||||
|
|
||||||
|
// --- 5. Write back ---
|
||||||
|
QFile out(pdfPath);
|
||||||
|
if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) return;
|
||||||
|
out.write(result);
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace PdfLinks
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2006-2025 The QElectroTech Team
|
||||||
|
This file is part of QElectroTech.
|
||||||
|
|
||||||
|
QElectroTech is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
QElectroTech is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#ifndef PDF_LINKS_H
|
||||||
|
#define PDF_LINKS_H
|
||||||
|
|
||||||
|
#include <QMap>
|
||||||
|
#include <QPointF>
|
||||||
|
#include <QRectF>
|
||||||
|
#include <QString>
|
||||||
|
#include <QTransform>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
class QPdfEngine;
|
||||||
|
class Diagram;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Shared helper that turns a project's cross-references and folio reports
|
||||||
|
into clickable internal hyperlinks in a Qt-generated PDF. Used by both the
|
||||||
|
GUI print path (ProjectPrintWindow) and the headless CLI export, each of
|
||||||
|
which builds its own page geometry and passes it in — this code never
|
||||||
|
computes the scene-to-page mapping itself.
|
||||||
|
*/
|
||||||
|
namespace PdfLinks {
|
||||||
|
|
||||||
|
/**
|
||||||
|
Geometry mapping for one rendered PDF page. Each caller builds this
|
||||||
|
from its OWN page setup (printer page layout vs QPdfWriter), since the
|
||||||
|
device-pixel and point conversions differ between them.
|
||||||
|
*/
|
||||||
|
struct PageGeometry {
|
||||||
|
/// scene coordinates -> device pixels (the same "fit" render() applied)
|
||||||
|
QTransform sceneToDevice;
|
||||||
|
/// device paint rectangle, in pixels (the page area)
|
||||||
|
QRectF target;
|
||||||
|
/// links whose rectangle falls outside this are dropped
|
||||||
|
QRectF pageBounds;
|
||||||
|
/// device pixels -> PDF points (replicates the engine's page matrix)
|
||||||
|
std::function<QPointF(const QPointF &)> devToPdf;
|
||||||
|
/// a diagram -> its source rectangle in scene pixels (for /FitR framing)
|
||||||
|
std::function<QRectF(Diagram *)> sourceRectOf;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
Inject clickable cross-reference / folio-report hyperlinks for @p diagram
|
||||||
|
into the current page of @p engine. Each link is emitted as a URI
|
||||||
|
annotation encoding the target page and a /FitR rectangle;
|
||||||
|
convertUriToGoTo() then rewrites those into native internal GoTo actions.
|
||||||
|
*/
|
||||||
|
void injectCrossRefLinks(QPdfEngine *engine, Diagram *diagram,
|
||||||
|
const PageGeometry &geom,
|
||||||
|
const QMap<Diagram *, int> &pageMap,
|
||||||
|
const QString &outputFileName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
Post-process a Qt-generated PDF file: rewrite every "/S /URI" link
|
||||||
|
annotation into a native internal "/S /GoTo" action (page + /FitR or
|
||||||
|
/Fit destination) and rebuild the xref table. No-op if the file has no
|
||||||
|
such annotations.
|
||||||
|
*/
|
||||||
|
void convertUriToGoTo(const QString &pdfPath);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // PDF_LINKS_H
|
||||||
@@ -18,12 +18,20 @@
|
|||||||
#include "projectprintwindow.h"
|
#include "projectprintwindow.h"
|
||||||
|
|
||||||
#include "../diagram.h"
|
#include "../diagram.h"
|
||||||
|
#include "../pdf_links.h"
|
||||||
#include "../qeticons.h"
|
#include "../qeticons.h"
|
||||||
#include "../qetproject.h"
|
#include "../qetproject.h"
|
||||||
#include "../qetversion.h"
|
#include "../qetversion.h"
|
||||||
|
#include "../qetgraphicsitem/crossrefitem.h"
|
||||||
|
#include "../qetgraphicsitem/dynamicelementtextitem.h"
|
||||||
|
#include "../qetgraphicsitem/elementtextitemgroup.h"
|
||||||
|
|
||||||
#include "ui_projectprintwindow.h"
|
#include "ui_projectprintwindow.h"
|
||||||
|
|
||||||
|
// Private Qt PDF engine for drawHyperlink() — not public API, stable since Qt4
|
||||||
|
// Requires QT += gui-private in qelectrotech.pro
|
||||||
|
#include <private/qpdf_p.h>
|
||||||
|
|
||||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
|
||||||
# include <QDesktopWidget>
|
# include <QDesktopWidget>
|
||||||
#else
|
#else
|
||||||
@@ -37,6 +45,11 @@
|
|||||||
#include <QPrintDialog>
|
#include <QPrintDialog>
|
||||||
#include <QPrintPreviewWidget>
|
#include <QPrintPreviewWidget>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief ProjectPrintWindow::ProjectPrintWindow
|
* @brief ProjectPrintWindow::ProjectPrintWindow
|
||||||
@@ -190,40 +203,71 @@ ProjectPrintWindow::~ProjectPrintWindow()
|
|||||||
*/
|
*/
|
||||||
void ProjectPrintWindow::requestPaint()
|
void ProjectPrintWindow::requestPaint()
|
||||||
{
|
{
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
auto screen = this->screen();
|
#ifdef QT_DEBUG
|
||||||
if(screen)
|
qDebug() << "--";
|
||||||
{
|
qDebug() << "DiagramPrintDialog::print printer_->resolution() before " << m_printer->resolution();
|
||||||
#ifdef QT_DEBUG
|
qDebug() << "DiagramPrintDialog::print screennumber " << QApplication::desktop()->screenNumber();
|
||||||
qDebug() << "--";
|
#endif
|
||||||
qDebug() << "DiagramPrintDialog::print printer_->resolution() before " << m_printer->resolution();
|
|
||||||
qDebug() << "DiagramPrintDialog::print screennumber " << screen->name();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
qreal dotsPerInch = (qreal)screen->logicalDotsPerInch();
|
QScreen *srn = QApplication::screens().at(QApplication::desktop()->screenNumber());
|
||||||
m_printer->setResolution(dotsPerInch);
|
qreal dotsPerInch = (qreal)srn->logicalDotsPerInch();
|
||||||
|
m_printer->setResolution(dotsPerInch);
|
||||||
|
|
||||||
#ifdef QT_DEBUG
|
#ifdef QT_DEBUG
|
||||||
qDebug() << "DiagramPrintDialog::print dotsPerInch " << dotsPerInch;
|
qDebug() << "DiagramPrintDialog::print dotsPerInch " << dotsPerInch;
|
||||||
qDebug() << "DiagramPrintDialog::print printer_->resolution() after" << m_printer->resolution();
|
qDebug() << "DiagramPrintDialog::print printer_->resolution() after" << m_printer->resolution();
|
||||||
qDebug() << "--";
|
qDebug() << "--";
|
||||||
#endif
|
#endif
|
||||||
}
|
#endif
|
||||||
#endif
|
#endif
|
||||||
#endif
|
|
||||||
|
|
||||||
if (!m_project->diagrams().count()) {
|
if (!m_project->diagrams().count()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build diagram -> first physical PDF page number map (1-based)
|
||||||
|
// Must be done before the print loop since page numbers depend on order
|
||||||
|
QMap<Diagram*, int> diagramPageMap;
|
||||||
|
{
|
||||||
|
int pageNum = 1;
|
||||||
|
for (auto diagram : selectedDiagram()) {
|
||||||
|
diagramPageMap.insert(diagram, pageNum);
|
||||||
|
// Each diagram may span multiple pages if not fit_page
|
||||||
|
if (!ui->m_fit_in_page_cb->isChecked()) {
|
||||||
|
auto option = exportProperties();
|
||||||
|
bool full_page = m_printer->fullPage();
|
||||||
|
int h = horizontalPagesCount(diagram, option, full_page);
|
||||||
|
int v = verticalPagesCount(diagram, option, full_page);
|
||||||
|
pageNum += h * v;
|
||||||
|
} else {
|
||||||
|
pageNum += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool first = true;
|
bool first = true;
|
||||||
QPainter painter(m_printer);
|
QPainter painter(m_printer);
|
||||||
|
|
||||||
|
// A real PDF export uses the QPdfEngine; the on-screen preview uses a
|
||||||
|
// preview paint engine. We only post-process when actually writing a PDF.
|
||||||
|
const bool pdfExport =
|
||||||
|
(m_printer->outputFormat() == QPrinter::PdfFormat)
|
||||||
|
&& (dynamic_cast<QPdfEngine*>(painter.paintEngine()) != nullptr);
|
||||||
|
|
||||||
for (auto diagram : selectedDiagram())
|
for (auto diagram : selectedDiagram())
|
||||||
{
|
{
|
||||||
first ? first = false : m_printer->newPage();
|
first ? first = false : m_printer->newPage();
|
||||||
printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer);
|
printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer, diagramPageMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: do NOT call painter.end() or pdfConvertUriToGoTo() here.
|
||||||
|
// We are inside the paintRequested slot: the QPrintPreviewWidget still
|
||||||
|
// owns the paint cycle. On macOS arm64 (Metal/CALayer compositor),
|
||||||
|
// closing the QPainter manually inside this slot leaves the backing
|
||||||
|
// store in an undefined state, producing a black screen after export.
|
||||||
|
// pdfConvertUriToGoTo() is deferred to print() via QTimer::singleShot(0).
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -233,7 +277,7 @@ void ProjectPrintWindow::requestPaint()
|
|||||||
* @param fit_page
|
* @param fit_page
|
||||||
* @param printer
|
* @param printer
|
||||||
*/
|
*/
|
||||||
void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer)
|
void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap<Diagram*, int> &diagramPageMap)
|
||||||
{
|
{
|
||||||
|
|
||||||
////Prepare the print////
|
////Prepare the print////
|
||||||
@@ -268,9 +312,9 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter
|
|||||||
#if TODO_LIST
|
#if TODO_LIST
|
||||||
#pragma message("@TODO remove code for QT 6 or later")
|
#pragma message("@TODO remove code for QT 6 or later")
|
||||||
#endif
|
#endif
|
||||||
qDebug()<<"Help code for QT 6 or later";
|
qDebug()<<"Help code for QT 6 or later";
|
||||||
auto printed_rect = full_page ? printer->paperRect(QPrinter::Millimeter) :
|
auto printed_rect = full_page ? printer->paperRect(QPrinter::Millimeter) :
|
||||||
printer->pageRect(QPrinter::Millimeter);
|
printer->pageRect(QPrinter::Millimeter);
|
||||||
#endif
|
#endif
|
||||||
auto used_width = printed_rect.width();
|
auto used_width = printed_rect.width();
|
||||||
auto used_height = printed_rect.height();
|
auto used_height = printed_rect.height();
|
||||||
@@ -320,6 +364,65 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////Inject PDF cross-reference links////
|
||||||
|
if (printer->outputFormat() == QPrinter::PdfFormat && fit_page) {
|
||||||
|
auto *pdfEngine = dynamic_cast<QPdfEngine*>(painter->paintEngine());
|
||||||
|
if (pdfEngine) {
|
||||||
|
|
||||||
|
// QGraphicsScene::render() fait save()/restore() : worldTransform()
|
||||||
|
// est revenu a l'identite ici. On reconstruit DONC explicitement la
|
||||||
|
// transform appliquee par :
|
||||||
|
// diagram->render(painter, QRectF(), diagram_rect, KeepAspectRatio)
|
||||||
|
// cible vide => painter->viewport() ; source = diagram_rect ; centre.
|
||||||
|
const QRectF target = QRectF(painter->viewport());
|
||||||
|
const QRectF source = QRectF(diagram_rect); // meme source que render()
|
||||||
|
|
||||||
|
// render() ANCRE en haut-gauche (pas de centrage) :
|
||||||
|
// translate(target.topLeft) . scale(s,s) . translate(-source.topLeft)
|
||||||
|
// On reproduit EXACTEMENT ca — surtout PAS de (target-source*s)/2.
|
||||||
|
const qreal s = qMin(target.width() / source.width(),
|
||||||
|
target.height() / source.height());
|
||||||
|
|
||||||
|
QTransform fit;
|
||||||
|
fit.translate(target.x(), target.y());
|
||||||
|
fit.scale(s, s);
|
||||||
|
fit.translate(-source.x(), -source.y()); // scene -> pixels device
|
||||||
|
|
||||||
|
// IMPORTANT : QPdfEngine::drawHyperlink() applique lui-meme
|
||||||
|
// pageMatrix() (echelle 72/resolution + inversion de Y + marges).
|
||||||
|
// On lui passe donc le rectangle en PIXELS DEVICE, sans aucune
|
||||||
|
// conversion en points ni flip de notre cote.
|
||||||
|
const QRectF pageBounds(0, 0, target.width(), target.height());
|
||||||
|
|
||||||
|
// ---- Device-pixels -> PDF points, replicating QPdfEnginePrivate::pageMatrix()
|
||||||
|
// (same geometry for every page: same printer, page size and margins). ----
|
||||||
|
const qreal pt_scale = 72.0 / printer->resolution();
|
||||||
|
const qreal fullH_pt = printer->pageLayout().fullRectPoints().height();
|
||||||
|
const bool fullPageMode =
|
||||||
|
(printer->pageLayout().mode() == QPageLayout::FullPageMode);
|
||||||
|
const QRect paintPx =
|
||||||
|
printer->pageLayout().paintRectPixels(printer->resolution());
|
||||||
|
auto devToPdf = [=](const QPointF &d) -> QPointF {
|
||||||
|
qreal dx = d.x(), dy = d.y();
|
||||||
|
if (!fullPageMode) { dx += paintPx.left(); dy += paintPx.top(); }
|
||||||
|
return QPointF(pt_scale * dx, fullH_pt - pt_scale * dy);
|
||||||
|
};
|
||||||
|
|
||||||
|
PdfLinks::PageGeometry geom;
|
||||||
|
geom.sceneToDevice = fit;
|
||||||
|
geom.target = target;
|
||||||
|
geom.pageBounds = pageBounds;
|
||||||
|
geom.devToPdf = devToPdf;
|
||||||
|
geom.sourceRectOf = [this](Diagram *dg) {
|
||||||
|
return QRectF(diagramRect(dg, exportProperties()));
|
||||||
|
};
|
||||||
|
PdfLinks::injectCrossRefLinks(
|
||||||
|
pdfEngine, diagram, geom, diagramPageMap,
|
||||||
|
printer->outputFileName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
////PDF links end////
|
||||||
|
|
||||||
////Print is finished, restore diagram and graphics item properties
|
////Print is finished, restore diagram and graphics item properties
|
||||||
for (auto view : diagram->views()) {
|
for (auto view : diagram->views()) {
|
||||||
view->setInteractive(true);
|
view->setInteractive(true);
|
||||||
@@ -344,7 +447,7 @@ QRect ProjectPrintWindow::diagramRect(Diagram *diagram, const ExportProperties &
|
|||||||
diagram_rect.setHeight(diagram_rect.height() - titleblock_height);
|
diagram_rect.setHeight(diagram_rect.height() - titleblock_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Adjust the border of diagram to 1px (width of the line)
|
//Adjust the border of diagram to 1px (width of the line)
|
||||||
diagram_rect.adjust(0,0,1,1);
|
diagram_rect.adjust(0,0,1,1);
|
||||||
|
|
||||||
return (diagram_rect.toAlignedRect());
|
return (diagram_rect.toAlignedRect());
|
||||||
@@ -359,7 +462,7 @@ QRect ProjectPrintWindow::diagramRect(Diagram *diagram, const ExportProperties &
|
|||||||
* with the orientation and the paper format used by the actual printer
|
* with the orientation and the paper format used by the actual printer
|
||||||
*/
|
*/
|
||||||
int ProjectPrintWindow::horizontalPagesCount(
|
int ProjectPrintWindow::horizontalPagesCount(
|
||||||
Diagram *diagram, const ExportProperties &option, bool full_page) const
|
Diagram *diagram, const ExportProperties &option, bool full_page) const
|
||||||
{
|
{
|
||||||
QRect printable_area;
|
QRect printable_area;
|
||||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||||
@@ -388,7 +491,7 @@ int ProjectPrintWindow::horizontalPagesCount(
|
|||||||
* with the orientation and paper format used by the actual printer
|
* with the orientation and paper format used by the actual printer
|
||||||
*/
|
*/
|
||||||
int ProjectPrintWindow::verticalPagesCount(
|
int ProjectPrintWindow::verticalPagesCount(
|
||||||
Diagram *diagram, const ExportProperties &option, bool full_page) const
|
Diagram *diagram, const ExportProperties &option, bool full_page) const
|
||||||
{
|
{
|
||||||
QRect printable_area;
|
QRect printable_area;
|
||||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||||
@@ -514,7 +617,7 @@ void ProjectPrintWindow::loadPageSetupForCurrentPrinter()
|
|||||||
QString value = settings.value("orientation", "landscape").toString();
|
QString value = settings.value("orientation", "landscape").toString();
|
||||||
m_printer->setPageOrientation(
|
m_printer->setPageOrientation(
|
||||||
value == "landscape" ? QPageLayout::Landscape :
|
value == "landscape" ? QPageLayout::Landscape :
|
||||||
QPageLayout::Portrait);
|
QPageLayout::Portrait);
|
||||||
}
|
}
|
||||||
if (settings.contains("papersize"))
|
if (settings.contains("papersize"))
|
||||||
{
|
{
|
||||||
@@ -775,17 +878,37 @@ void ProjectPrintWindow::on_m_uncheck_all_clicked()
|
|||||||
|
|
||||||
void ProjectPrintWindow::print()
|
void ProjectPrintWindow::print()
|
||||||
{
|
{
|
||||||
m_preview->print();
|
const bool isPdf = (m_printer->outputFormat() == QPrinter::PdfFormat);
|
||||||
|
const QString pdfFile = isPdf ? m_printer->outputFileName() : QString();
|
||||||
|
|
||||||
|
m_preview->print(); // triggers requestPaint() synchronously; painter
|
||||||
|
// is created/destroyed inside that call
|
||||||
|
|
||||||
savePageSetupForCurrentPrinter();
|
savePageSetupForCurrentPrinter();
|
||||||
this->close();
|
|
||||||
|
if (isPdf && !pdfFile.isEmpty()) {
|
||||||
|
// Defer post-processing and window close to the next event-loop
|
||||||
|
// iteration. This lets the macOS arm64 Metal compositor finish
|
||||||
|
// compositing the backing store before the window is destroyed,
|
||||||
|
// which prevents the black screen observed on Apple Silicon under
|
||||||
|
// macOS Sequoia (QPrintPreviewWidget + CALayer timing issue).
|
||||||
|
QTimer::singleShot(0, this, [this, pdfFile]() {
|
||||||
|
// Convert URI link annotations into native internal GoTo/FitR
|
||||||
|
// actions so cross-references jump inside the document.
|
||||||
|
PdfLinks::convertUriToGoTo(pdfFile);
|
||||||
|
this->close();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this->close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProjectPrintWindow::on_m_date_cb_userDateChanged(const QDate &date)
|
void ProjectPrintWindow::on_m_date_cb_userDateChanged(const QDate &date)
|
||||||
{
|
{
|
||||||
auto index = ui->m_date_from_cb->currentIndex();
|
auto index = ui->m_date_from_cb->currentIndex();
|
||||||
// 0 = all date
|
// 0 = all date
|
||||||
// 1 = from the date
|
// 1 = from the date
|
||||||
// 2 = at the date
|
// 2 = at the date
|
||||||
|
|
||||||
if (index) { on_m_uncheck_all_clicked(); }
|
if (index) { on_m_uncheck_all_clicked(); }
|
||||||
else { on_m_check_all_pb_clicked(); }
|
else { on_m_check_all_pb_clicked(); }
|
||||||
@@ -795,7 +918,7 @@ void ProjectPrintWindow::on_m_date_cb_userDateChanged(const QDate &date)
|
|||||||
{
|
{
|
||||||
auto diagram_date = diagram->border_and_titleblock.date();
|
auto diagram_date = diagram->border_and_titleblock.date();
|
||||||
if ( (index == 1 && diagram_date >= date) ||
|
if ( (index == 1 && diagram_date >= date) ||
|
||||||
(index == 2 && diagram_date == date) )
|
(index == 2 && diagram_date == date) )
|
||||||
m_diagram_list_hash.value(diagram)->setChecked(true);
|
m_diagram_list_hash.value(diagram)->setChecked(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include "../exportproperties.h"
|
#include "../exportproperties.h"
|
||||||
|
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
|
#include <QMap>
|
||||||
#include <QPrinter>
|
#include <QPrinter>
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
@@ -79,7 +80,7 @@ class ProjectPrintWindow : public QMainWindow
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void requestPaint();
|
void requestPaint();
|
||||||
void printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer);
|
void printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap<Diagram*, int> &diagramPageMap = {});
|
||||||
QRect diagramRect(Diagram *diagram, const ExportProperties &option) const;
|
QRect diagramRect(Diagram *diagram, const ExportProperties &option) const;
|
||||||
int horizontalPagesCount(Diagram *diagram, const ExportProperties &option, bool full_page) const;
|
int horizontalPagesCount(Diagram *diagram, const ExportProperties &option, bool full_page) const;
|
||||||
int verticalPagesCount(Diagram *diagram, const ExportProperties &option, bool full_page) const;
|
int verticalPagesCount(Diagram *diagram, const ExportProperties &option, bool full_page) const;
|
||||||
|
|||||||
@@ -183,7 +183,16 @@ bool QET::orthogonalProjection(
|
|||||||
|
|
||||||
// determine le point d'intersection des deux droites = le projete orthogonal
|
// determine le point d'intersection des deux droites = le projete orthogonal
|
||||||
QPointF intersection_point;
|
QPointF intersection_point;
|
||||||
QLineF::IntersectType it = line.intersects(perpendicular_line, &intersection_point);
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 5.14 or later")
|
||||||
|
#endif
|
||||||
|
QLineF::IntersectType it = line.
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
|
||||||
|
intersect // ### Qt 6: remove
|
||||||
|
#else
|
||||||
|
intersects
|
||||||
|
#endif
|
||||||
|
(perpendicular_line, &intersection_point);
|
||||||
|
|
||||||
// ne devrait pas arriver (mais bon...)
|
// ne devrait pas arriver (mais bon...)
|
||||||
if (it == QLineF::NoIntersection) return(false);
|
if (it == QLineF::NoIntersection) return(false);
|
||||||
@@ -536,8 +545,16 @@ QString QET::joinWithSpaces(const QStringList &string_list) {
|
|||||||
QStringList QET::splitWithSpaces(const QString &string) {
|
QStringList QET::splitWithSpaces(const QString &string) {
|
||||||
// les chaines sont separees par des espaces non echappes
|
// les chaines sont separees par des espaces non echappes
|
||||||
// = avec un nombre nul ou pair de backslashes devant
|
// = avec un nombre nul ou pair de backslashes devant
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 5.14 or later")
|
||||||
|
#endif
|
||||||
QStringList escaped_strings = string.split(QRegularExpression("[^\\]?(?:\\\\)* "),
|
QStringList escaped_strings = string.split(QRegularExpression("[^\\]?(?:\\\\)* "),
|
||||||
Qt::SkipEmptyParts);
|
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
||||||
|
QString
|
||||||
|
#else
|
||||||
|
Qt
|
||||||
|
#endif
|
||||||
|
::SkipEmptyParts);
|
||||||
|
|
||||||
QStringList returned_list;
|
QStringList returned_list;
|
||||||
foreach(QString escaped_string, escaped_strings) {
|
foreach(QString escaped_string, escaped_strings) {
|
||||||
@@ -667,7 +684,14 @@ bool QET::writeXmlFile(QDomDocument &xml_doc, const QString &filepath, QString *
|
|||||||
}
|
}
|
||||||
|
|
||||||
QTextStream out(&file);
|
QTextStream out(&file);
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
|
||||||
|
out.setCodec("UTF-8");
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 6 or later")
|
||||||
|
#endif
|
||||||
out.setEncoding(QStringConverter::Utf8);
|
out.setEncoding(QStringConverter::Utf8);
|
||||||
|
#endif
|
||||||
out.setGenerateByteOrderMark(false);
|
out.setGenerateByteOrderMark(false);
|
||||||
out << xml_doc.toString(4);
|
out << xml_doc.toString(4);
|
||||||
if (!file.commit())
|
if (!file.commit())
|
||||||
@@ -798,7 +822,14 @@ bool QET::writeToFile(QDomDocument &xml_doc, QFile *file, QString *error_message
|
|||||||
|
|
||||||
QTextStream out(file);
|
QTextStream out(file);
|
||||||
out.seek(0);
|
out.seek(0);
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
|
||||||
|
out.setCodec("UTF-8");
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 6 or later")
|
||||||
|
#endif
|
||||||
out.setEncoding(QStringConverter::Utf8);
|
out.setEncoding(QStringConverter::Utf8);
|
||||||
|
#endif
|
||||||
out.setGenerateByteOrderMark(false);
|
out.setGenerateByteOrderMark(false);
|
||||||
out << xml_doc.toString(4);
|
out << xml_doc.toString(4);
|
||||||
if (opened_here) {
|
if (opened_here) {
|
||||||
|
|||||||
@@ -204,7 +204,14 @@ void QETApp::setLanguage(const QString &desired_language) {
|
|||||||
QString languages_path = languagesPath();
|
QString languages_path = languagesPath();
|
||||||
|
|
||||||
// load Qt library translations
|
// load Qt library translations
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
|
||||||
|
QString qt_l10n_path = QLibraryInfo::location(QLibraryInfo::TranslationsPath);
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 6 or later")
|
||||||
|
#endif
|
||||||
QString qt_l10n_path = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
|
QString qt_l10n_path = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
|
||||||
|
#endif
|
||||||
if (!qtTranslator.load("qt_" + desired_language, qt_l10n_path))
|
if (!qtTranslator.load("qt_" + desired_language, qt_l10n_path))
|
||||||
{
|
{
|
||||||
qWarning() << "failed to load"
|
qWarning() << "failed to load"
|
||||||
@@ -219,20 +226,20 @@ void QETApp::setLanguage(const QString &desired_language) {
|
|||||||
|
|
||||||
// load translations for the QET application
|
// load translations for the QET application
|
||||||
// charge les traductions pour l'application QET
|
// charge les traductions pour l'application QET
|
||||||
if (!qetTranslator.load("qet_" + desired_language, languages_path)) {
|
// desired_language may be a full locale such as "pt_BR": try that exact
|
||||||
/* in case of failure,
|
// translation, then the base language ("pt"), then fall back to English.
|
||||||
* we fall back on the native channels for French
|
// French is the application's source language and needs no translation.
|
||||||
* en cas d'echec,
|
const QString base_language = desired_language.section('_', 0, 0);
|
||||||
* on retombe sur les chaines natives pour le francais
|
bool loaded = qetTranslator.load("qet_" + desired_language, languages_path);
|
||||||
*/
|
if (!loaded && base_language != desired_language)
|
||||||
if (desired_language != "fr") {
|
loaded = qetTranslator.load("qet_" + base_language, languages_path);
|
||||||
// use of the English version by default
|
if (!loaded && base_language != "fr") {
|
||||||
// utilisation de la version anglaise par defaut
|
// use of the English version by default
|
||||||
if(!qetTranslator.load("qet_en", languages_path))
|
// utilisation de la version anglaise par defaut
|
||||||
qWarning() << "failed to load"
|
if(!qetTranslator.load("qet_en", languages_path))
|
||||||
<< "qet_en" << languages_path << "(" << __FILE__
|
qWarning() << "failed to load"
|
||||||
<< __LINE__ << __FUNCTION__ << ")";
|
<< "qet_en" << languages_path << "(" << __FILE__
|
||||||
}
|
<< __LINE__ << __FUNCTION__ << ")";
|
||||||
}
|
}
|
||||||
qApp->installTranslator(&qetTranslator);
|
qApp->installTranslator(&qetTranslator);
|
||||||
|
|
||||||
@@ -256,7 +263,11 @@ QString QETApp::langFromSetting()
|
|||||||
QSettings settings;
|
QSettings settings;
|
||||||
system_language = settings.value("lang", "system").toString();
|
system_language = settings.value("lang", "system").toString();
|
||||||
if(system_language == "system") {
|
if(system_language == "system") {
|
||||||
system_language = QLocale::system().name().left(2);
|
// Keep the full locale (e.g. "pt_BR"), not just the base language
|
||||||
|
// ("pt"): QET ships regional translations (pt_BR, nl_BE, nl_NL) and
|
||||||
|
// truncating here loaded the wrong one. setLanguage() falls back to
|
||||||
|
// the base language when no regional translation exists.
|
||||||
|
system_language = QLocale::system().name();
|
||||||
}
|
}
|
||||||
lang_is_set = true;
|
lang_is_set = true;
|
||||||
}
|
}
|
||||||
@@ -1221,7 +1232,21 @@ QString QETApp::languagesPath()
|
|||||||
* en l'absence d'option de compilation, on utilise le dossier lang,
|
* en l'absence d'option de compilation, on utilise le dossier lang,
|
||||||
* situe a cote du binaire executable
|
* situe a cote du binaire executable
|
||||||
*/
|
*/
|
||||||
return(QCoreApplication::applicationDirPath() + "/lang/");
|
{
|
||||||
|
const QString bin_dir = QCoreApplication::applicationDirPath();
|
||||||
|
const QString next_to_bin = bin_dir + "/lang/";
|
||||||
|
// Some packagings (notably the Windows installer) put the binary in a
|
||||||
|
// "bin" subfolder while "lang" sits beside it (../lang). Fall back to
|
||||||
|
// that layout when the folder next to the binary is absent, so the
|
||||||
|
// translations are found without a --lang-dir argument. See issue #86.
|
||||||
|
if (!QDir(next_to_bin).exists()) {
|
||||||
|
const QString sibling_of_bin =
|
||||||
|
QDir::cleanPath(bin_dir + "/../lang") + "/";
|
||||||
|
if (QDir(sibling_of_bin).exists())
|
||||||
|
return(sibling_of_bin);
|
||||||
|
}
|
||||||
|
return(next_to_bin);
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
#ifndef QET_LANG_PATH_RELATIVE_TO_BINARY_PATH
|
#ifndef QET_LANG_PATH_RELATIVE_TO_BINARY_PATH
|
||||||
/* the compilation option represents
|
/* the compilation option represents
|
||||||
|
|||||||
@@ -451,8 +451,16 @@ void GraphicsTablePropertiesEditor::setUpEditConnection()
|
|||||||
m_edit_connection << connect(ui->m_table_left_margin, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply);
|
m_edit_connection << connect(ui->m_table_left_margin, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply);
|
||||||
m_edit_connection << connect(ui->m_table_right_margin, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply);
|
m_edit_connection << connect(ui->m_table_right_margin, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply);
|
||||||
m_edit_connection << connect(ui->m_table_bottom_margin, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply);
|
m_edit_connection << connect(ui->m_table_bottom_margin, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply);
|
||||||
m_edit_connection << connect(m_table_button_group, QOverload<int>::of(&QButtonGroup::idClicked), this, &GraphicsTablePropertiesEditor::apply);
|
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // ### Qt 6: remove
|
||||||
m_edit_connection << connect(m_header_button_group, QOverload<int>::of(&QButtonGroup::idClicked), this, &GraphicsTablePropertiesEditor::apply);
|
m_edit_connection << connect(m_table_button_group, QOverload<int>::of(&QButtonGroup::buttonClicked), this, &GraphicsTablePropertiesEditor::apply);
|
||||||
|
m_edit_connection << connect(m_header_button_group, QOverload<int>::of(&QButtonGroup::buttonClicked), this, &GraphicsTablePropertiesEditor::apply);
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 5.15 or later")
|
||||||
|
#endif
|
||||||
|
m_edit_connection << connect(m_table_button_group, QOverload<int>::of(&QButtonGroup::idClicked), this, &GraphicsTablePropertiesEditor::apply);
|
||||||
|
m_edit_connection << connect(m_header_button_group, QOverload<int>::of(&QButtonGroup::idClicked), this, &GraphicsTablePropertiesEditor::apply);
|
||||||
|
#endif
|
||||||
m_edit_connection << connect(ui->m_display_n_row_sb, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply);
|
m_edit_connection << connect(ui->m_display_n_row_sb, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply);
|
||||||
m_edit_connection << connect(ui->m_display_n_row_sb, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::updateInfoLabel);
|
m_edit_connection << connect(ui->m_display_n_row_sb, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::updateInfoLabel);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1725,7 +1725,14 @@ QSet<Conductor *> Conductor::relatedPotentialConductors(const bool all_diagram,
|
|||||||
for (Conductor *c : other_conductors_list_t) {
|
for (Conductor *c : other_conductors_list_t) {
|
||||||
other_conductors += c->relatedPotentialConductors(all_diagram, t_list);
|
other_conductors += c->relatedPotentialConductors(all_diagram, t_list);
|
||||||
}
|
}
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
||||||
|
other_conductors += other_conductors_list_t.toSet();
|
||||||
|
#else
|
||||||
|
#if TODO_LIST
|
||||||
|
#pragma message("@TODO remove code for QT 5.14 or later")
|
||||||
|
#endif
|
||||||
other_conductors += QSet<Conductor*>(other_conductors_list_t.begin(),other_conductors_list_t.end());
|
other_conductors += QSet<Conductor*>(other_conductors_list_t.begin(),other_conductors_list_t.end());
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||