mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-25 00:14:15 +02:00
Compare commits
333 Commits
resolve-1461
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0020e0cd13 | |||
| 2222153662 | |||
| 255def181c | |||
| 2f4af977b7 | |||
| bf59f963c1 | |||
| 5df19f6672 | |||
| 212e00c1a8 | |||
| af138558ba | |||
| 5a29d6edaf | |||
| 662e8a1d68 | |||
| ae943b1a95 | |||
| 9feadb0499 | |||
| 2a6de652f7 | |||
| f3e7bff4ad | |||
| 8d448a8117 | |||
| 8b59bcb992 | |||
| 6a6e558194 | |||
| 82c96b84d3 | |||
| 129648f92a | |||
| f6be424cad | |||
| bf940a7205 | |||
| 535a3b03a3 | |||
| f68927d0a4 | |||
| 93157a579d | |||
| 38107d4d73 | |||
| 5c2726d34b | |||
| 2ae9196a09 | |||
| 44c96a4c95 | |||
| df73934bfb | |||
| 208b5e1570 | |||
| 86563850f8 | |||
| 790497978a | |||
| b099c65115 | |||
| f8e9a1a457 | |||
| 337502edfc | |||
| edf761c2b6 | |||
| 489676b155 | |||
| b98193dbe2 | |||
| 56b24aaa0e | |||
| 3da3a78850 | |||
| dfec60a3a5 | |||
| bd70fc8b20 | |||
| 149d92545d | |||
| 6225b20f32 | |||
| 8d5c164d09 | |||
| f692d2d35a | |||
| 650e318e02 | |||
| 06d31d9921 | |||
| 139993a691 | |||
| 2bedfaa394 | |||
| 8ce3b214c1 | |||
| 7d75c72f97 | |||
| e7d644003f | |||
| 122678a797 | |||
| de248eb20a | |||
| 76a36bc5e9 | |||
| 0e5d4ad990 | |||
| f4a7d5ebbb | |||
| 537e3c3c10 | |||
| dc30b26dab | |||
| 04ad1c890a | |||
| 672ba7c409 | |||
| 025bd0a57e | |||
| d05cc654ad | |||
| 992058348b | |||
| 36b1cb08ff | |||
| 5baf5bec60 | |||
| dc92a57873 | |||
| 1bf076eca8 | |||
| 8f2cd9f039 | |||
| 214b0e3b7d | |||
| 52adc8b3f8 | |||
| 74f908aa1e | |||
| f828e464c4 | |||
| 25ffa414af | |||
| 9c3c19a316 | |||
| 0a577eaabf | |||
| 6e83012109 | |||
| 79ab711d93 | |||
| 3de7f525cc | |||
| aebe82d6c2 | |||
| eba05bed43 | |||
| 7a2b390b78 | |||
| 59c2776350 | |||
| 9827a12841 | |||
| b6312f1f78 | |||
| aa7f328aeb | |||
| 3a863d4b09 | |||
| d2bacfd526 | |||
| 29a8f018f4 | |||
| 123c56bb13 | |||
| 5641b031f9 | |||
| adf3ea301c | |||
| ee227a007e | |||
| c32ebc5936 | |||
| bab2707fff | |||
| 94a929c8c0 | |||
| 9e37fa2ca7 | |||
| 758703f50e | |||
| a12fb43bcb | |||
| e7a168d1f9 | |||
| a9715b5bf6 | |||
| f5ffbd6a9c | |||
| 4f0c493a7f | |||
| f994957965 | |||
| f6c9aeb177 | |||
| 3738b3c1d8 | |||
| 8fa58bd609 | |||
| 5837bec4fe | |||
| 3e2ad8c4bd | |||
| 4fee464919 | |||
| 2adbc17b3c | |||
| 64e8ca0263 | |||
| 289e33d06e | |||
| 46d8588113 | |||
| 31730678c5 | |||
| 7fcd6086a8 | |||
| 5292e290d1 | |||
| 1c0049e083 | |||
| 833ed0f523 | |||
| 7f05b23062 | |||
| 735995037c | |||
| 92ee1b6617 | |||
| 0bcd0ca4b6 | |||
| a5da2e9b8d | |||
| cafae25c31 | |||
| 6dce85f2fb | |||
| b4d69f176a | |||
| 266d1e2573 | |||
| 1e988eadfd | |||
| 3abaf21302 | |||
| 171a597b12 | |||
| 4d61909fd1 | |||
| 9421aff1b2 | |||
| 3ce605b57a | |||
| afe2138feb | |||
| 9586bc16cd | |||
| 9a41e01187 | |||
| 308cf4b551 | |||
| ba64fd5164 | |||
| 7fa242a55f | |||
| d1b5489bfe | |||
| f32df521fc | |||
| 24c90cc049 | |||
| 662a0c8f5c | |||
| 89bf07f5c7 | |||
| ff18467117 | |||
| 015811d243 | |||
| ddc71553e2 | |||
| cbe41aeebf | |||
| ea2acfcaeb | |||
| f7196c48e1 | |||
| 851ddfad16 | |||
| 41339869a6 | |||
| 3a92ec3fb1 | |||
| b473a07b7d | |||
| a98f8e33be | |||
| 959695ed5c | |||
| 64869810cd | |||
| 3b517d31c0 | |||
| 89c3979957 | |||
| f325b8001e | |||
| 1ff2fa5dcf | |||
| 7d966a0ed4 | |||
| a8463288aa | |||
| 83607c0b76 | |||
| 04716b0731 | |||
| ff65b85357 | |||
| 1080188f6e | |||
| 8c5a16d50e | |||
| a715ff25ff | |||
| b393e8a133 | |||
| ff1a81d85d | |||
| a0e2193971 | |||
| fa498a3d20 | |||
| 389dae2079 | |||
| e46492d575 | |||
| e05b511cb2 | |||
| ce0798bb18 | |||
| 8db65ddaeb | |||
| 1b54155cf9 | |||
| e46bb97c5b | |||
| 5cba4dd7d7 | |||
| b36d9fc9a9 | |||
| ee2a060b16 | |||
| 1c0deb2bc8 | |||
| abd1c8df2f | |||
| f49abcc6cc | |||
| ea9da9b2c8 | |||
| 3c40bf5bd1 | |||
| 732017b015 | |||
| eccf11d792 | |||
| ea0d86e8f4 | |||
| 0a0809d7f4 | |||
| 0b0c20fdb5 | |||
| 108f49443d | |||
| ae151b350a | |||
| 3d2da9ed4e | |||
| fed204eb78 | |||
| 34557928d1 | |||
| f4dcbcc366 | |||
| c13595f8a4 | |||
| a8f70fe071 | |||
| bc6da100d3 | |||
| 894f9e39c0 | |||
| 6ec9108ac3 | |||
| 04f48d1de4 | |||
| 61bf2e1afa | |||
| 391ed4a252 | |||
| 10ba853b28 | |||
| b1cd3a8a6e | |||
| 5a60dac2aa | |||
| bdd3b20500 | |||
| d2c2a09dae | |||
| 7d87c0e789 | |||
| 3365817cf1 | |||
| 5e28ec4246 | |||
| 5cbc1c4b8d | |||
| cc9fe1a5c8 | |||
| 2d32be22eb | |||
| e612a59888 | |||
| 1c55c3f7b8 | |||
| 10988770c6 | |||
| cc5a634cab | |||
| 0f65e912e7 | |||
| 6b08b87d16 | |||
| 3521c64ba3 | |||
| fcc11bff74 | |||
| e8a5d6aec0 | |||
| b4bbd17933 | |||
| 973417a694 | |||
| d97434cec4 | |||
| 8301856ee7 | |||
| 7e82d290f9 | |||
| c7ae853e8f | |||
| 4df4458d7b | |||
| ea8871af1b | |||
| ec8fd5f8d6 | |||
| 2bd19e04be | |||
| e8201abb8a | |||
| fcc797342e | |||
| e3fa4a049c | |||
| 7e96bb51d2 | |||
| c4b5cf2f11 | |||
| 8ef92c75c8 | |||
| 8d56d927e1 | |||
| d50f27dc8b | |||
| 8d59d25796 | |||
| e883d2c3bf | |||
| 892a899a20 | |||
| 68914ee31b | |||
| 902f12e333 | |||
| 224324c56f | |||
| b085dcd5dd | |||
| 786f8574d9 | |||
| 6af9c097d8 | |||
| d27187ddeb | |||
| d5c90a0861 | |||
| 3c6d8dcb09 | |||
| 74b480934f | |||
| fba67c3b2a | |||
| c65a81b0ca | |||
| a4ca61d48b | |||
| 41baea32a8 | |||
| 3f3453b3ce | |||
| e5c5670f24 | |||
| cd3a726e26 | |||
| e398f65f0a | |||
| 56b142127b | |||
| 9d807101e4 | |||
| f244f6dda0 | |||
| f06fd11305 | |||
| dbecfae35b | |||
| 2ce508e373 | |||
| f42958d2c2 | |||
| 33a731b56f | |||
| c0a5084bc4 | |||
| 373a705bbb | |||
| af142aba90 | |||
| 8cda53f4ce | |||
| 4e8af81fc6 | |||
| 9920c13bd9 | |||
| 1768518b44 | |||
| da6eb83035 | |||
| 6751fc6f19 | |||
| bf465f9245 | |||
| 2bf73585ba | |||
| f65b24250c | |||
| a9ed99293e | |||
| 590bd6e532 | |||
| 286779a2e4 | |||
| 4c3d87e3f7 | |||
| 56c704be1f | |||
| 5975b75766 | |||
| c259cae429 | |||
| 408ef5114d | |||
| 39855e1bb5 | |||
| f48bd6717d | |||
| 8d118faf36 | |||
| 8b680af938 | |||
| 7c2a09f2bc | |||
| 132eefb8b8 | |||
| fbda162e34 | |||
| f7e0283e61 | |||
| bfa9912c5f | |||
| 4104bb8d53 | |||
| 798010cc83 | |||
| b8390c853c | |||
| a6d81a21a8 | |||
| 975ec57c74 | |||
| 9d3b9ae789 | |||
| 60489c9893 | |||
| 5c4fa8fadf | |||
| b151f52428 | |||
| 244de05421 | |||
| 9078e4e321 | |||
| 65b10b01d9 | |||
| b972dbec4b | |||
| e7fd820db6 | |||
| 8ae937007c | |||
| b891b6354a | |||
| 2f94724df3 | |||
| 8ad33f6281 | |||
| 8ea051acfc | |||
| 2f98872d7a | |||
| e89ccf2a40 | |||
| ecc7a39131 | |||
| 00e200b504 | |||
| 1c4618c8dd | |||
| 1ad3c8e2b2 | |||
| aa503aec81 | |||
| c240e4e6f7 | |||
| 561ddc29bf |
@@ -117,7 +117,7 @@ jobs:
|
||||
-DQT_VERSION_MAJOR=6 \
|
||||
-DBUILD_WITH_KF=ON \
|
||||
-DBUILD_KF=OFF \
|
||||
-DPACKAGE_TESTS=ON \
|
||||
-DPACKAGE_TESTS=OFF \
|
||||
-DCMAKE_POLICY_DEFAULT_CMP0077=NEW \
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
|
||||
-DQET_EXPORT_PROJECT_DB=ON \
|
||||
@@ -509,6 +509,21 @@ jobs:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 2 : Publie les assets nightly (exe + zip) sur la release
|
||||
# Ne tourne que sur push master (pas sur les PRs)
|
||||
#
|
||||
# Release tag: "nightly-qt6" (was "nightly").
|
||||
# The old "nightly" release became IMMUTABLE after it was republished by
|
||||
# this job while GitHub's "Release immutability" repo setting was briefly
|
||||
# enabled (Oct. 2026). Turning the setting off again does not revert a
|
||||
# release that was published while it was on: assets could no longer be
|
||||
# added or deleted there, and its tag can never host a new release, even
|
||||
# after the release itself was deleted. Hence a fresh tag. The old
|
||||
# "nightly" release (with the frozen Qt5 legacy build) was deleted at the
|
||||
# same time; its tag stays locked and must not be reused.
|
||||
#
|
||||
# Do NOT re-enable "Release immutability" while this single, rolling
|
||||
# nightly release model is used: the next republish would lock
|
||||
# "nightly-qt6" the same way. Immutability requires one new release per
|
||||
# build (draft -> upload assets -> publish).
|
||||
# ---------------------------------------------------------------------------
|
||||
publish-nightly-assets:
|
||||
needs: [build-windows-qt6]
|
||||
@@ -516,6 +531,8 @@ jobs:
|
||||
if: github.event_name != 'pull_request'
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
NIGHTLY_TAG: nightly-qt6
|
||||
|
||||
steps:
|
||||
- name: Download installer artifact
|
||||
@@ -532,27 +549,33 @@ jobs:
|
||||
path: downloaded/portable/
|
||||
merge-multiple: true
|
||||
|
||||
# Only Qt6-tagged assets are deleted/replaced here. Assets without "qt6"
|
||||
# in the name are the frozen Qt5 legacy build (last one ever published,
|
||||
# before the Qt5 CI job was removed) — deliberately left untouched so
|
||||
# they stay downloadable indefinitely instead of disappearing.
|
||||
- name: Delete old nightly Qt6 assets (.exe and .zip)
|
||||
# Remove the previous .exe/.zip before uploading the new ones.
|
||||
# On the very first run the release does not exist yet: skip cleanly
|
||||
# (the default bash shell runs with pipefail, so an unguarded
|
||||
# `gh release view` on a missing release would fail the step).
|
||||
- name: Delete old nightly assets (.exe and .zip)
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
gh release view nightly --repo "$REPO" --json assets \
|
||||
--jq '.assets[] | select(.name | test("qt6")) | select(.name | test("\\.(exe|zip)$")) | .name' \
|
||||
if ! gh release view "$NIGHTLY_TAG" --repo "$REPO" > /dev/null 2>&1; then
|
||||
echo "Release '$NIGHTLY_TAG' does not exist yet — it will be created by the next step."
|
||||
exit 0
|
||||
fi
|
||||
gh release view "$NIGHTLY_TAG" --repo "$REPO" --json assets \
|
||||
--jq '.assets[] | select(.name | test("\\.(exe|zip)$")) | .name' \
|
||||
| while read -r name; do
|
||||
echo "Deleting old Qt6 asset: $name"
|
||||
gh release delete-asset nightly "$name" --repo "$REPO" --yes
|
||||
echo "Deleting old asset: $name"
|
||||
gh release delete-asset "$NIGHTLY_TAG" "$name" --repo "$REPO" --yes
|
||||
done
|
||||
echo "Old Qt6 .exe and .zip assets deleted (legacy Qt5 assets left untouched)."
|
||||
echo "Old .exe and .zip assets deleted."
|
||||
|
||||
- name: Update nightly release
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: nightly
|
||||
tag_name: ${{ env.NIGHTLY_TAG }}
|
||||
# Only used when the tag does not exist yet (first run).
|
||||
target_commitish: ${{ github.sha }}
|
||||
name: "Nightly Build – Windows"
|
||||
body: |
|
||||
🔧 **Automated nightly build** — may be unstable.
|
||||
@@ -565,8 +588,6 @@ jobs:
|
||||
|
||||
> ⚠️ This is a development version; it introduces new features you want, but may cause bugs that have not yet been identified yet.
|
||||
> For stable releases, see the [Releases page](https://github.com/${{ github.repository }}/releases).
|
||||
|
||||
> 🗄️ Files without `-qt6-` in the name are the last Qt5 build ever published (frozen, unmaintained). Files tagged `-qt6-` are the actively maintained build.
|
||||
prerelease: true
|
||||
make_latest: false
|
||||
files: |
|
||||
|
||||
@@ -14,6 +14,13 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
# Rolling Qt6 nightly release, updated on every build. Replaces the old
|
||||
# "nightly" tag, which became immutable while the "Release immutability"
|
||||
# setting was briefly enabled (Oct. 2026) — see the note on
|
||||
# publish-nightly-assets in windows-build.yml.
|
||||
env:
|
||||
NIGHTLY_TAG: nightly-qt6
|
||||
|
||||
jobs:
|
||||
build-msi:
|
||||
name: Build MSI with WiX v7 (${{ matrix.flavor }})
|
||||
@@ -371,19 +378,32 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
# The release may not exist yet (e.g. a manual MSI run before
|
||||
# windows-build.yml has published anything on NIGHTLY_TAG):
|
||||
# nothing to delete then.
|
||||
gh release view $env:NIGHTLY_TAG --repo $env:REPO *> $null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Release '$env:NIGHTLY_TAG' does not exist yet — nothing to delete."
|
||||
exit 0
|
||||
}
|
||||
$pattern = "-qt6.*\\.msi$"
|
||||
$names = gh release view nightly --repo $env:REPO --json assets --jq ".assets[] | select(.name | test(`"$pattern`")) | .name"
|
||||
$names = gh release view $env:NIGHTLY_TAG --repo $env:REPO --json assets --jq ".assets[] | select(.name | test(`"$pattern`")) | .name"
|
||||
foreach ($name in ($names -split "`n" | Where-Object { $_ })) {
|
||||
Write-Host "Deleting old asset: $name"
|
||||
gh release delete-asset nightly $name --repo $env:REPO --yes
|
||||
gh release delete-asset $env:NIGHTLY_TAG $name --repo $env:REPO --yes
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
# prerelease / make_latest set explicitly: if this step happens to
|
||||
# create the release (it does not exist yet), it must not become the
|
||||
# repository's "Latest" release.
|
||||
- name: Upload MSI to nightly release
|
||||
if: always()
|
||||
if: always() && hashFiles('dist/*.msi') != ''
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: nightly
|
||||
tag_name: ${{ env.NIGHTLY_TAG }}
|
||||
prerelease: true
|
||||
make_latest: false
|
||||
files: dist/*.msi
|
||||
fail_on_unmatched_files: true
|
||||
env:
|
||||
@@ -413,11 +433,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
# Regenerate the page whenever the MSI job actually ran (success OR
|
||||
# packaging/signing failure) — not only on full success.
|
||||
# generate-page.py queries the published assets on the "nightly"
|
||||
# release itself (line 407: `gh release view nightly --json assets`),
|
||||
# so it already handles a missing MSI natively (empty MSI_NAME -> no
|
||||
# MSI button on the page). The page only needs the exe/zip already
|
||||
# published by windows-build.yml, independent of the MSI's fate.
|
||||
# The step below queries the published assets on the NIGHTLY_TAG
|
||||
# release itself, so it already handles a missing MSI natively (empty
|
||||
# MSI_NAME -> no MSI button on the page). The page only needs the
|
||||
# exe/zip already published by windows-build.yml, independent of the
|
||||
# MSI's fate.
|
||||
# Only "skipped"/"cancelled" are excluded: if build-msi never ran at
|
||||
# all (e.g. Windows Build itself failed), there is nothing new to
|
||||
# publish and regenerating the page would be pointless.
|
||||
@@ -447,33 +467,23 @@ jobs:
|
||||
set -euo pipefail
|
||||
REPO="${{ github.repository }}"
|
||||
|
||||
ASSETS=$(gh release view nightly --repo "$REPO" --json assets --jq '.assets[].name')
|
||||
ASSETS=$(gh release view "$NIGHTLY_TAG" --repo "$REPO" --json assets --jq '.assets[].name')
|
||||
|
||||
EXE_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.exe$' | head -1)
|
||||
ZIP_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.zip$' | head -1)
|
||||
EXE_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.exe$' | head -1 || echo "")
|
||||
ZIP_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.zip$' | head -1 || echo "")
|
||||
MSI_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.msi$' | head -1 || echo "")
|
||||
|
||||
# Legacy Qt5 assets (no "qt6" in the name): last ever published,
|
||||
# frozen — windows-build.yml/windows-msi.yml no longer delete or
|
||||
# replace these, so they keep pointing at the same files release
|
||||
# after release. Rendered as a separate "legacy" section if present.
|
||||
LEGACY_EXE_NAME=$(echo "$ASSETS" | grep -v 'qt6' | grep '\.exe$' | head -1 || echo "")
|
||||
LEGACY_ZIP_NAME=$(echo "$ASSETS" | grep -v 'qt6' | grep '\.zip$' | head -1 || echo "")
|
||||
LEGACY_MSI_NAME=$(echo "$ASSETS" | grep -v 'qt6' | grep '\.msi$' | head -1 || echo "")
|
||||
if [ -z "$EXE_NAME" ] || [ -z "$ZIP_NAME" ]; then
|
||||
echo "::error::No Qt6 .exe/.zip found on release '$NIGHTLY_TAG' — refusing to publish a page with dead links."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BASE="https://github.com/$REPO/releases/download/nightly"
|
||||
BASE="https://github.com/$REPO/releases/download/$NIGHTLY_TAG"
|
||||
INSTALLER_URL="$BASE/$EXE_NAME"
|
||||
PORTABLE_URL="$BASE/$ZIP_NAME"
|
||||
MSI_URL=""
|
||||
[ -n "$MSI_NAME" ] && MSI_URL="$BASE/$MSI_NAME"
|
||||
|
||||
LEGACY_INSTALLER_URL=""
|
||||
LEGACY_PORTABLE_URL=""
|
||||
LEGACY_MSI_URL=""
|
||||
[ -n "$LEGACY_EXE_NAME" ] && LEGACY_INSTALLER_URL="$BASE/$LEGACY_EXE_NAME"
|
||||
[ -n "$LEGACY_ZIP_NAME" ] && LEGACY_PORTABLE_URL="$BASE/$LEGACY_ZIP_NAME"
|
||||
[ -n "$LEGACY_MSI_NAME" ] && LEGACY_MSI_URL="$BASE/$LEGACY_MSI_NAME"
|
||||
|
||||
SHA="${{ github.event.workflow_run.head_sha || github.sha }}"
|
||||
SHORT="${SHA:0:7}"
|
||||
DATE=$(date -u '+%Y-%m-%d %H:%M UTC')
|
||||
@@ -482,7 +492,7 @@ jobs:
|
||||
|
||||
export DATE SHORT REPO SHA RUN_URL RUN_NUMBER
|
||||
export INSTALLER_URL PORTABLE_URL MSI_URL
|
||||
export LEGACY_INSTALLER_URL LEGACY_PORTABLE_URL LEGACY_MSI_URL
|
||||
export NIGHTLY_TAG
|
||||
|
||||
python3 source/build-aux/generate-page.py
|
||||
|
||||
|
||||
+29
-13
@@ -227,9 +227,11 @@ for why this matters).
|
||||
`QtPdf` is available as an optional component in the Qt Online Installer
|
||||
(look for "Qt PDF" under the Qt 6.4+ node); if you skip it, PDF page import
|
||||
is silently disabled exactly as on the other platforms. KDE Frameworks are
|
||||
not prebuilt for Windows by any of the routes below, so Windows builds
|
||||
normally use `-DBUILD_WITH_KF=OFF` (see the
|
||||
[autosave caveat](#9-building-without-kde-frameworks)).
|
||||
prebuilt for Windows only in MSYS2's UCRT64 and CLANG64 environments (the
|
||||
UCRT64 route below, which is also what the official Windows build uses).
|
||||
With MSVC or any other MinGW, use `-DBUILD_WITH_KF=OFF` (see the
|
||||
[autosave caveat](#9-building-without-kde-frameworks)); otherwise the build
|
||||
downloads and compiles KDE Frameworks itself, which is slow.
|
||||
|
||||
### MSVC (Visual Studio 2019 / 2022)
|
||||
|
||||
@@ -259,25 +261,35 @@ Two common ways to get a MinGW toolchain that matches a Qt6 MinGW kit: Qt's
|
||||
own bundled MinGW (installed alongside a `mingw_64` Qt kit via the Qt
|
||||
Online Installer), or [MSYS2](https://www.msys2.org).
|
||||
|
||||
Using MSYS2, from a **MINGW64** shell:
|
||||
Using MSYS2, from a **UCRT64** shell (the same packages and options as
|
||||
`.github/workflows/windows-build.yml`):
|
||||
```sh
|
||||
pacman -S --needed \
|
||||
mingw-w64-x86_64-toolchain \
|
||||
mingw-w64-x86_64-qt6-base mingw-w64-x86_64-qt6-tools \
|
||||
mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja \
|
||||
mingw-w64-x86_64-sqlite3
|
||||
mingw-w64-ucrt-x86_64-toolchain \
|
||||
mingw-w64-ucrt-x86_64-qt6-base mingw-w64-ucrt-x86_64-qt6-svg \
|
||||
mingw-w64-ucrt-x86_64-qt6-tools mingw-w64-ucrt-x86_64-qt6-translations \
|
||||
mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-ninja \
|
||||
mingw-w64-ucrt-x86_64-pkg-config mingw-w64-ucrt-x86_64-sqlite3 \
|
||||
mingw-w64-ucrt-x86_64-kcoreaddons mingw-w64-ucrt-x86_64-kwidgetsaddons \
|
||||
mingw-w64-ucrt-x86_64-extra-cmake-modules
|
||||
|
||||
mkdir build && cd build
|
||||
cmake .. -G Ninja -DBUILD_WITH_KF=OFF -DCMAKE_BUILD_TYPE=Release
|
||||
cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=/ucrt64 \
|
||||
-DBUILD_WITH_KF=ON -DBUILD_KF=OFF \
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
|
||||
-DSQLite3_INCLUDE_DIR=/ucrt64/include \
|
||||
-DSQLite3_LIBRARY=/ucrt64/lib/libsqlite3.dll.a
|
||||
cmake --build .
|
||||
```
|
||||
|
||||
If `libbacktrace` is installed in your MSYS2 environment, pass the following
|
||||
`-DBUILD_KF=OFF` makes the build use the KDE Frameworks packages just
|
||||
installed instead of downloading and compiling its own.
|
||||
|
||||
If `libbacktrace` is installed in your MSYS2 environment, add the following
|
||||
cached CMake variables when configuring. They are only necessary in that
|
||||
case, because `FindBacktrace` needs them to locate the library:
|
||||
|
||||
```sh
|
||||
cmake .. -G Ninja -DBUILD_WITH_KF=OFF -DCMAKE_BUILD_TYPE=Release \
|
||||
-DBacktrace_INCLUDE_DIR=/c/msys64/clang64/include \
|
||||
-DBacktrace_LIBRARY=/c/msys64/clang64/lib/libbacktrace.a
|
||||
```
|
||||
@@ -287,7 +299,10 @@ and `Backtrace_LIBRARY` to the `libbacktrace.a` file. The paths above are the
|
||||
usual locations for the MSYS2 `clang64` environment; adjust them if your
|
||||
installation uses a different prefix.
|
||||
|
||||
(KF6 isn't packaged in MSYS2 either, hence `-DBUILD_WITH_KF=OFF` again.)
|
||||
KF6 is packaged for MSYS2's UCRT64 and CLANG64 environments only. In a
|
||||
**MINGW64** shell (`mingw-w64-x86_64-*` packages) it is not available: install
|
||||
the same packages without the three KDE ones and configure with
|
||||
`-DBUILD_WITH_KF=OFF` instead of `-DBUILD_WITH_KF=ON -DBUILD_KF=OFF`.
|
||||
|
||||
Using the Qt Online Installer's bundled MinGW kit instead: point
|
||||
`CMAKE_PREFIX_PATH` at that kit (e.g. `C:\Qt\6.x.x\mingw_64`) and make sure
|
||||
@@ -332,7 +347,8 @@ platforms, including FreeBSD as of this writing.
|
||||
`BUILD_WITH_KF=OFF` builds QET against a bundled fallback UI layer instead
|
||||
of KDE Frameworks (`kcoreaddons`/`kwidgetsaddons`). This is a fully
|
||||
supported configuration and is, in practice, the default on platforms where
|
||||
KF6 isn't readily packaged (macOS and Windows in particular).
|
||||
KF6 isn't readily packaged (macOS, and Windows outside MSYS2's UCRT64 and
|
||||
CLANG64 environments).
|
||||
|
||||
Almost everything works identically either way. The one known caveat:
|
||||
**autosave behavior is not as polished without KF**, since QET relies on
|
||||
|
||||
+13
-60
@@ -7,30 +7,25 @@ Environment variables required:
|
||||
DATE, SHORT, REPO, SHA, RUN_URL, RUN_NUMBER,
|
||||
INSTALLER_URL, PORTABLE_URL, MSI_URL (optional)
|
||||
|
||||
Optional (frozen Qt5 legacy build — omitted entirely if empty):
|
||||
LEGACY_INSTALLER_URL, LEGACY_PORTABLE_URL, LEGACY_MSI_URL
|
||||
Optional:
|
||||
NIGHTLY_TAG tag of the rolling nightly release (default: "nightly-qt6")
|
||||
|
||||
NOTE: Windows Qt5 CI build removed (Qt6 is now the sole track built and
|
||||
signed going forward). INSTALLER_URL / PORTABLE_URL / MSI_URL point to the
|
||||
Qt6 build artifacts. The LEGACY_* variables, when set, point to the last
|
||||
Qt5 nightly assets that were ever published — kept downloadable but frozen
|
||||
(windows-build.yml no longer regenerates or deletes them).
|
||||
NOTE: Qt6 is the sole Windows track. The frozen Qt5 legacy build and the
|
||||
old "nightly" release that held it were removed in Oct. 2026, after that
|
||||
release became immutable (see windows-build.yml, publish-nightly-assets).
|
||||
"""
|
||||
import os
|
||||
|
||||
date = os.environ.get("DATE", "")
|
||||
short = os.environ.get("SHORT", "")
|
||||
repo = os.environ.get("REPO", "")
|
||||
sha = os.environ.get("SHA", "")
|
||||
run_url = os.environ.get("RUN_URL", "")
|
||||
run_number = os.environ.get("RUN_NUMBER", "")
|
||||
date = os.environ.get("DATE", "")
|
||||
short = os.environ.get("SHORT", "")
|
||||
repo = os.environ.get("REPO", "")
|
||||
sha = os.environ.get("SHA", "")
|
||||
run_url = os.environ.get("RUN_URL", "")
|
||||
run_number = os.environ.get("RUN_NUMBER", "")
|
||||
installer_url = os.environ.get("INSTALLER_URL", "")
|
||||
portable_url = os.environ.get("PORTABLE_URL", "")
|
||||
msi_url = os.environ.get("MSI_URL", "")
|
||||
|
||||
legacy_installer_url = os.environ.get("LEGACY_INSTALLER_URL", "")
|
||||
legacy_portable_url = os.environ.get("LEGACY_PORTABLE_URL", "")
|
||||
legacy_msi_url = os.environ.get("LEGACY_MSI_URL", "")
|
||||
nightly_tag = os.environ.get("NIGHTLY_TAG", "") or "nightly-qt6"
|
||||
|
||||
msi_block = ""
|
||||
if msi_url:
|
||||
@@ -40,47 +35,6 @@ if msi_url:
|
||||
<span class="btn-text">Windows Installer .msi<small>.msi — for enterprise / GPO deployment</small></span>
|
||||
</a>"""
|
||||
|
||||
# Legacy Qt5 section — only rendered if at least one legacy asset exists.
|
||||
legacy_block = ""
|
||||
if legacy_installer_url or legacy_portable_url or legacy_msi_url:
|
||||
legacy_installer_btn = ""
|
||||
if legacy_installer_url:
|
||||
legacy_installer_btn = f"""
|
||||
<a class="btn btn-secondary" href="{legacy_installer_url}">
|
||||
<span class="btn-icon">⬇</span>
|
||||
<span class="btn-text">Windows Installer (Qt5, legacy)<small>.exe — frozen, no longer updated</small></span>
|
||||
</a>"""
|
||||
legacy_msi_btn = ""
|
||||
if legacy_msi_url:
|
||||
legacy_msi_btn = f"""
|
||||
<a class="btn btn-secondary" href="{legacy_msi_url}">
|
||||
<span class="btn-icon">⬇</span>
|
||||
<span class="btn-text">Windows Installer .msi (Qt5, legacy)<small>.msi — frozen, no longer updated</small></span>
|
||||
</a>"""
|
||||
legacy_portable_btn = ""
|
||||
if legacy_portable_url:
|
||||
legacy_portable_btn = f"""
|
||||
<a class="btn btn-secondary" href="{legacy_portable_url}">
|
||||
<span class="btn-icon">📦</span>
|
||||
<span class="btn-text">Windows Portable (Qt5, legacy)<small>.zip — frozen, no longer updated</small></span>
|
||||
</a>"""
|
||||
|
||||
legacy_block = f"""
|
||||
<div class="card">
|
||||
<h2>🗃 Windows — x86_64 — Qt5 (legacy, unmaintained)</h2>
|
||||
<div class="warning">
|
||||
🗃 <strong>Legacy build — frozen, no longer updated.</strong> This is the
|
||||
last Qt5 build published before Windows CI switched to Qt6 only. Kept available for
|
||||
anyone who still needs it, but it will not receive further fixes or security updates.
|
||||
Please migrate to the Qt6 build above when you can.
|
||||
</div>
|
||||
<div class="downloads">
|
||||
{legacy_installer_btn}
|
||||
{legacy_msi_btn}
|
||||
{legacy_portable_btn}
|
||||
</div>
|
||||
</div>"""
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -146,13 +100,12 @@ For production use, download a <a href="https://github.com/{repo}/releases">stab
|
||||
<span class="btn-icon">📦</span>
|
||||
<span class="btn-text">Windows Portable<small>.zip — no installation required, extract and run "Lancer QET.bat"</small></span>
|
||||
</a>
|
||||
<a class="btn btn-secondary" href="https://github.com/{repo}/releases/tag/nightly">
|
||||
<a class="btn btn-secondary" href="https://github.com/{repo}/releases/tag/{nightly_tag}">
|
||||
<span class="btn-icon">📦</span>
|
||||
<span class="btn-text">All nightly files on GitHub<small>Release page with checksums</small></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{legacy_block}
|
||||
</main>
|
||||
<footer>
|
||||
Auto-generated by GitHub Actions ·
|
||||
|
||||
@@ -15,6 +15,10 @@ platforms:
|
||||
amd64:
|
||||
arm64:
|
||||
|
||||
layout:
|
||||
/usr/share/qelectrotech:
|
||||
symlink: $SNAP/usr/share/qelectrotech
|
||||
|
||||
apps:
|
||||
qelectrotech:
|
||||
command: usr/bin/qelectrotech
|
||||
|
||||
@@ -338,6 +338,9 @@ Section ""
|
||||
WriteRegStr HKEY_CLASSES_ROOT "qet_diagram_file\DefaultIcon" "" "$final_project_ico"
|
||||
WriteRegStr HKEY_CLASSES_ROOT "qet_diagram_file\shell\open\command" "" '"$final_qet_exe" "%1"'
|
||||
|
||||
; File associations - .qetz, the zipped project: the same file type
|
||||
WriteRegStr HKEY_CLASSES_ROOT ".qetz" "" "qet_diagram_file"
|
||||
|
||||
; File associations - .elmt
|
||||
WriteRegStr HKEY_CLASSES_ROOT ".elmt" "" "qet_element_file"
|
||||
WriteRegStr HKEY_CLASSES_ROOT "qet_element_file" "" "Element QET"
|
||||
@@ -486,6 +489,7 @@ Section "Uninstall"
|
||||
; Remove file associations registry keys
|
||||
DeleteRegKey HKEY_CLASSES_ROOT "Applications\qelectrotech.exe"
|
||||
DeleteRegKey HKEY_CLASSES_ROOT ".qet"
|
||||
DeleteRegKey HKEY_CLASSES_ROOT ".qetz"
|
||||
DeleteRegKey HKEY_CLASSES_ROOT "qet_diagram_file"
|
||||
DeleteRegKey HKEY_CLASSES_ROOT ".elmt"
|
||||
DeleteRegKey HKEY_CLASSES_ROOT "qet_element_file"
|
||||
|
||||
@@ -69,6 +69,10 @@ set reg_file=qet_install_file_associations.reg
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_diagram_file\shell\open\command]
|
||||
echo @="\"%final_qet_exe%\" \"%%1\""
|
||||
|
||||
rem *.qetz (zipped project): the same file type as *.qet
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\.qetz]
|
||||
echo @="qet_diagram_file"
|
||||
|
||||
rem association de fichier *.elmt
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\.elmt]
|
||||
echo @="qet_element_file"
|
||||
|
||||
@@ -173,6 +173,10 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/container/qetcontainer.h
|
||||
${QET_DIR}/sources/container/qetzip.cpp
|
||||
${QET_DIR}/sources/container/qetzip.h
|
||||
${QET_DIR}/sources/container/qetzreader.cpp
|
||||
${QET_DIR}/sources/container/qetzreader.h
|
||||
${QET_DIR}/sources/container/qetzwriter.cpp
|
||||
${QET_DIR}/sources/container/qetzwriter.h
|
||||
${QET_DIR}/sources/borderproperties.cpp
|
||||
${QET_DIR}/sources/borderproperties.h
|
||||
${QET_DIR}/sources/bordertitleblock.cpp
|
||||
@@ -291,6 +295,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/qetmainwindow.h
|
||||
${QET_DIR}/sources/qetmessagebox.cpp
|
||||
${QET_DIR}/sources/qetmessagebox.h
|
||||
${QET_DIR}/sources/projectlock.cpp
|
||||
${QET_DIR}/sources/projectlock.h
|
||||
${QET_DIR}/sources/qetproject.cpp
|
||||
${QET_DIR}/sources/qetproject.h
|
||||
${QET_DIR}/sources/qetregexpvalidator.cpp
|
||||
@@ -384,6 +390,8 @@ set(QET_SRC_FILES
|
||||
|
||||
${QET_DIR}/sources/autobreakconductor.cpp
|
||||
${QET_DIR}/sources/autobreakconductor.h
|
||||
${QET_DIR}/sources/symboltextplacement.cpp
|
||||
${QET_DIR}/sources/symboltextplacement.h
|
||||
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddelement.cpp
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddelement.h
|
||||
@@ -709,10 +717,13 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/TerminalStrip/physicalterminal.h
|
||||
${QET_DIR}/sources/TerminalStrip/realterminal.cpp
|
||||
${QET_DIR}/sources/TerminalStrip/realterminal.h
|
||||
${QET_DIR}/sources/TerminalStrip/terminalcatalog.h
|
||||
${QET_DIR}/sources/TerminalStrip/terminalstripbridge.cpp
|
||||
${QET_DIR}/sources/TerminalStrip/terminalstripbridge.h
|
||||
${QET_DIR}/sources/TerminalStrip/terminalstrip.cpp
|
||||
${QET_DIR}/sources/TerminalStrip/terminalstrip.h
|
||||
${QET_DIR}/sources/TerminalStrip/terminalstripautoimport.cpp
|
||||
${QET_DIR}/sources/TerminalStrip/terminalstripautoimport.h
|
||||
${QET_DIR}/sources/TerminalStrip/terminalstripdata.cpp
|
||||
${QET_DIR}/sources/TerminalStrip/terminalstripdata.h
|
||||
${QET_DIR}/sources/TerminalStrip/ui/addterminalstripitemdialog.cpp
|
||||
@@ -757,6 +768,12 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/TerminalStrip/GraphicsItem/terminalstripitem.h
|
||||
${QET_DIR}/sources/TerminalStrip/GraphicsItem/trueterminalstrip.cpp
|
||||
${QET_DIR}/sources/TerminalStrip/GraphicsItem/trueterminalstrip.h
|
||||
${QET_DIR}/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.cpp
|
||||
${QET_DIR}/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.h
|
||||
${QET_DIR}/sources/TerminalStrip/GraphicsItem/terminalstripdrawerhelpers.h
|
||||
${QET_DIR}/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.cpp
|
||||
${QET_DIR}/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.h
|
||||
${QET_DIR}/sources/TerminalStrip/GraphicsItem/terminalstripdrawerhelpers.h
|
||||
${QET_DIR}/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp
|
||||
${QET_DIR}/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.h
|
||||
${QET_DIR}/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutpattern.cpp
|
||||
@@ -820,6 +837,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/ui/elementinfopartwidget.h
|
||||
${QET_DIR}/sources/ui/elementinfowidget.cpp
|
||||
${QET_DIR}/sources/ui/elementinfowidget.h
|
||||
${QET_DIR}/sources/ui/multielementinfowidget.cpp
|
||||
${QET_DIR}/sources/ui/multielementinfowidget.h
|
||||
${QET_DIR}/sources/ui/terminalnumberingdialog.cpp
|
||||
${QET_DIR}/sources/ui/terminalnumberingdialog.h
|
||||
${QET_DIR}/sources/ui/cablenumberingdialog.cpp
|
||||
@@ -940,7 +959,10 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/utils/qetsettings.cpp
|
||||
${QET_DIR}/sources/utils/qetsettings.h
|
||||
${QET_DIR}/sources/utils/qetutils.cpp
|
||||
${QET_DIR}/sources/utils/previousversions.cpp
|
||||
${QET_DIR}/sources/utils/previousversions.h
|
||||
${QET_DIR}/sources/utils/qetutils.h
|
||||
${QET_DIR}/sources/utils/syncfolder.h
|
||||
|
||||
${QET_DIR}/sources/materiallist/materialentrydialog.cpp
|
||||
${QET_DIR}/sources/materiallist/materialentrydialog.h
|
||||
@@ -1107,4 +1129,5 @@ set(TS_FILES
|
||||
${QET_DIR}/lang/qet_tr.ts
|
||||
${QET_DIR}/lang/qet_uk.ts
|
||||
${QET_DIR}/lang/qet_zh.ts
|
||||
${QET_DIR}/lang/qet_zh_TW.ts
|
||||
)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<!-- Paint brush handle -->
|
||||
<path d="M19.7 4.3a2.5 2.5 0 0 0-3.5 0l-7.7 7.7a3.5 3.5 0 0 0-.9 1.6l-.6 2.4 2.4-.6a3.5 3.5 0 0 0 1.6-.9l7.7-7.7a2.5 2.5 0 0 0 0-3.5z"
|
||||
fill="#f39c12" stroke="#d68910" stroke-width="0.8" stroke-linejoin="round"/>
|
||||
<!-- Ferrule (metal band) -->
|
||||
<path d="M14.5 9.5 l-2-2 l1.2-1.2 l2 2 z" fill="#bdc3c7"/>
|
||||
<!-- Brush tip / bristles with active paint -->
|
||||
<path d="M7 16c-1.5.5-3.5 1.5-4 4 1.5 0 3.5-.5 4.5-1.5s1-2.5-.5-2.5z"
|
||||
fill="#27ae60" stroke="#1e8449" stroke-width="0.5"/>
|
||||
<!-- Paint stroke / apply check badge in bottom-right -->
|
||||
<circle cx="17.5" cy="17.5" r="5" fill="#27ae60" stroke="#ffffff" stroke-width="1.2"/>
|
||||
<polyline points="15.2,17.5 16.8,19.2 20,15.8" fill="none" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 916 B |
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<!-- Paint brush handle -->
|
||||
<path d="M19.7 4.3a2.5 2.5 0 0 0-3.5 0l-7.7 7.7a3.5 3.5 0 0 0-.9 1.6l-.6 2.4 2.4-.6a3.5 3.5 0 0 0 1.6-.9l7.7-7.7a2.5 2.5 0 0 0 0-3.5z"
|
||||
fill="#f39c12" stroke="#d68910" stroke-width="0.8" stroke-linejoin="round"/>
|
||||
<!-- Ferrule (metal band) -->
|
||||
<path d="M14.5 9.5 l-2-2 l1.2-1.2 l2 2 z" fill="#bdc3c7"/>
|
||||
<!-- Brush tip / bristles -->
|
||||
<path d="M7 16c-1.5.5-3.5 1.5-4 4 1.5 0 3.5-.5 4.5-1.5s1-2.5-.5-2.5z"
|
||||
fill="#2980b9" stroke="#1f618d" stroke-width="0.5"/>
|
||||
<!-- Copy badge: two overlapping small rectangles in bottom-right -->
|
||||
<rect x="13" y="13" width="7" height="8" rx="1.2" fill="#ffffff" stroke="#2c3e50" stroke-width="1.2"/>
|
||||
<rect x="16" y="16" width="7" height="8" rx="1.2" fill="#3498db" stroke="#2c3e50" stroke-width="1.2"/>
|
||||
<line x1="18" y1="19" x2="21" y2="19" stroke="#ffffff" stroke-width="1" stroke-linecap="round"/>
|
||||
<line x1="18" y1="21" x2="21" y2="21" stroke="#ffffff" stroke-width="1" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
+2241
-1475
File diff suppressed because it is too large
Load Diff
+3865
-2911
File diff suppressed because it is too large
Load Diff
+4551
-3596
File diff suppressed because it is too large
Load Diff
+2222
-1469
File diff suppressed because it is too large
Load Diff
+2710
-1790
File diff suppressed because it is too large
Load Diff
+4885
-3924
File diff suppressed because it is too large
Load Diff
+2741
-1802
File diff suppressed because it is too large
Load Diff
+3477
-2539
File diff suppressed because it is too large
Load Diff
+2193
-1477
File diff suppressed because it is too large
Load Diff
+2314
-1396
File diff suppressed because it is too large
Load Diff
+2228
-1501
File diff suppressed because it is too large
Load Diff
+3992
-3093
File diff suppressed because it is too large
Load Diff
+5633
-4635
File diff suppressed because it is too large
Load Diff
+5023
-4055
File diff suppressed because it is too large
Load Diff
+4521
-3513
File diff suppressed because it is too large
Load Diff
+2212
-1473
File diff suppressed because it is too large
Load Diff
+2222
-1469
File diff suppressed because it is too large
Load Diff
+2215
-1462
File diff suppressed because it is too large
Load Diff
+4047
-3128
File diff suppressed because it is too large
Load Diff
@@ -2697,6 +2697,36 @@ Hold Shift and drag to move this text on its own</source>
|
||||
</context>
|
||||
<context>
|
||||
<name>DynamicElementTextModel</name>
|
||||
<message>
|
||||
<location filename="../sources/ui/dynamicelementtextmodel.cpp" line="330"/>
|
||||
<source>Height</source>
|
||||
<translation>Hoogte</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/dynamicelementtextmodel.cpp" line="327"/>
|
||||
<source>Width fixed</source>
|
||||
<translation>Vaste breedte</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/dynamicelementtextmodel.cpp" line="330"/>
|
||||
<source>Height fixed</source>
|
||||
<translation>Vaste hoogte</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/dynamicelementtextmodel.cpp" line="328"/>
|
||||
<source>Ticked, the text field keeps the width above and the text wraps inside it. Not ticked, it is as wide as the text.</source>
|
||||
<translation>Aangevinkt houdt het tekstveld de breedte hierboven en loopt de tekst erin door. Niet aangevinkt is het zo breed als de tekst.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/dynamicelementtextmodel.cpp" line="331"/>
|
||||
<source>Ticked, the text field keeps the height above and the lines that do not fit are cut off. Not ticked, it is as high as the text.</source>
|
||||
<translation>Aangevinkt houdt het tekstveld de hoogte hierboven en worden regels die niet passen afgesneden. Niet aangevinkt is het zo hoog als de tekst.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/dynamicelementtextmodel.cpp" line="649"/>
|
||||
<source>Change the height of an element text</source>
|
||||
<translation>De hoogte van een elementtekst wijzigen</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/dynamicelementtextmodel.cpp" line="70"/>
|
||||
<source>Property</source>
|
||||
@@ -9936,6 +9966,62 @@ Available options:
|
||||
</context>
|
||||
<context>
|
||||
<name>QETDiagramEditor</name>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="497"/>
|
||||
<source>Place symbol texts</source>
|
||||
<translation>Symbolteksten plaatsen</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="498"/>
|
||||
<source>Place the label and the notes of the selected symbols as one block beside them</source>
|
||||
<comment>status bar tip</comment>
|
||||
<translation>Plaatst het label en de opmerkingen van de geselecteerde symbolen als één blok ernaast</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="507"/>
|
||||
<source>Above, left</source>
|
||||
<translation>Boven, links</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="508"/>
|
||||
<source>Above</source>
|
||||
<translation>Boven</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="509"/>
|
||||
<source>Above, right</source>
|
||||
<translation>Boven, rechts</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="510"/>
|
||||
<source>Left</source>
|
||||
<translation>Links</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="511"/>
|
||||
<source>On the symbol</source>
|
||||
<translation>Op het symbool</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="512"/>
|
||||
<source>Right</source>
|
||||
<translation>Rechts</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="513"/>
|
||||
<source>Below, left</source>
|
||||
<translation>Onder, links</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="514"/>
|
||||
<source>Below</source>
|
||||
<translation>Onder</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="515"/>
|
||||
<source>Below, right</source>
|
||||
<translation>Onder, rechts</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="169"/>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="3709"/>
|
||||
@@ -10263,6 +10349,16 @@ Available options:
|
||||
<source>Display or hide the grid of sheet</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="595"/>
|
||||
<source>Folio-wide crosshair</source>
|
||||
<translation>Dradenkruis over het hele blad</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="597"/>
|
||||
<source>Show two lines across the whole folio under the mouse, to see whether parts are in line</source>
|
||||
<translation>Toont twee lijnen over het hele blad onder de muis, om te zien of onderdelen uitgelijnd zijn</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="567"/>
|
||||
<source>Text grid</source>
|
||||
@@ -21739,4 +21835,12 @@ The following variables can be used:
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SymbolTextPlacement</name>
|
||||
<message>
|
||||
<location filename="../sources/symboltextplacement.cpp" line="120"/>
|
||||
<source>Place symbol texts</source>
|
||||
<translation>Symbolteksten plaatsen</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
|
||||
+2193
-1477
File diff suppressed because it is too large
Load Diff
+2790
-1854
File diff suppressed because it is too large
Load Diff
+6733
-5743
File diff suppressed because it is too large
Load Diff
+2300
-1407
File diff suppressed because it is too large
Load Diff
+2197
-1470
File diff suppressed because it is too large
Load Diff
+2200
-1477
File diff suppressed because it is too large
Load Diff
+4181
-3274
File diff suppressed because it is too large
Load Diff
+9833
-8706
File diff suppressed because it is too large
Load Diff
+2208
-1478
File diff suppressed because it is too large
Load Diff
+2200
-1477
File diff suppressed because it is too large
Load Diff
+2222
-1469
File diff suppressed because it is too large
Load Diff
+5141
-4232
File diff suppressed because it is too large
Load Diff
+4663
-3642
File diff suppressed because it is too large
Load Diff
+3952
-3025
File diff suppressed because it is too large
Load Diff
+24778
File diff suppressed because it is too large
Load Diff
@@ -32,11 +32,64 @@
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>qet</string>
|
||||
<string>qetz</string>
|
||||
</array>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>org.qelectrotech.project</string>
|
||||
<string>org.qelectrotech.project-zip</string>
|
||||
</array>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>org.qelectrotech.project</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>QElectroTech project</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.xml</string>
|
||||
</array>
|
||||
<key>UTTypeIconFile</key>
|
||||
<string>qet</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>qet</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<string>application/x-qet-project</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>org.qelectrotech.project-zip</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>QElectroTech zipped project</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.zip-archive</string>
|
||||
</array>
|
||||
<key>UTTypeIconFile</key>
|
||||
<string>qet</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>qetz</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<string>application/x-qet-project+zip</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>qelectrotech</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
|
||||
@@ -6,7 +6,7 @@ Exec=qelectrotech %F
|
||||
Icon=qelectrotech
|
||||
Terminal=false
|
||||
Type=Application
|
||||
MimeType=application/x-qet-project;application/x-qet-element;application/x-qet-titleblock;
|
||||
MimeType=application/x-qet-project;application/x-qet-project+zip;application/x-qet-element;application/x-qet-titleblock;
|
||||
Categories=Graphics;
|
||||
Keywords=Graphics;Science;Electricity;Engineering;
|
||||
Comment=Edit electrical diagrams.
|
||||
|
||||
@@ -37,6 +37,12 @@
|
||||
<summary xml:lang="tr">Elektrik şeması düzenleyici</summary>
|
||||
<summary xml:lang="uk">Редактор електричних схем</summary>
|
||||
<summary xml:lang="zh">电气图编辑器</summary>
|
||||
<provides>
|
||||
<mediatype>application/x-qet-project</mediatype>
|
||||
<mediatype>application/x-qet-project+zip</mediatype>
|
||||
<mediatype>application/x-qet-element</mediatype>
|
||||
<mediatype>application/x-qet-titleblock</mediatype>
|
||||
</provides>
|
||||
<content_rating type="oars-1.1"/>
|
||||
<releases>
|
||||
<release version="0.100.1-dev" date="2026"/>
|
||||
|
||||
@@ -34,6 +34,16 @@
|
||||
<comment xml:lang="zh">QElectroTech 项目文件</comment>
|
||||
<glob pattern="*.qet" />
|
||||
</mime-type>
|
||||
<mime-type type="application/x-qet-project+zip">
|
||||
<alias type="application/x-qelectrotech-project+zip"/>
|
||||
<sub-class-of type="application/zip"/>
|
||||
<comment>QElectroTech zipped project file</comment>
|
||||
<glob pattern="*.qetz" />
|
||||
<magic priority="60">
|
||||
<match type="string" offset="30" value="mimetypeapplication/x-qet-project+zip"/>
|
||||
<match type="string" offset="30" value="mimetypeapplication/x-qelectrotech-project+zip"/>
|
||||
</magic>
|
||||
</mime-type>
|
||||
<mime-type type="application/x-qet-element">
|
||||
<sub-class-of type="text/xml"/>
|
||||
<comment>QElectroTech element file</comment>
|
||||
|
||||
@@ -25,6 +25,13 @@ here read the model.
|
||||
|
||||
## Tools
|
||||
|
||||
Every tool takes a `.qet` or a `.qetz` (the same project, zipped). The
|
||||
tools that read a project's XML read the `.qet` QElectroTech saves from a
|
||||
`.qetz`, made once per version of the file with the configured
|
||||
QElectroTech (`QET_BINARY`); the first read of a big `.qetz` takes as long
|
||||
as QElectroTech needs to save it. The tools that launch QElectroTech hand
|
||||
it the `.qetz` itself, and write one when the output path ends in `.qetz`.
|
||||
|
||||
| Tool | What it answers |
|
||||
|---|---|
|
||||
| `qet_project_info` | title, format version, folios with their uuids, element and conductor counts |
|
||||
|
||||
+149
-22
@@ -56,6 +56,7 @@ workspace policy applies exactly as it does over stdio.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
@@ -97,11 +98,80 @@ def _root(path: str) -> ET.Element:
|
||||
if not p.is_file():
|
||||
raise ValueError(f"no such file: {p}")
|
||||
try:
|
||||
return ET.parse(p).getroot()
|
||||
return ET.parse(_as_qet(p)).getroot()
|
||||
except ET.ParseError as exc:
|
||||
raise ValueError(f"{p.name} is not parseable XML: {exc}") from exc
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# zipped projects (.qetz)
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
# A .qetz is QElectroTech's zipped project: a database and one file per
|
||||
# folio. The tools that read a project's XML read the .qet QElectroTech
|
||||
# saves from it, so they read what QElectroTech reads, whatever the zipped
|
||||
# format holds; one copy per version of the file (path, size, time).
|
||||
# Only the newest few are kept, and the server removes them when it exits.
|
||||
_QETZ_COPIES: dict = {}
|
||||
_QETZ_COPIES_MAX = 8
|
||||
|
||||
|
||||
def _forget_qetz_copy(key) -> None:
|
||||
out = _QETZ_COPIES.pop(key, None)
|
||||
if out is not None:
|
||||
shutil.rmtree(out.parent, ignore_errors=True)
|
||||
|
||||
|
||||
@atexit.register
|
||||
def _forget_qetz_copies() -> None:
|
||||
for key in list(_QETZ_COPIES):
|
||||
_forget_qetz_copy(key)
|
||||
|
||||
|
||||
def _is_qetz(path) -> bool:
|
||||
return str(path).lower().endswith(".qetz")
|
||||
|
||||
|
||||
def _as_qet(path) -> Path:
|
||||
"""The file the XML tools read for `path`: itself, or for a .qetz the
|
||||
.qet QElectroTech saves from it (made with the configured binary)."""
|
||||
p = Path(path).expanduser()
|
||||
if not _is_qetz(p):
|
||||
return p
|
||||
if not p.is_file():
|
||||
raise ValueError(f"no such file: {p}")
|
||||
st = p.stat()
|
||||
key = (str(p.resolve()), st.st_size, st.st_mtime_ns)
|
||||
cached = _QETZ_COPIES.get(key)
|
||||
if cached is not None and cached.is_file():
|
||||
_QETZ_COPIES[key] = _QETZ_COPIES.pop(key) # newest last
|
||||
return cached
|
||||
binary = resolve_binary()
|
||||
if not binary:
|
||||
raise ValueError(f"{p.name} is a zipped project (.qetz): reading it needs "
|
||||
"QElectroTech; set QET_BINARY")
|
||||
folder = Path(tempfile.mkdtemp(prefix="qet-mcp-qetz-"))
|
||||
out = folder / (p.stem + ".qet")
|
||||
# Saving as .qet builds every folio: allow a big project its time
|
||||
result = _run_qet(str(binary), ["--resave", str(p), str(out)], timeout=900)
|
||||
# A run that timed out or failed may have left part of a .qet behind
|
||||
if not result.get("ok") or not out.is_file() or out.stat().st_size == 0:
|
||||
shutil.rmtree(folder, ignore_errors=True)
|
||||
if result.get("timed_out"):
|
||||
detail = f"timed out after {result.get('timeout_s')} s"
|
||||
else:
|
||||
detail = (result.get("stderr") or result.get("stdout") or "").strip()[-400:]
|
||||
raise ValueError(f"QElectroTech could not read {p.name}"
|
||||
+ (f": {detail}" if detail else ""))
|
||||
# An older version of the same file is not read again
|
||||
for old in [k for k in _QETZ_COPIES if k[0] == key[0]]:
|
||||
_forget_qetz_copy(old)
|
||||
_QETZ_COPIES[key] = out
|
||||
while len(_QETZ_COPIES) > _QETZ_COPIES_MAX:
|
||||
_forget_qetz_copy(next(iter(_QETZ_COPIES)))
|
||||
return out
|
||||
|
||||
|
||||
def _element_info(el: ET.Element) -> dict:
|
||||
"""The <elementInformations> bag, as a plain dict."""
|
||||
out = {}
|
||||
@@ -795,23 +865,35 @@ def _num(v) -> float:
|
||||
|
||||
|
||||
def tool_scan(directory: str, tag: str = "conductor",
|
||||
attribute: str = "cable", recursive: bool = True) -> dict:
|
||||
"""Sweep every .qet in a directory, counting how many <tag> carry a
|
||||
attribute: str = "cable", recursive: bool = True,
|
||||
max_qetz: int = 20) -> dict:
|
||||
"""Sweep every .qet and .qetz in a directory, counting how many <tag> carry a
|
||||
non-empty `attribute`.
|
||||
|
||||
This is the corpus question: "3190 conductors, 0 cable values across
|
||||
25 projects" is exactly one call to this tool.
|
||||
|
||||
Each .qetz is read by QElectroTech saving it as a .qet, which can take
|
||||
minutes for a big project, so only the first `max_qetz` are read; the
|
||||
others are listed as skipped.
|
||||
"""
|
||||
d = Path(directory).expanduser()
|
||||
if not d.is_dir():
|
||||
raise ValueError(f"not a directory: {d}")
|
||||
files = sorted(d.rglob("*.qet") if recursive else d.glob("*.qet"))
|
||||
files = sorted([f for pattern in ("*.qet", "*.qetz")
|
||||
for f in (d.rglob(pattern) if recursive else d.glob(pattern))])
|
||||
total = non_empty = 0
|
||||
per_file, values, unreadable = [], {}, []
|
||||
per_file, values, unreadable, skipped = [], {}, [], []
|
||||
qetz_read = 0
|
||||
for f in files:
|
||||
if _is_qetz(f):
|
||||
if qetz_read >= max_qetz:
|
||||
skipped.append(f.name)
|
||||
continue
|
||||
qetz_read += 1
|
||||
try:
|
||||
root = ET.parse(f).getroot()
|
||||
except ET.ParseError as exc:
|
||||
root = ET.parse(_as_qet(f)).getroot()
|
||||
except (ET.ParseError, ValueError) as exc:
|
||||
unreadable.append({"file": f.name, "error": str(exc)})
|
||||
continue
|
||||
n = ne = 0
|
||||
@@ -826,6 +908,9 @@ def tool_scan(directory: str, tag: str = "conductor",
|
||||
per_file.append({"file": f.name, tag: n, "non_empty": ne})
|
||||
return {
|
||||
"directory": str(d), "files": len(files), "unreadable": unreadable,
|
||||
**({"skipped_qetz": skipped,
|
||||
"note": f"only the first {max_qetz} .qetz were read (max_qetz)"}
|
||||
if skipped else {}),
|
||||
"tag": tag, "attribute": attribute,
|
||||
"total": total, "non_empty": non_empty,
|
||||
"distinct_values": sorted(values.items(), key=lambda kv: -kv[1])[:25],
|
||||
@@ -987,6 +1072,23 @@ def _user_collections() -> dict:
|
||||
return found
|
||||
|
||||
|
||||
def _keep_tail(text: str, tail: int) -> str:
|
||||
"""The last `tail` characters of a run's output, but every line an edit
|
||||
script reported through its marker kept whole, wherever it stands.
|
||||
|
||||
A large qet_edit logs far more than `tail` characters; cut from the
|
||||
front, it lost the capability report the script makes first, and the
|
||||
tool then said the binary had never run the script."""
|
||||
if len(text) <= tail:
|
||||
return text
|
||||
marked = [ln for ln in text.splitlines() if "QETEDIT " in ln]
|
||||
rest = text[-tail:]
|
||||
if not marked:
|
||||
return rest
|
||||
kept = set(rest.splitlines())
|
||||
return "\n".join([ln for ln in marked if ln not in kept] + [rest])
|
||||
|
||||
|
||||
def _run_qet(binary: str, args: list[str], timeout: int = 180,
|
||||
elements_dir: str | None = None,
|
||||
script: str | None = None, tail: int = 4000,
|
||||
@@ -1065,7 +1167,7 @@ def _run_qet(binary: str, args: list[str], timeout: int = 180,
|
||||
"hint": "a modal dialog during load will hang a headless "
|
||||
"run; check the project's format version"}
|
||||
result = {"ok": p.returncode == 0, "exit_code": p.returncode,
|
||||
"stdout": p.stdout[-tail:], "stderr": p.stderr[-tail:]}
|
||||
"stdout": _keep_tail(p.stdout, tail), "stderr": _keep_tail(p.stderr, tail)}
|
||||
# QElectroTech refuses --run when scripting is switched off, which
|
||||
# it is by default. Its own message is clear but French, and it
|
||||
# names a settings dialog that nobody driving this server is
|
||||
@@ -1496,6 +1598,13 @@ OPS = {
|
||||
"remove_folio": ("removeFolio", [("folio", "folio")]),
|
||||
"set_folio": ("setFolioProperty", [("folio", "folio"), ("property", "str"),
|
||||
("value", "str")]),
|
||||
# A cable drawn across conductors, as the cable tool draws it: every
|
||||
# conductor the line crosses gets the next core and its colour.
|
||||
"add_cable": ("addCable", [("folio", "folio"), ("designation", "str"),
|
||||
("type", "str"), ("core_colors", "list"),
|
||||
("length", "str"),
|
||||
("x1", "num"), ("y1", "num"),
|
||||
("x2", "num"), ("y2", "num")]),
|
||||
"add_terminal_strip": ("addTerminalStrip", [("installation", "str"), ("location", "str"),
|
||||
("name", "str")]),
|
||||
"remove_terminal_strip": ("removeTerminalStrip", [("strip", "folio")]),
|
||||
@@ -2687,22 +2796,34 @@ def tool_element_search(directory: str, query: str = "", link_type: str | None =
|
||||
# either would bury the real findings.
|
||||
# - "simple" elements are checked for duplicates too but only as a warning:
|
||||
# industrial.qet reuses V1..V6 across folios on purpose.
|
||||
#
|
||||
# A label names one device per place: the same "-F1" in two distribution
|
||||
# boards (+UV1-F1, +UV2-F1) is two devices, as in IEC 81346 / EPLAN. The
|
||||
# place is the element's own plant/location, else its folio's -- what the
|
||||
# label formulas %M/%LM show. element_nomenclature_view carries only the
|
||||
# element's, so these two checks read the tables.
|
||||
_LABELS_PER_PLACE_SQL = (
|
||||
"SELECT ei.label AS label, "
|
||||
"COALESCE(NULLIF(ei.plant,''), di.plant, '') AS plant, "
|
||||
"COALESCE(NULLIF(ei.location,''), di.locmach, '') AS location, COUNT(*) AS n "
|
||||
"FROM element_info ei JOIN element e ON ei.element_uuid = e.uuid "
|
||||
"JOIN diagram_info di ON di.diagram_uuid = e.diagram_uuid "
|
||||
"WHERE COALESCE(ei.label,'') <> '' AND e.type = '{type}' "
|
||||
"AND COALESCE(LOWER(TRIM(ei.exclude_from_bom)), '') NOT IN ('true', '1', 'yes', 'on') "
|
||||
"GROUP BY ei.label, 2, 3 HAVING n > 1 ORDER BY n DESC, ei.label")
|
||||
|
||||
CHECKS = {
|
||||
"duplicate_master_labels": {
|
||||
"severity": "error",
|
||||
"note": "Two master elements with the same label are ambiguous in every "
|
||||
"report that keys on it (BOM, cross-references, wiring list).",
|
||||
"sql": "SELECT label, COUNT(*) AS n FROM element_nomenclature_view "
|
||||
"WHERE COALESCE(label,'') <> '' AND element_type = 'master' "
|
||||
"GROUP BY label HAVING n > 1 ORDER BY n DESC, label",
|
||||
"sql": _LABELS_PER_PLACE_SQL.format(type="master"),
|
||||
},
|
||||
"duplicate_simple_labels": {
|
||||
"severity": "warning",
|
||||
"note": "Legitimate when a label is reused on purpose across folios "
|
||||
"(industrial.qet does); worth a look otherwise.",
|
||||
"sql": "SELECT label, COUNT(*) AS n FROM element_nomenclature_view "
|
||||
"WHERE COALESCE(label,'') <> '' AND element_type = 'simple' "
|
||||
"GROUP BY label HAVING n > 1 ORDER BY n DESC, label",
|
||||
"sql": _LABELS_PER_PLACE_SQL.format(type="simple"),
|
||||
},
|
||||
"unlabelled_masters": {
|
||||
"severity": "warning",
|
||||
@@ -3919,8 +4040,10 @@ def scripts_dir() -> Path:
|
||||
|
||||
SERVER_INSTRUCTIONS = (
|
||||
"This server works with QElectroTech, a free editor for electrical "
|
||||
"diagrams. A project (.qet) holds folios (sheets) of symbols "
|
||||
"(elements) joined by wires (conductors). Folio indexes count from 0.\n"
|
||||
"diagrams. A project (.qet, or .qetz: the same project zipped) holds "
|
||||
"folios (sheets) of symbols (elements) joined by wires (conductors). "
|
||||
"Every tool takes either; a .qetz is read through QElectroTech, so the "
|
||||
"first read of a big one takes a while. Folio indexes count from 0.\n"
|
||||
"Start with qet_about: where QElectroTech keeps things, what is "
|
||||
"switched on, the stored scripts, and house_style -- this "
|
||||
"installation's own drawing conventions if the user set any. Follow "
|
||||
@@ -4854,10 +4977,12 @@ TOOLS = [
|
||||
},
|
||||
{
|
||||
"name": "qet_scan",
|
||||
"description": "Sweep every .qet in a directory and count how many nodes of "
|
||||
"a given tag carry a non-empty attribute, with the distinct "
|
||||
"values found. For corpus questions such as how many "
|
||||
"conductors in the shipped examples have a cable value.",
|
||||
"description": "Sweep every .qet and .qetz in a directory and count how many "
|
||||
"nodes of a given tag carry a non-empty attribute, with the "
|
||||
"distinct values found. For corpus questions such as how many "
|
||||
"conductors in the shipped examples have a cable value. Each "
|
||||
".qetz is read through QElectroTech, which can take minutes "
|
||||
"for a big project: only the first max_qetz are read.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -4865,12 +4990,14 @@ TOOLS = [
|
||||
"tag": {"type": "string", "default": "conductor"},
|
||||
"attribute": {"type": "string", "default": "cable"},
|
||||
"recursive": {"type": "boolean", "default": True},
|
||||
"max_qetz": {"type": "integer", "default": 20},
|
||||
},
|
||||
"required": ["directory"],
|
||||
},
|
||||
"handler": lambda a: tool_scan(a["directory"], a.get("tag", "conductor"),
|
||||
a.get("attribute", "cable"),
|
||||
a.get("recursive", True)),
|
||||
a.get("recursive", True),
|
||||
a.get("max_qetz", 20)),
|
||||
},
|
||||
{
|
||||
"name": "qet_element_info",
|
||||
@@ -5051,7 +5178,7 @@ TOOLS = [
|
||||
"it to one element AFTER it is placed (add_element does not number "
|
||||
"what it places); slaves and reports are refused, since they take "
|
||||
"their label from their master. "
|
||||
"Terminal strips: add_terminal_strip returns an index (name "
|
||||
"Cables: add_cable (folio, designation -- empty for the next free name --, type, core_colors, length, x1, y1, x2, y2) draws a new cable's trunk line across the conductors between the two points, as the cable tool does: each crossed conductor, in order along the line, gets the next core and the colour core_colors lists at that place; a conductor another cable already holds makes the op fail. Returns the cable's uuid. Terminal strips: add_terminal_strip returns an index (name "
|
||||
"it \"$id\"); add_to_strip puts a terminal-type element on "
|
||||
"it, and refuses any other kind. group_terminals/bridge_terminals "
|
||||
"take \"indices\" (at least two) into that strip's real-terminal "
|
||||
|
||||
@@ -437,6 +437,9 @@ class EditValidation(unittest.TestCase):
|
||||
"set_element_text": el + [{"op": "set_element_text", "folio": "$f", "element": "$e", "index": 0,
|
||||
"property": "x", "value": "5"}],
|
||||
"delete_element_text": el + [{"op": "delete_element_text", "folio": "$f", "element": "$e", "index": 0}],
|
||||
"add_cable": [f, {"op": "add_cable", "id": "k", "folio": "$f", "designation": "-W1",
|
||||
"type": "NYM-J 3x1,5", "core_colors": ["BN", "BU", "GNYE"],
|
||||
"length": "2 m", "x1": 0, "y1": 0, "x2": 100, "y2": 0}],
|
||||
"add_terminal_strip": [{"op": "add_terminal_strip", "installation": "a", "location": "b", "name": "c"}],
|
||||
"remove_terminal_strip": [{"op": "remove_terminal_strip", "strip": 0}],
|
||||
"add_to_strip": el + [{"op": "add_to_strip", "strip": 0, "folio": "$f", "element": "$e"}],
|
||||
@@ -4534,12 +4537,28 @@ class Integration(unittest.TestCase):
|
||||
for i in range(4)],
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$e0", "from_terminal": 0, "to": "$e1", "to_terminal": 0},
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$e2", "from_terminal": 0, "to": "$e3", "to_terminal": 0},
|
||||
{"op": "set_conductor", "folio": "$f", "element": "$e2", "terminal": 0, "property": "num", "value": "W2"},
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$e0", "from_terminal": 0, "to": "$e2", "to_terminal": 0},
|
||||
], self.sb.p("conflict.qet"), elements_dir=ELEMENTS, timeout=60)
|
||||
self.assertFalse(r.get("timed_out"), "the call hung on a modal dialog")
|
||||
self.assertFalse(r["ok"])
|
||||
self.assertEqual(r["operations"][-1]["result"], False)
|
||||
|
||||
def test_joining_two_agreeing_potentials_asks_nothing(self):
|
||||
"""Two potentials with the same properties leave nothing to choose:
|
||||
the wire joining them is drawn, headless as well."""
|
||||
base = self.sb.new()
|
||||
r = self.ok(self.sb.edit(base, [
|
||||
{"op": "add_folio", "id": "f"},
|
||||
*[{"op": "add_element", "id": f"e{i}", "folio": "$f", "path": COIL, "x": 100 + i * 200, "y": 100}
|
||||
for i in range(4)],
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$e0", "from_terminal": 0, "to": "$e1", "to_terminal": 0},
|
||||
{"op": "set_conductor", "folio": "$f", "element": "$e0", "terminal": 0, "property": "num", "value": "N"},
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$e2", "from_terminal": 0, "to": "$e3", "to_terminal": 0},
|
||||
{"op": "set_conductor", "folio": "$f", "element": "$e2", "terminal": 0, "property": "num", "value": "N"},
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$e0", "from_terminal": 0, "to": "$e2", "to_terminal": 0}]))
|
||||
self.assertEqual([c["num"] for c in m.tool_conductors(r["output"])["conductors"]], ["N"] * 3)
|
||||
|
||||
# ---- edit: conductor properties, potentials ----
|
||||
|
||||
def test_conductor_property_applies_to_the_whole_potential(self):
|
||||
@@ -4555,6 +4574,53 @@ class Integration(unittest.TestCase):
|
||||
rows = m.tool_conductors(r["output"])["conductors"]
|
||||
self.assertEqual([c["num"] for c in rows], ["W7", "W7"])
|
||||
|
||||
def test_new_conductor_inherits_a_potential_through_terminal_and_report(self):
|
||||
"""A wire drawn to the free side of a terminal element, or to a
|
||||
report linked to a wired one, is on that potential already: it
|
||||
takes the potential's number and line style, not the folio's
|
||||
defaults (ConductorCreator::existingPotential)."""
|
||||
base = self.sb.new()
|
||||
ops = [{"op": "add_folio", "id": "f"}, {"op": "add_folio", "id": "g"},
|
||||
{"op": "add_element", "id": "a", "folio": "$f", "path": COIL, "x": 100, "y": 100},
|
||||
{"op": "add_element", "id": "t", "folio": "$f", "path": TERMINAL, "x": 300, "y": 300},
|
||||
{"op": "add_element", "id": "b", "folio": "$f", "path": COIL, "x": 500, "y": 100},
|
||||
{"op": "add_element", "id": "go", "folio": "$f", "path": NEXT_REPORT, "x": 700, "y": 100},
|
||||
{"op": "add_element", "id": "in", "folio": "$g", "path": PREVIOUS_REPORT, "x": 100, "y": 100},
|
||||
{"op": "add_element", "id": "c", "folio": "$g", "path": COIL, "x": 300, "y": 100},
|
||||
{"op": "link_elements", "folio": "$f", "element": "$go", "to_folio": "$g", "to": "$in"},
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$a", "from_terminal": 0, "to": "$t", "to_terminal": 0},
|
||||
{"op": "set_conductor", "folio": "$f", "element": "$a", "terminal": 0, "property": "num", "value": "W7"},
|
||||
{"op": "set_conductor", "folio": "$f", "element": "$a", "terminal": 0, "property": "style", "value": "dashed"},
|
||||
# the terminal's other side, then on through the report
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$t", "from_terminal": 1, "to": "$b", "to_terminal": 0},
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$b", "from_terminal": 0, "to": "$go", "to_terminal": 0},
|
||||
{"op": "add_conductor", "folio": "$g", "from": "$in", "from_terminal": 0, "to": "$c", "to_terminal": 0}]
|
||||
r = self.ok(self.sb.edit(base, ops))
|
||||
rows = m.tool_conductors(r["output"])["conductors"]
|
||||
self.assertEqual([c["num"] for c in rows], ["W7"] * 4)
|
||||
styles = {c["value"] for c in m.tool_conductors(r["output"], attribute="style")["conductors"]}
|
||||
self.assertEqual(len(styles), 1, styles)
|
||||
self.assertIn("dash", styles.pop())
|
||||
|
||||
def test_nomenclature_takes_the_place_of_the_folio(self):
|
||||
"""An element without its own plant / location is where its folio
|
||||
is: element_nomenclature_view (BOM, nomenclature tables) shows the
|
||||
folio's =plant +location; an element's own value still wins."""
|
||||
base = self.sb.new()
|
||||
ops = [{"op": "add_folio", "id": "f"},
|
||||
{"op": "set_folio", "folio": "$f", "property": "plant", "value": "HAUS"},
|
||||
{"op": "set_folio", "folio": "$f", "property": "locmach", "value": "UV1"},
|
||||
{"op": "add_element", "id": "a", "folio": "$f", "path": COIL, "x": 100, "y": 100},
|
||||
{"op": "set_label", "folio": "$f", "element": "$a", "label": "KA1"},
|
||||
{"op": "add_element", "id": "b", "folio": "$f", "path": COIL, "x": 300, "y": 100},
|
||||
{"op": "set_label", "folio": "$f", "element": "$b", "label": "KA2"},
|
||||
{"op": "set_info", "folio": "$f", "element": "$b", "key": "location", "value": "UV2"}]
|
||||
r = self.ok(self.sb.edit(base, ops))
|
||||
q = m.tool_query(BINARY, r["output"], "SELECT label, plant, location FROM "
|
||||
"element_nomenclature_view ORDER BY label")
|
||||
self.assertEqual(q["rows"], [{"label": "KA1", "plant": "HAUS", "location": "UV1"},
|
||||
{"label": "KA2", "plant": "HAUS", "location": "UV2"}])
|
||||
|
||||
# ---- conductor manual routing ----
|
||||
|
||||
def test_move_conductor_segment_reroutes_the_path(self):
|
||||
@@ -5828,6 +5894,22 @@ class Integration(unittest.TestCase):
|
||||
line = [l for l in (out["stdout"] + out["stderr"]).splitlines() if l.startswith("DB ")][0]
|
||||
self.assertEqual(json.loads(line[3:]), [{"wire_number": "W1"}])
|
||||
|
||||
def test_duplicate_labels_are_counted_per_location(self):
|
||||
"""-F1 in two distribution boards is two devices: the folio's
|
||||
location (+UV1, +UV2) is part of the name, as %LM shows it. KM1 on
|
||||
+UV1 and +UV2 is fine, a second KM1 on +UV2 is the finding."""
|
||||
base = self.sb.new(folios=3)
|
||||
ops = []
|
||||
for folio, ort in enumerate(("UV1", "UV2", "UV2")):
|
||||
ops += [{"op": "set_folio", "folio": folio, "property": "locmach", "value": ort},
|
||||
{"op": "add_element", "id": f"k{folio}", "folio": folio, "path": COIL,
|
||||
"x": 100, "y": 100},
|
||||
{"op": "set_label", "folio": folio, "element": f"$k{folio}", "label": "KM1"}]
|
||||
r = self.ok(self.sb.edit(base, ops))
|
||||
found = {f["check"]: f for f in m.tool_check(BINARY, r["output"])["findings"]}
|
||||
self.assertEqual(found["duplicate_master_labels"]["rows"],
|
||||
[{"label": "KM1", "plant": "", "location": "UV2", "n": 2}])
|
||||
|
||||
def test_check_fires_on_a_project_with_known_defects(self):
|
||||
"""Newly placed elements store NULL where a loaded file stores '', and
|
||||
`col = ''` matches only the second: the first version of these checks
|
||||
@@ -6476,6 +6558,173 @@ class CorpusIntegration(unittest.TestCase):
|
||||
sb.close()
|
||||
|
||||
|
||||
class ZippedProjectWithoutBinary(unittest.TestCase):
|
||||
def test_reading_a_qetz_without_qelectrotech_says_what_to_set(self):
|
||||
"""A .qetz is read through QElectroTech (m._as_qet()): with none
|
||||
configured, the answer names QET_BINARY rather than failing to
|
||||
parse a zip as XML."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
z = Path(tmp) / "p.qetz"
|
||||
z.write_bytes(b"PK\x03\x04 not read")
|
||||
with mock.patch.object(m, "resolve_binary", return_value=None):
|
||||
with self.assertRaises(ValueError) as caught:
|
||||
m.tool_project_info(str(z))
|
||||
self.assertIn("QET_BINARY", str(caught.exception))
|
||||
|
||||
QET = '<project><diagram><conductors><conductor cable="W1"/></conductors></diagram></project>'
|
||||
|
||||
def fake_resave(self, ok=True, timed_out=False):
|
||||
"""A _run_qet that writes a small .qet where --resave was asked to"""
|
||||
def run(binary, args, timeout=180, **kw):
|
||||
Path(args[2]).write_text(self.QET)
|
||||
if timed_out:
|
||||
return {"ok": False, "timed_out": True, "timeout_s": timeout}
|
||||
return {"ok": ok, "exit_code": 0 if ok else 1, "stderr": "", "stdout": ""}
|
||||
return run
|
||||
|
||||
def qetz_files(self, tmp, n):
|
||||
files = []
|
||||
for i in range(n):
|
||||
z = Path(tmp) / f"p{i}.qetz"
|
||||
z.write_bytes(b"PK")
|
||||
files.append(z)
|
||||
return files
|
||||
|
||||
def test_a_failed_or_timed_out_conversion_is_not_kept(self):
|
||||
"""A .qet left behind by a run that did not finish is not read"""
|
||||
for kind in ({"ok": False}, {"timed_out": True}):
|
||||
with self.subTest(**kind), tempfile.TemporaryDirectory() as tmp:
|
||||
m._forget_qetz_copies()
|
||||
z, = self.qetz_files(tmp, 1)
|
||||
with mock.patch.object(m, "resolve_binary", return_value="qet"), \
|
||||
mock.patch.object(m, "_run_qet", side_effect=self.fake_resave(**kind)):
|
||||
with self.assertRaises(ValueError):
|
||||
m._as_qet(z)
|
||||
self.assertEqual(m._QETZ_COPIES, {})
|
||||
|
||||
def test_only_the_newest_conversions_are_kept_on_disk(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
m._forget_qetz_copies()
|
||||
files = self.qetz_files(tmp, m._QETZ_COPIES_MAX + 2)
|
||||
with mock.patch.object(m, "resolve_binary", return_value="qet"), \
|
||||
mock.patch.object(m, "_run_qet", side_effect=self.fake_resave()):
|
||||
copies = [m._as_qet(z) for z in files]
|
||||
self.assertEqual(len(m._QETZ_COPIES), m._QETZ_COPIES_MAX)
|
||||
self.assertFalse(copies[0].parent.exists())
|
||||
self.assertTrue(copies[-1].is_file())
|
||||
# A new version of a file replaces its old copy
|
||||
os.utime(files[-1], ns=(1, 1))
|
||||
newer = m._as_qet(files[-1])
|
||||
self.assertFalse(copies[-1].parent.exists())
|
||||
m._forget_qetz_copies()
|
||||
self.assertFalse(newer.parent.exists())
|
||||
|
||||
def test_scan_reads_at_most_max_qetz(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
m._forget_qetz_copies()
|
||||
self.qetz_files(tmp, 3)
|
||||
with mock.patch.object(m, "resolve_binary", return_value="qet"), \
|
||||
mock.patch.object(m, "_run_qet", side_effect=self.fake_resave()) as run:
|
||||
scan = m.tool_scan(tmp, max_qetz=2)
|
||||
m._forget_qetz_copies()
|
||||
self.assertEqual(run.call_count, 2)
|
||||
self.assertEqual((scan["total"], scan["non_empty"]), (2, 2))
|
||||
self.assertEqual(scan["skipped_qetz"], ["p2.qetz"])
|
||||
|
||||
|
||||
@needs_examples
|
||||
class ZippedProjects(unittest.TestCase):
|
||||
"""Every tool reads and writes QElectroTech's zipped project (.qetz) as
|
||||
it does the .qet: the XML tools read the .qet QElectroTech saves from
|
||||
it, the tools that launch QElectroTech hand it the .qetz."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.tmp = tempfile.TemporaryDirectory()
|
||||
root = Path(cls.tmp.name)
|
||||
cls.env = mock.patch.dict(os.environ, {"QET_BINARY": BINARY, "QET_ENABLE_SCRIPTING": "1",
|
||||
"QET_MCP_SCRIPTS_DIR": str(root / "scripts"),
|
||||
"QET_MCP_INFO_FILE": str(root / "none.json")})
|
||||
cls.env.start()
|
||||
cls.qet, cls.qetz = root / "p.qet", root / "p.qetz"
|
||||
source = str(Path(EXAMPLES) / "industrial.qet")
|
||||
for out in (cls.qet, cls.qetz):
|
||||
r = m._run_qet(BINARY, ["--resave", source, str(out)], timeout=600)
|
||||
assert out.is_file(), r
|
||||
assert cls.qetz.read_bytes()[:2] == b"PK"
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.env.stop()
|
||||
cls.tmp.cleanup()
|
||||
|
||||
def test_read_tools_answer_as_for_the_qet(self):
|
||||
def strip(d):
|
||||
return {k: v for k, v in d.items() if k not in ("path", "file")}
|
||||
for tool in (m.tool_project_info, m.tool_elements, m.tool_conductors, m.tool_items):
|
||||
self.assertEqual(strip(tool(str(self.qetz))), strip(tool(str(self.qet))), tool.__name__)
|
||||
|
||||
def test_diff_finds_nothing_between_the_qet_and_the_qetz(self):
|
||||
d = m.tool_diff(str(self.qet), str(self.qetz))
|
||||
self.assertTrue(d.get("identical") or not any(
|
||||
v.get("added") or v.get("removed") or v.get("changed")
|
||||
for v in d.values() if isinstance(v, dict)), d)
|
||||
|
||||
def test_scan_counts_a_qetz(self):
|
||||
folder = Path(self.tmp.name) / "scan"
|
||||
folder.mkdir(exist_ok=True)
|
||||
for f in (self.qet, self.qetz):
|
||||
shutil.copy2(f, folder / f.name)
|
||||
scan = m.tool_scan(str(folder), "conductor", "cable", recursive=False)
|
||||
self.assertEqual(scan["files"], 2)
|
||||
self.assertEqual(scan["unreadable"], [])
|
||||
self.assertEqual(scan["per_file"][0]["conductor"], scan["per_file"][1]["conductor"])
|
||||
|
||||
def test_a_qetz_is_converted_once_while_it_does_not_change(self):
|
||||
m._QETZ_COPIES.clear()
|
||||
real = m._run_qet
|
||||
with mock.patch.object(m, "_run_qet", side_effect=real) as run:
|
||||
m.tool_project_info(str(self.qetz))
|
||||
m.tool_elements(str(self.qetz))
|
||||
self.assertEqual(run.call_count, 1)
|
||||
|
||||
def test_query_reads_the_qetz_itself(self):
|
||||
sql = "SELECT COUNT(*) AS n FROM element"
|
||||
self.assertEqual(m.tool_query(BINARY, str(self.qetz), sql)["rows"],
|
||||
m.tool_query(BINARY, str(self.qet), sql)["rows"])
|
||||
|
||||
def test_edit_writes_a_qetz(self):
|
||||
first = m.tool_elements(str(self.qetz))["elements"][0]
|
||||
out = Path(self.tmp.name) / "edited.qetz"
|
||||
r = m.tool_edit(BINARY, str(self.qetz),
|
||||
[{"op": "rotate_element", "folio": first["folio"] - 1,
|
||||
"element": first["uuid"], "angle": 90}],
|
||||
str(out), ELEMENTS or None)
|
||||
self.assertTrue(r.get("ok"), r)
|
||||
self.assertEqual(out.read_bytes()[:2], b"PK")
|
||||
# The rotation is what the diff of the two .qetz finds
|
||||
rotated = m.tool_diff(str(self.qetz), str(out))["elements"]["rotated"]
|
||||
self.assertEqual([r["uuid"] for r in rotated], [first["uuid"]])
|
||||
self.assertNotEqual(*rotated[0]["orientation"])
|
||||
|
||||
|
||||
class TestKeepTail(unittest.TestCase):
|
||||
"""A large edit logs more than the kept tail; the script's own report
|
||||
lines must survive the cut, or qet_edit says the script never ran."""
|
||||
|
||||
def test_short_output_is_untouched(self):
|
||||
self.assertEqual(m._keep_tail("a\nb", 100), "a\nb")
|
||||
|
||||
def test_marker_lines_survive_the_cut(self):
|
||||
first = 'QETEDIT {"kind": "capabilities", "missing": []}'
|
||||
text = first + "\n" + "x" * 1000 + "\n" + 'QETEDIT {"kind": "save", "result": true}'
|
||||
kept = m._keep_tail(text, 100)
|
||||
self.assertIn(first, kept)
|
||||
self.assertIn('"kind": "save"', kept)
|
||||
self.assertEqual(kept.count('"kind": "save"'), 1)
|
||||
self.assertEqual(m._parse_script_output(kept).get("missing_methods"), [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"binary : {BINARY or '(none: integration tests skipped)'}")
|
||||
print(f"elements : {ELEMENTS or '(none)'}")
|
||||
|
||||
@@ -69,6 +69,10 @@ set reg_file=qet_install_file_associations.reg
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_diagram_file\shell\open\command]
|
||||
echo @="\"%final_qet_exe%\" \"%%1\""
|
||||
|
||||
rem *.qetz (zipped project): the same file type as *.qet
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\.qetz]
|
||||
echo @="qet_diagram_file"
|
||||
|
||||
rem association de fichier *.elmt
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\.elmt]
|
||||
echo @="qet_element_file"
|
||||
|
||||
@@ -467,5 +467,7 @@
|
||||
<file>licenses/QElectroTech.LICENSE</file>
|
||||
<file>licenses/QET-Elements.NOTICE</file>
|
||||
<file>licenses/QET-Elements.LICENSE</file>
|
||||
<file>ico/format-copy.svg</file>
|
||||
<file>ico/format-apply.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -526,6 +526,13 @@ void ElementsCollectionModel::removeProject(QETProject *project)
|
||||
if (!m_project_list.contains(project))
|
||||
return;
|
||||
|
||||
//A loading may be setting up this project's items on worker
|
||||
//threads: let it end, and keep the items about to be deleted out of
|
||||
//the list applied when it is finished.
|
||||
m_future.waitForFinished();
|
||||
for (ElementCollectionItem *eci : projectItems(project))
|
||||
m_items_list_to_setUp.removeOne(eci);
|
||||
|
||||
int row = m_project_list.indexOf(project);
|
||||
if (removeRows(row, 1, QModelIndex())) {
|
||||
m_project_list.removeOne(project);
|
||||
|
||||
@@ -141,6 +141,11 @@ void ElementsCollectionWidget::addProject(QETProject *project)
|
||||
}
|
||||
|
||||
void ElementsCollectionWidget::removeProject(QETProject *project) {
|
||||
//Before the first load and during a load, the project is also
|
||||
//waiting to be loaded, or in the model being loaded
|
||||
m_waiting_project.removeAll(project);
|
||||
if (m_new_model)
|
||||
m_new_model->removeProject(project);
|
||||
if (m_model)
|
||||
m_model->removeProject(project);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
#include "ui_propertieseditordockwidget.h"
|
||||
#include "propertieseditorwidget.h"
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QTimer>
|
||||
|
||||
/**
|
||||
@brief PropertiesEditorDockWidget::PropertiesEditorDockWidget
|
||||
Constructor
|
||||
@@ -29,6 +32,13 @@ PropertiesEditorDockWidget::PropertiesEditorDockWidget(QWidget *parent) :
|
||||
ui(new Ui::PropertiesEditorDockWidget)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
// Tabbed or not can change without the selection changing
|
||||
connect(this, &QDockWidget::dockLocationChanged,
|
||||
this, &PropertiesEditorDockWidget::updateSizeConstraint);
|
||||
connect(this, &QDockWidget::topLevelChanged,
|
||||
this, &PropertiesEditorDockWidget::updateSizeConstraint);
|
||||
connect(this, &QDockWidget::visibilityChanged,
|
||||
this, &PropertiesEditorDockWidget::updateSizeConstraint);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,6 +58,9 @@ PropertiesEditorDockWidget::~PropertiesEditorDockWidget()
|
||||
*/
|
||||
void PropertiesEditorDockWidget::clear()
|
||||
{
|
||||
if (!m_editor_list.isEmpty())
|
||||
rememberHeight();
|
||||
|
||||
foreach (PropertiesEditorWidget *editor, m_editor_list)
|
||||
{
|
||||
m_editor_list.removeOne(editor);
|
||||
@@ -56,6 +69,7 @@ void PropertiesEditorDockWidget::clear()
|
||||
}
|
||||
|
||||
m_editor_list.clear();
|
||||
updateSizeConstraint();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,8 +108,12 @@ bool PropertiesEditorDockWidget::addEditor(PropertiesEditorWidget *editor,
|
||||
if (!editor) return false;
|
||||
if (m_editor_list.contains(editor)) return true;
|
||||
|
||||
const bool was_empty = m_editor_list.isEmpty();
|
||||
ui -> m_main_vlayout -> insertWidget(index, editor);
|
||||
m_editor_list << editor;
|
||||
if (was_empty)
|
||||
restoreHeight();
|
||||
updateSizeConstraint();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -118,9 +136,125 @@ QList<PropertiesEditorWidget *> PropertiesEditorDockWidget::editors() const
|
||||
*/
|
||||
bool PropertiesEditorDockWidget::removeEditor(PropertiesEditorWidget *editor)
|
||||
{
|
||||
if (m_editor_list.size() == 1 && m_editor_list.first() == editor)
|
||||
rememberHeight();
|
||||
|
||||
bool result = m_editor_list.removeOne(editor);
|
||||
if (result)
|
||||
ui -> m_main_vlayout -> removeWidget(editor);
|
||||
updateSizeConstraint();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief PropertiesEditorDockWidget::rememberHeight
|
||||
The dock shrinks to its title bar while it has no editor, and the
|
||||
other docks of its area take the room. Keep the heights the docks
|
||||
of the area have now, the heights the user gave them, to give them
|
||||
back when an editor comes back.
|
||||
*/
|
||||
void PropertiesEditorDockWidget::rememberHeight()
|
||||
{
|
||||
// Still the shrunk height: the last editor came and went
|
||||
// before the heights were given back
|
||||
if (m_restore_pending || isFloating() || !isVisible())
|
||||
return;
|
||||
// A tabbed dock does not shrink: nothing to give back
|
||||
if (isTabbed()) {
|
||||
m_area_docks.clear();
|
||||
m_area_heights.clear();
|
||||
return;
|
||||
}
|
||||
auto main_window = qobject_cast<QMainWindow *>(parentWidget());
|
||||
if (!main_window)
|
||||
return;
|
||||
|
||||
m_area_docks.clear();
|
||||
m_area_heights.clear();
|
||||
const Qt::DockWidgetArea area = main_window->dockWidgetArea(this);
|
||||
for (QDockWidget *dock : main_window->findChildren<QDockWidget *>())
|
||||
{
|
||||
if (dock->parentWidget() == main_window && !dock->isFloating()
|
||||
&& dock->isVisible()
|
||||
&& main_window->dockWidgetArea(dock) == area)
|
||||
{
|
||||
m_area_docks << dock;
|
||||
m_area_heights << dock->height();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief PropertiesEditorDockWidget::restoreHeight
|
||||
Give the docks of the area back the heights they had before this
|
||||
dock shrank. Without this the area is split again from the docks'
|
||||
size hints, and giving this dock alone its height back takes the
|
||||
room only from its neighbours.
|
||||
*/
|
||||
void PropertiesEditorDockWidget::restoreHeight()
|
||||
{
|
||||
if (m_area_docks.isEmpty() || m_restore_pending)
|
||||
return;
|
||||
|
||||
m_restore_pending = true;
|
||||
// Once the layout has given the dock its maximum height back
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
m_restore_pending = false;
|
||||
auto main_window = qobject_cast<QMainWindow *>(parentWidget());
|
||||
if (!main_window || isFloating() || !isVisible()
|
||||
|| m_editor_list.isEmpty() || isTabbed())
|
||||
return;
|
||||
|
||||
QList<QDockWidget *> docks;
|
||||
QList<int> heights;
|
||||
const Qt::DockWidgetArea area = main_window->dockWidgetArea(this);
|
||||
for (int i = 0 ; i < m_area_docks.size() ; ++i)
|
||||
{
|
||||
QDockWidget *dock = m_area_docks.at(i);
|
||||
// Closed, moved or deleted since
|
||||
if (dock && !dock->isFloating() && dock->isVisible()
|
||||
&& main_window->dockWidgetArea(dock) == area)
|
||||
{
|
||||
docks << dock;
|
||||
heights << m_area_heights.at(i);
|
||||
}
|
||||
}
|
||||
if (docks.contains(this))
|
||||
main_window->resizeDocks(docks, heights, Qt::Vertical);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@brief PropertiesEditorDockWidget::updateSizeConstraint
|
||||
The empty dock shrinks to its title bar, but not while it shares a tab
|
||||
stack with other docks: the stack takes the smallest maximum height of
|
||||
its tabs, so the empty dock would hold every other tab at its title bar
|
||||
and the stack could not be resized.
|
||||
*/
|
||||
void PropertiesEditorDockWidget::updateSizeConstraint()
|
||||
{
|
||||
const bool tabbed = isTabbed();
|
||||
const QLayout::SizeConstraint constraint = tabbed
|
||||
? QLayout::SetDefaultConstraint
|
||||
: QLayout::SetMaximumSize;
|
||||
if (ui->verticalLayout->sizeConstraint() == constraint)
|
||||
return;
|
||||
|
||||
ui->verticalLayout->setSizeConstraint(constraint);
|
||||
// SetMaximumSize left its maximum on the contents
|
||||
if (tabbed)
|
||||
ui->dockWidgetContents->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief PropertiesEditorDockWidget::isTabbed
|
||||
@return true if this dock shares a tab stack with other docks
|
||||
*/
|
||||
bool PropertiesEditorDockWidget::isTabbed() const
|
||||
{
|
||||
auto main_window = qobject_cast<QMainWindow *>(parentWidget());
|
||||
return main_window && !isFloating()
|
||||
&& !main_window->tabifiedDockWidgets(
|
||||
const_cast<PropertiesEditorDockWidget *>(this)).isEmpty();
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#define SELECTIONPROPERTIESDOCKWIDGET_H
|
||||
|
||||
#include <QDockWidget>
|
||||
#include <QPointer>
|
||||
|
||||
class PropertiesEditorWidget;
|
||||
|
||||
@@ -45,7 +46,15 @@ class PropertiesEditorDockWidget : public QDockWidget
|
||||
QList <PropertiesEditorWidget *> m_editor_list;
|
||||
|
||||
private:
|
||||
void rememberHeight();
|
||||
void restoreHeight();
|
||||
void updateSizeConstraint();
|
||||
bool isTabbed() const;
|
||||
|
||||
Ui::PropertiesEditorDockWidget *ui;
|
||||
QList<QPointer<QDockWidget>> m_area_docks;
|
||||
QList<int> m_area_heights;
|
||||
bool m_restore_pending = false;
|
||||
};
|
||||
|
||||
#endif // SELECTIONPROPERTIESDOCKWIDGET_H
|
||||
|
||||
@@ -226,9 +226,12 @@ void SearchAndReplaceWorker::replaceConductor(QList<Conductor *> list)
|
||||
project_->undoStack()->beginMacro(QObject::tr("Search / replace conductors properties."));
|
||||
for (Conductor *c : list)
|
||||
{
|
||||
ConductorProperties cp = applyChange(c->properties(), m_conductor_properties);
|
||||
//Captured before the pushes below: QPropertyUndoCommand applies
|
||||
//on push, so reading it afterwards would give back @a cp.
|
||||
const ConductorProperties old_properties = c->properties();
|
||||
ConductorProperties cp = applyChange(old_properties, m_conductor_properties);
|
||||
|
||||
if (cp != c->properties())
|
||||
if (cp != old_properties)
|
||||
{
|
||||
QSet <Conductor *> conductors_list = c->relatedPotentialConductors(true);
|
||||
conductors_list << c;
|
||||
@@ -239,6 +242,26 @@ void SearchAndReplaceWorker::replaceConductor(QList<Conductor *> list)
|
||||
new_value.setValue(cp);
|
||||
project_->undoStack()->push(new QPropertyUndoCommand(cc, "properties", old_value, new_value));
|
||||
}
|
||||
|
||||
//The other side of a function separated terminal element
|
||||
//only receives the voltage / protocol, and only when this
|
||||
//replacement really changed it: the comparison is this
|
||||
//replacement's own before / after (@a old_properties vs
|
||||
//@a cp), never the far wire's own value, which would make
|
||||
//every replacement overwrite it (@see
|
||||
//Conductor::functionSeparatedConductors).
|
||||
for (Conductor *cc : c->functionSeparatedConductors(true))
|
||||
{
|
||||
ConductorProperties separated_cp = cc->properties();
|
||||
separated_cp.applyTensionProtocolChange(old_properties, cp);
|
||||
if (separated_cp == cc->properties())
|
||||
continue;
|
||||
|
||||
QVariant old_value, new_value;
|
||||
old_value.setValue(cc->properties());
|
||||
new_value.setValue(separated_cp);
|
||||
project_->undoStack()->push(new QPropertyUndoCommand(cc, "properties", old_value, new_value));
|
||||
}
|
||||
}
|
||||
}
|
||||
project_->undoStack()->endMacro();
|
||||
@@ -351,6 +374,25 @@ void SearchAndReplaceWorker::replaceAdvanced(
|
||||
new_value.setValue(new_properties);
|
||||
project_->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value));
|
||||
}
|
||||
|
||||
//The other side of a function separated terminal element
|
||||
//only receives the voltage / protocol, and only when
|
||||
//this replacement really changed it: the comparison is
|
||||
//this replacement's own before / after, never the far
|
||||
//wire's own value (@see
|
||||
//Conductor::functionSeparatedConductors).
|
||||
for (Conductor *c : conductor->functionSeparatedConductors(true))
|
||||
{
|
||||
ConductorProperties separated_properties = c->properties();
|
||||
separated_properties.applyTensionProtocolChange(old_properties, new_properties);
|
||||
if (separated_properties == c->properties())
|
||||
continue;
|
||||
|
||||
QVariant old_value, new_value;
|
||||
old_value.setValue(c->properties());
|
||||
new_value.setValue(separated_properties);
|
||||
project_->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,7 +307,7 @@ void SearchAndReplaceWidget::fillItemsList()
|
||||
|
||||
m_indi_text_qtwi->sortChildren(0, Qt::AscendingOrder);
|
||||
|
||||
for (Conductor *c : dc.m_potential_conductors)
|
||||
for (Conductor *c : dc.potentialConductors())
|
||||
{
|
||||
QTreeWidgetItem *qtwi = new QTreeWidgetItem(m_conductor_qtwi);
|
||||
qtwi->setText(0, c->properties().text);
|
||||
|
||||
@@ -171,6 +171,32 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripLayoutPattern::drawStyleToString
|
||||
* @return the string used to save @a style in the project xml (terminal strip item)
|
||||
*/
|
||||
QString TerminalStripLayoutPattern::drawStyleToString(DrawStyle style)
|
||||
{
|
||||
switch (style) {
|
||||
case DrawStyle::RealisticPushIn : return QStringLiteral("realistic_push_in");
|
||||
case DrawStyle::RealisticScrew : return QStringLiteral("realistic_screw");
|
||||
default : return QStringLiteral("schematic");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripLayoutPattern::drawStyleFromString
|
||||
* @return the style saved as @a string, or @a default_style if @a string is unknown or empty
|
||||
* (item saved before the drawing styles : the schematic style is kept).
|
||||
*/
|
||||
TerminalStripLayoutPattern::DrawStyle TerminalStripLayoutPattern::drawStyleFromString(const QString &string, DrawStyle default_style)
|
||||
{
|
||||
if (string == QLatin1String("schematic")) return DrawStyle::Schematic;
|
||||
if (string == QLatin1String("realistic_push_in") || string == QLatin1String("realistic")) return DrawStyle::RealisticPushIn;
|
||||
if (string == QLatin1String("realistic_screw")) return DrawStyle::RealisticScrew;
|
||||
return default_style;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripLayoutPattern::xmlTagName
|
||||
* @return the tag name of the xml element used by toXml / fromXml
|
||||
@@ -228,6 +254,11 @@ QDomElement TerminalStripLayoutPattern::toXml(QDomDocument &document) const
|
||||
element.setAttribute(QStringLiteral("bridge_point_y_offset_%1").arg(i), m_bridge_point_y_offset.at(i));
|
||||
}
|
||||
|
||||
element.setAttribute(QStringLiteral("realistic_px_per_mm"), m_realistic_px_per_mm);
|
||||
element.setAttribute(QStringLiteral("realistic_length_mm"), m_realistic_length_mm);
|
||||
element.setAttribute(QStringLiteral("realistic_end_stops"), m_realistic_end_stops ? 1 : 0);
|
||||
element.setAttribute(QStringLiteral("realistic_group_marker"), m_realistic_group_marker ? 1 : 0);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
@@ -290,4 +321,19 @@ void TerminalStripLayoutPattern::fromXml(const QDomElement &layout_element)
|
||||
for (auto i = 0; i < m_bridge_point_y_offset.size(); ++i) {
|
||||
m_bridge_point_y_offset[i] = realFromXml(layout_element, QStringLiteral("bridge_point_y_offset_%1").arg(i), m_bridge_point_y_offset.at(i));
|
||||
}
|
||||
|
||||
const auto px_per_mm_ = realFromXml(layout_element, QStringLiteral("realistic_px_per_mm"), m_realistic_px_per_mm);
|
||||
if (px_per_mm_ > 0) {
|
||||
m_realistic_px_per_mm = px_per_mm_;
|
||||
}
|
||||
const auto length_mm_ = realFromXml(layout_element, QStringLiteral("realistic_length_mm"), m_realistic_length_mm);
|
||||
if (length_mm_ > 0) {
|
||||
m_realistic_length_mm = length_mm_;
|
||||
}
|
||||
if (layout_element.hasAttribute(QStringLiteral("realistic_end_stops"))) {
|
||||
m_realistic_end_stops = layout_element.attribute(QStringLiteral("realistic_end_stops")).toInt() != 0;
|
||||
}
|
||||
if (layout_element.hasAttribute(QStringLiteral("realistic_group_marker"))) {
|
||||
m_realistic_group_marker = layout_element.attribute(QStringLiteral("realistic_group_marker")).toInt() != 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,37 @@ class TerminalStripLayoutPattern
|
||||
public:
|
||||
TerminalStripLayoutPattern();
|
||||
|
||||
/**
|
||||
* @brief The DrawStyle enum
|
||||
* How the terminals of a strip are drawn.
|
||||
* The style is chosen for each terminal strip item, when the item is
|
||||
* added to a folio (see TerminalStripItem::setDrawStyle), the layout
|
||||
* only hold the values used by the styles.
|
||||
* Schematic : the classic drawing (header, xrefs, symbols, connections, cables).
|
||||
* RealisticPushIn : the strip seen from above, at scale, like the mounting
|
||||
* drawing of a cabinet : end stops, group marker, push-in wire entries.
|
||||
* RealisticScrew : same as RealisticPushIn, with screw clamps.
|
||||
* The schematic style use most of the values of this class,
|
||||
* the realistic styles use the values prefixed by m_realistic_.
|
||||
*/
|
||||
enum class DrawStyle {
|
||||
Schematic = 0,
|
||||
RealisticPushIn = 1,
|
||||
RealisticScrew = 2
|
||||
};
|
||||
static QString drawStyleToString(DrawStyle style);
|
||||
static DrawStyle drawStyleFromString(const QString &string, DrawStyle default_style);
|
||||
|
||||
//Realistic style
|
||||
//Scale of the drawing : number of pixels for one millimeter
|
||||
qreal m_realistic_px_per_mm{4.0};
|
||||
//Length (mm) of a terminal along the wires, from the top to the bottom
|
||||
qreal m_realistic_length_mm{60.0};
|
||||
//Draw an end stop at each end of the strip
|
||||
bool m_realistic_end_stops{true};
|
||||
//Draw a group marker carrier with the name of the strip, after the first end stop
|
||||
bool m_realistic_group_marker{true};
|
||||
|
||||
//Header of terminal strip
|
||||
QRectF m_header_rect{0,30,50,146};
|
||||
Qt::Orientation m_header_text_orientation{Qt::Horizontal};
|
||||
@@ -96,8 +127,11 @@ class TerminalStripLayoutPattern
|
||||
qreal m_cable_wire_length{40};
|
||||
//Length of the cable
|
||||
qreal m_cable_length{60};
|
||||
//Length of the wires after the cable
|
||||
qreal m_cable_end_length{40};
|
||||
//Length of the wires after the cable. It leaves the room the box
|
||||
//naming where the cable goes needs: three lines (Installation,
|
||||
//Location, Label) at the default font of 15 px, so the box does
|
||||
//not have to shrink its text down.
|
||||
qreal m_cable_end_length{80};
|
||||
qreal m_bridge_point_d{5};
|
||||
QVector<qreal> m_bridge_point_y_offset{50,70,90,110};
|
||||
|
||||
|
||||
@@ -0,0 +1,906 @@
|
||||
/*
|
||||
Copyright 2006-2026 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 "realisticterminalstripdrawer.h"
|
||||
#include "terminalstripdrawerhelpers.h"
|
||||
#include "../terminalcatalog.h"
|
||||
|
||||
#include <QFontMetricsF>
|
||||
#include <QHash>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPair>
|
||||
#include <QScopedPointer>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace TerminalStripDrawer {
|
||||
namespace Realistic {
|
||||
|
||||
namespace {
|
||||
|
||||
//Width (mm) of the accessories. Values of usual products, enough for a drawing.
|
||||
constexpr qreal end_stop_width_mm{8.0};
|
||||
constexpr qreal group_marker_width_mm{11.0};
|
||||
constexpr qreal separator_width_mm{1.5};
|
||||
//Biggest radius of a wire entry (mm), for the wide terminals
|
||||
constexpr qreal max_entry_radius_mm{1.9};
|
||||
|
||||
//Vertical layout of a terminal, in fraction of its length (top = 0, bottom = 1).
|
||||
//The wire entries are in [entries_top, band_top] and [band_bottom, 1 - entries_top],
|
||||
//the middle band (marker tag, bridge channels) is in [band_top, band_bottom].
|
||||
constexpr qreal entries_top{0.04};
|
||||
constexpr qreal band_top{0.40};
|
||||
constexpr qreal band_bottom{0.60};
|
||||
constexpr qreal channels_top{0.405};
|
||||
constexpr qreal channels_bottom{0.445};
|
||||
|
||||
//Colors of the accessories, close to the real products
|
||||
const QColor end_stop_color{232, 228, 220};
|
||||
const QColor end_stop_grip_color{35, 35, 35};
|
||||
const QColor group_marker_color{168, 168, 168};
|
||||
const QColor group_marker_text_color{200, 0, 0};
|
||||
const QColor band_color{85, 85, 85};
|
||||
const QColor channel_color{55, 55, 55};
|
||||
const QColor bridge_color{200, 30, 30};
|
||||
const QColor separator_color{196, 196, 198};
|
||||
const QColor fuse_holder_color{38, 38, 40};
|
||||
const QColor knife_color{240, 120, 0};
|
||||
const QColor led_color{220, 20, 20};
|
||||
|
||||
/**
|
||||
* @brief The PhysicalGeometry struct
|
||||
* What is needed to draw a physical terminal.
|
||||
*/
|
||||
struct PhysicalGeometry
|
||||
{
|
||||
qreal width_mm{0};
|
||||
bool separator_after{false}; //A separator plate is mounted after this terminal
|
||||
QVector<QSharedPointer<AbstractRealTerminalInterface>> reals;
|
||||
//Index of the physical terminal in the strip, and of each real terminal
|
||||
//in the physical terminal : the same indexes than the schematic style,
|
||||
//used to find the element of a clicked terminal.
|
||||
int physical_index{-1};
|
||||
QVector<int> real_indexes;
|
||||
};
|
||||
|
||||
/**
|
||||
* @return the physical terminals of @a strip with their width, in the drawing order.
|
||||
* The width of a physical terminal is the biggest width of its real terminals,
|
||||
* or the reference width if no width is known.
|
||||
*/
|
||||
QVector<PhysicalGeometry> geometry(const AbstractTerminalStripInterface *strip)
|
||||
{
|
||||
QVector<PhysicalGeometry> geometry_;
|
||||
if (!strip) {
|
||||
return geometry_;
|
||||
}
|
||||
|
||||
int physical_index_{-1};
|
||||
for (const auto &physical_t : strip->physicalTerminal())
|
||||
{
|
||||
++physical_index_;
|
||||
if (!physical_t) {
|
||||
continue;
|
||||
}
|
||||
PhysicalGeometry g_;
|
||||
g_.physical_index = physical_index_;
|
||||
int real_index_{-1};
|
||||
for (const auto &real_t : physical_t->realTerminals())
|
||||
{
|
||||
++real_index_;
|
||||
if (!real_t) {
|
||||
continue;
|
||||
}
|
||||
g_.reals.append(real_t);
|
||||
g_.real_indexes.append(real_index_);
|
||||
g_.width_mm = std::max(g_.width_mm, real_t->widthMm());
|
||||
g_.separator_after = g_.separator_after || real_t->hasSeparatorAfter();
|
||||
if (g_.reals.size() == 4) { //Same limit than the schematic style
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (g_.reals.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (g_.width_mm <= 0) {
|
||||
g_.width_mm = Helpers::referenceTerminalWidthMm();
|
||||
}
|
||||
geometry_.append(g_);
|
||||
}
|
||||
return geometry_;
|
||||
}
|
||||
|
||||
qreal endStopWidthMm(const TerminalStripLayoutPattern &pattern) {
|
||||
if (!pattern.m_realistic_end_stops) {
|
||||
return 0;
|
||||
}
|
||||
return end_stop_width_mm;
|
||||
}
|
||||
|
||||
qreal groupMarkerWidthMm(const TerminalStripLayoutPattern &pattern) {
|
||||
return pattern.m_realistic_group_marker ? group_marker_width_mm : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief fitFont
|
||||
* Reduce the pixel size of the font of @a painter so that @a text fit in
|
||||
* @a length (along the text) and @a thickness (across the text).
|
||||
* The font is never enlarged.
|
||||
*/
|
||||
void fitFont(QPainter *painter, const QString &text, qreal length, qreal thickness)
|
||||
{
|
||||
auto font_{painter->font()};
|
||||
qreal size_{font_.pixelSize() > 0 ? qreal(font_.pixelSize()) : font_.pointSizeF()};
|
||||
const QFontMetricsF metrics_{font_};
|
||||
qreal factor_{1};
|
||||
if (metrics_.horizontalAdvance(text) > length && length > 0) {
|
||||
factor_ = std::min(factor_, length / metrics_.horizontalAdvance(text));
|
||||
}
|
||||
if (metrics_.height() > thickness && thickness > 0) {
|
||||
factor_ = std::min(factor_, thickness / metrics_.height());
|
||||
}
|
||||
if (factor_ < 1)
|
||||
{
|
||||
size_ = std::max<qreal>(2.0, size_ * factor_);
|
||||
if (font_.pixelSize() > 0) {
|
||||
font_.setPixelSize(std::max(1, static_cast<int>(size_)));
|
||||
} else {
|
||||
font_.setPointSizeF(size_);
|
||||
}
|
||||
painter->setFont(font_);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief drawVerticalText
|
||||
* Draw @a text from the bottom to the top, centered in @a rect.
|
||||
* The font is reduced if needed.
|
||||
*/
|
||||
void drawVerticalText(QPainter *painter, const QRectF &rect, const QString &text, const QColor &color)
|
||||
{
|
||||
if (text.isEmpty() || rect.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
painter->save();
|
||||
fitFont(painter, text, rect.height() - 2, rect.width() - 1);
|
||||
painter->setPen(color);
|
||||
painter->translate(rect.left(), rect.bottom());
|
||||
painter->rotate(270);
|
||||
painter->drawText(QRectF{0, 0, rect.height(), rect.width()},
|
||||
Qt::AlignCenter, text);
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief drawGrip
|
||||
* Draw a black ribbed grip (end of an end stop).
|
||||
*/
|
||||
void drawGrip(QPainter *painter, const QRectF &rect)
|
||||
{
|
||||
painter->save();
|
||||
painter->setBrush(end_stop_grip_color);
|
||||
painter->drawRect(rect);
|
||||
painter->setPen(QPen(QColor(150, 150, 150), 0));
|
||||
const qreal step_{std::max<qreal>(1.5, rect.height() / 5)};
|
||||
for (qreal y = rect.top() + step_ ; y < rect.bottom() - 0.5 ; y += step_) {
|
||||
painter->drawLine(QPointF{rect.left() + 1, y}, QPointF{rect.right() - 1, y});
|
||||
}
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief drawEndStop
|
||||
* Draw an end stop seen from above : a light body with a black grip at each end,
|
||||
* the fixing screw, the key slot and the locking part.
|
||||
* @param rect : the whole end stop
|
||||
*/
|
||||
void drawEndStop(QPainter *painter, const QRectF &rect)
|
||||
{
|
||||
painter->save();
|
||||
painter->setBrush(end_stop_color);
|
||||
painter->drawRect(rect);
|
||||
|
||||
const qreal w_{rect.width()};
|
||||
const qreal h_{rect.height()};
|
||||
const qreal cx_{rect.center().x()};
|
||||
const qreal grip_h_{h_ * 0.08};
|
||||
drawGrip(painter, QRectF{rect.left(), rect.top(), w_, grip_h_});
|
||||
drawGrip(painter, QRectF{rect.left(), rect.bottom() - grip_h_, w_, grip_h_});
|
||||
|
||||
//Fixing screw
|
||||
const qreal screw_r_{std::min(w_ * 0.3, h_ * 0.05)};
|
||||
const QPointF screw_c_{cx_, rect.top() + h_ * 0.25};
|
||||
painter->setBrush(QColor(60, 60, 60));
|
||||
painter->drawEllipse(screw_c_, screw_r_, screw_r_);
|
||||
painter->setBrush(QColor(20, 20, 20));
|
||||
painter->drawEllipse(screw_c_, screw_r_ * 0.65, screw_r_ * 0.65);
|
||||
|
||||
//Key slot : a narrow slot with a wider middle
|
||||
painter->setBrush(QColor(210, 205, 195));
|
||||
const qreal slot_w_{w_ * 0.22};
|
||||
painter->drawRoundedRect(QRectF{cx_ - slot_w_ / 2, rect.top() + h_ * 0.36, slot_w_, h_ * 0.26}, 1, 1);
|
||||
painter->drawRect(QRectF{cx_ - slot_w_, rect.top() + h_ * 0.45, slot_w_ * 2, h_ * 0.06});
|
||||
|
||||
//Locking part
|
||||
painter->setBrush(QColor(20, 20, 20));
|
||||
const qreal lock_w_{w_ * 0.36};
|
||||
painter->drawRect(QRectF{cx_ - lock_w_ / 2, rect.top() + h_ * 0.68, lock_w_, h_ * 0.07});
|
||||
painter->drawEllipse(QPointF{cx_, rect.top() + h_ * 0.79}, lock_w_ * 0.35, lock_w_ * 0.35);
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief leverZone
|
||||
* Vertical zone (in fraction of the length of the terminal) of the lever of a
|
||||
* fuse terminal or of the knife of a disconnect terminal, centered on the middle
|
||||
* band. For a terminal with one level, the lever goes from a wire entry to the other
|
||||
* (a bit shorter with screw clamps, whose screw heads are nearer the middle),
|
||||
* for a terminal with levels it stay on the middle band, between the wire entries.
|
||||
*/
|
||||
QPair<qreal, qreal> leverZone(int levels, bool screw)
|
||||
{
|
||||
if (levels > 1) {
|
||||
return qMakePair(qreal(0.395), qreal(0.605));
|
||||
}
|
||||
return screw ? qMakePair(qreal(0.345), qreal(0.655))
|
||||
: qMakePair(qreal(0.315), qreal(0.685));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief drawFuseHolder
|
||||
* Draw the hinged fuse holder of a fuse terminal, seen from above : a black lever
|
||||
* on the middle of the terminal with its hinge pin, a window on the fuse,
|
||||
* a ribbed grip at the other end and, for a fuse with led, the led.
|
||||
* The marker tag and the symbol are drawn after, on the lever.
|
||||
* @param x : left of the terminal
|
||||
* @param w : width of the terminal
|
||||
* @param length : length of the terminal
|
||||
*/
|
||||
void drawFuseHolder(QPainter *painter, qreal x, qreal w, qreal length, int levels, bool screw, bool led)
|
||||
{
|
||||
const auto zone_{leverZone(levels, screw)};
|
||||
const qreal top_{length * zone_.first};
|
||||
const qreal bottom_{length * zone_.second};
|
||||
const qreal inset_{w * 0.08};
|
||||
const QRectF lever_{x + inset_, top_, w - 2 * inset_, bottom_ - top_};
|
||||
const qreal cx_{x + w / 2};
|
||||
|
||||
painter->save();
|
||||
painter->setPen(QPen(Qt::black, 0.5));
|
||||
painter->setBrush(fuse_holder_color);
|
||||
painter->drawRoundedRect(lever_, w * 0.12, w * 0.12);
|
||||
|
||||
//Hinge pin
|
||||
const qreal pin_r_{w * 0.12};
|
||||
painter->setPen(QPen(QColor(90, 90, 90), 0.4));
|
||||
painter->setBrush(QColor(150, 150, 150));
|
||||
painter->drawEllipse(QPointF{cx_, top_ + pin_r_ * 2}, pin_r_, pin_r_);
|
||||
|
||||
//Window on the fuse, under the hinge pin and above the marker tag,
|
||||
//only when there is enough room
|
||||
const qreal window_top_{std::max(length * 0.38, top_ + pin_r_ * 3.4)};
|
||||
const qreal window_bottom_{length * 0.445};
|
||||
if (levels == 1 && window_bottom_ - window_top_ > length * 0.03)
|
||||
{
|
||||
painter->setPen(QPen(QColor(110, 110, 110), 0.4));
|
||||
painter->setBrush(QColor(205, 205, 200));
|
||||
painter->drawRoundedRect(QRectF{cx_ - w * 0.25, window_top_, w * 0.5, window_bottom_ - window_top_}, 1, 1);
|
||||
//The fuse in the window
|
||||
painter->setPen(QPen(QColor(120, 90, 40), std::max<qreal>(0.5, w * 0.08)));
|
||||
const qreal margin_{(window_bottom_ - window_top_) * 0.1};
|
||||
painter->drawLine(QPointF{cx_, window_top_ + margin_}, QPointF{cx_, window_bottom_ - margin_});
|
||||
}
|
||||
|
||||
//Ribbed grip at the other end
|
||||
painter->setPen(QPen(QColor(110, 110, 110), 0.4));
|
||||
const qreal grip_top_{bottom_ - (bottom_ - top_) * 0.13};
|
||||
for (int i = 0 ; i < 3 ; ++i) {
|
||||
const qreal y_{grip_top_ + i * (bottom_ - grip_top_) / 3};
|
||||
painter->drawLine(QPointF{lever_.left() + w * 0.12, y_}, QPointF{lever_.right() - w * 0.12, y_});
|
||||
}
|
||||
|
||||
//Led : lit when the fuse is blown. Between the type symbol
|
||||
//(bottom of the middle band) and the grip, whatever the length of the lever.
|
||||
const qreal symbol_bottom_{length * 0.595};
|
||||
if (led && grip_top_ > symbol_bottom_)
|
||||
{
|
||||
const qreal led_r_{std::min({w * 0.13, length * 0.012, (grip_top_ - symbol_bottom_) * 0.45})};
|
||||
painter->setPen(QPen(led_color.darker(160), 0.4));
|
||||
painter->setBrush(led_color);
|
||||
painter->drawEllipse(QPointF{cx_, (symbol_bottom_ + grip_top_) / 2}, led_r_, led_r_);
|
||||
}
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief drawDisconnectKnife
|
||||
* Draw the orange knife of a disconnect terminal, seen from above :
|
||||
* a narrow lever on the middle of the terminal with its hinge and its grip.
|
||||
*/
|
||||
void drawDisconnectKnife(QPainter *painter, qreal x, qreal w, qreal length, int levels, bool screw)
|
||||
{
|
||||
const auto zone_{leverZone(levels, screw)};
|
||||
const qreal top_{length * zone_.first};
|
||||
const qreal bottom_{length * zone_.second};
|
||||
const qreal cx_{x + w / 2};
|
||||
const qreal knife_w_{w * 0.56};
|
||||
|
||||
painter->save();
|
||||
painter->setPen(QPen(knife_color.darker(170), 0.5));
|
||||
painter->setBrush(knife_color);
|
||||
painter->drawRoundedRect(QRectF{cx_ - knife_w_ / 2, top_, knife_w_, bottom_ - top_},
|
||||
knife_w_ * 0.3, knife_w_ * 0.3);
|
||||
|
||||
//Hinge
|
||||
const qreal pin_r_{knife_w_ * 0.22};
|
||||
painter->setBrush(knife_color.darker(150));
|
||||
painter->drawEllipse(QPointF{cx_, top_ + pin_r_ * 2}, pin_r_, pin_r_);
|
||||
|
||||
//Grip
|
||||
painter->setPen(QPen(knife_color.darker(190), 0.5));
|
||||
const qreal grip_top_{bottom_ - (bottom_ - top_) * 0.12};
|
||||
for (int i = 0 ; i < 3 ; ++i) {
|
||||
const qreal y_{grip_top_ + i * (bottom_ - grip_top_) / 3};
|
||||
painter->drawLine(QPointF{cx_ - knife_w_ * 0.3, y_}, QPointF{cx_ + knife_w_ * 0.3, y_});
|
||||
}
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief drawTestSockets
|
||||
* Draw the two test sockets of a test terminal : a brass ring with a black hole.
|
||||
* For a terminal with one level they are between the wire entries and the middle band,
|
||||
* for a terminal with levels (no room between the entries) they are in the middle band,
|
||||
* in place of the bridge channels and of the type symbol.
|
||||
*/
|
||||
void drawTestSockets(QPainter *painter, qreal x, qreal w, qreal length, qreal s, int levels, bool screw, const QColor &fill)
|
||||
{
|
||||
const qreal r_{std::min({w * 0.22, 1.2 * s, length * 0.018})};
|
||||
const qreal offset_{levels > 1 ? 0.425 : (screw ? 0.37 : 0.355)};
|
||||
painter->save();
|
||||
for (const qreal f_ : {offset_, 1 - offset_})
|
||||
{
|
||||
const QPointF c_{x + w / 2, length * f_};
|
||||
painter->setPen(QPen(fill.darker(200), 0.4));
|
||||
painter->setBrush(QColor(200, 170, 60)); //Brass
|
||||
painter->drawEllipse(c_, r_, r_);
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(Qt::black);
|
||||
painter->drawEllipse(c_, r_ * 0.55, r_ * 0.55);
|
||||
}
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief drawSeparator
|
||||
* Draw a separator plate (partition plate) seen from above :
|
||||
* a thin plate as long as the terminals, with rounded ends and a light edge.
|
||||
*/
|
||||
void drawSeparator(QPainter *painter, const QRectF &rect)
|
||||
{
|
||||
painter->save();
|
||||
painter->setPen(QPen(separator_color.darker(170), 0.5));
|
||||
painter->setBrush(separator_color);
|
||||
const qreal r_{std::min(rect.width() / 2, qreal(2.0))};
|
||||
painter->drawRoundedRect(rect, r_, r_);
|
||||
//Light edge, the plate is a bit higher than the terminals
|
||||
painter->setPen(QPen(separator_color.lighter(118), std::max<qreal>(0.5, rect.width() * 0.25)));
|
||||
painter->drawLine(QPointF{rect.left() + rect.width() * 0.35, rect.top() + r_},
|
||||
QPointF{rect.left() + rect.width() * 0.35, rect.bottom() - r_});
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief drawGroupMarker
|
||||
* Draw the group marker carrier with the name of the strip on a white label.
|
||||
*/
|
||||
void drawGroupMarker(QPainter *painter, const QRectF &rect, const QString &text)
|
||||
{
|
||||
painter->save();
|
||||
painter->setBrush(group_marker_color);
|
||||
painter->drawRect(rect);
|
||||
|
||||
if (!text.isEmpty())
|
||||
{
|
||||
//White label : as long as the text, centered on the carrier
|
||||
const qreal label_w_{rect.width() * 0.6};
|
||||
fitFont(painter, text, rect.height() * 0.8, label_w_ - 2);
|
||||
const qreal text_len_{QFontMetricsF{painter->font()}.horizontalAdvance(text)};
|
||||
const qreal label_h_{std::min(rect.height() * 0.85, text_len_ + label_w_ * 0.6)};
|
||||
const QRectF label_{rect.center().x() - label_w_ / 2,
|
||||
rect.center().y() - label_h_ / 2,
|
||||
label_w_, label_h_};
|
||||
painter->setBrush(Qt::white);
|
||||
painter->drawRect(label_);
|
||||
drawVerticalText(painter, label_, text, group_marker_text_color);
|
||||
}
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief drawWireEntry
|
||||
* Draw one wire entry of a terminal.
|
||||
* Push-in : a black hole.
|
||||
* Screw : the head of the clamping screw, with the hole of the wire next to it.
|
||||
* @param center : center of the entry
|
||||
* @param width : width of the terminal
|
||||
* @param row_h : height available for the entry
|
||||
* @param max_r : biggest radius of the entry (a wide terminal don't have a huge hole)
|
||||
* @param outer : -1 if the end of the terminal is above the entry (top half), +1 if it is under (bottom half)
|
||||
* @param screw : true for a screw clamp
|
||||
*/
|
||||
void drawWireEntry(QPainter *painter, const QPointF ¢er, qreal width, qreal row_h,
|
||||
qreal max_r, qreal outer, const QColor &fill, bool screw)
|
||||
{
|
||||
painter->save();
|
||||
if (!screw)
|
||||
{
|
||||
const qreal r_{std::min({width * 0.32, row_h * 0.3, max_r})};
|
||||
painter->setBrush(Qt::black);
|
||||
painter->drawEllipse(center, r_, r_);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Hole of the wire, toward the end of the terminal
|
||||
const qreal hole_{std::min({width * 0.45, row_h * 0.22, max_r * 1.6})};
|
||||
const qreal hole_y_{outer < 0 ? center.y() - row_h * 0.38 : center.y() + row_h * 0.38 - hole_};
|
||||
painter->setBrush(Qt::black);
|
||||
painter->drawRect(QRectF{center.x() - hole_ / 2, hole_y_, hole_, hole_});
|
||||
//Screw head, toward the middle of the terminal
|
||||
const qreal r_{std::min({width * 0.36, row_h * 0.26, max_r * 1.1})};
|
||||
const QPointF c_{center.x(), center.y() - outer * row_h * 0.08};
|
||||
painter->setBrush(fill.darker(125));
|
||||
painter->drawEllipse(c_, r_ * 1.15, r_ * 1.15);
|
||||
painter->setBrush(QColor(175, 175, 175));
|
||||
painter->drawEllipse(c_, r_, r_);
|
||||
painter->setPen(QPen(Qt::black, std::max<qreal>(0.6, r_ * 0.25)));
|
||||
painter->drawLine(QPointF{c_.x() - r_ * 0.7, c_.y()}, QPointF{c_.x() + r_ * 0.7, c_.y()});
|
||||
}
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief drawEntriesHalf
|
||||
* Draw the rows of wire entries of one half of a terminal (top or bottom).
|
||||
* One row by level. Each row have a few ribs at its outer side and a line
|
||||
* at its inner side, like the real terminals.
|
||||
* @param outer : y of the outer side of the half (end of the terminal)
|
||||
* @param inner : y of the inner side of the half (middle band). For the bottom half outer > inner.
|
||||
* @param max_r : biggest radius of a wire entry
|
||||
*/
|
||||
void drawEntriesHalf(QPainter *painter, const QRectF &body, qreal outer, qreal inner,
|
||||
int levels, qreal max_r, const QColor &fill, bool screw)
|
||||
{
|
||||
const qreal row_h_{(inner - outer) / levels}; //Negative for the bottom half
|
||||
const qreal abs_row_h_{std::abs(row_h_)};
|
||||
const QPen rib_pen_{fill.darker(170), 0};
|
||||
|
||||
for (int k = 0 ; k < levels ; ++k)
|
||||
{
|
||||
const qreal a_{outer + k * row_h_};
|
||||
//Ribs
|
||||
painter->save();
|
||||
painter->setPen(rib_pen_);
|
||||
for (const qreal f_ : {0.12, 0.22, 0.32}) {
|
||||
const qreal y_{a_ + f_ * row_h_};
|
||||
painter->drawLine(QPointF{body.left(), y_}, QPointF{body.right(), y_});
|
||||
}
|
||||
//Limit of the row
|
||||
if (k > 0) {
|
||||
painter->drawLine(QPointF{body.left(), a_}, QPointF{body.right(), a_});
|
||||
}
|
||||
painter->restore();
|
||||
|
||||
drawWireEntry(painter, QPointF{body.center().x(), a_ + 0.67 * row_h_},
|
||||
body.width(), abs_row_h_, max_r, row_h_ > 0 ? -1 : 1, fill, screw);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The BridgePoint struct
|
||||
* Where a bridge is plugged.
|
||||
*/
|
||||
struct BridgePoint
|
||||
{
|
||||
QPointF point;
|
||||
qreal width{0};
|
||||
};
|
||||
|
||||
//True when at least one terminal of the strip carries a cable : the strip
|
||||
//then needs the zone drawn under it to let the cables out, like the
|
||||
//schematic style does.
|
||||
bool haveCable(const AbstractTerminalStripInterface *strip)
|
||||
{
|
||||
if (!strip) {
|
||||
return false;
|
||||
}
|
||||
for (const auto &physical_t : strip->physicalTerminal())
|
||||
{
|
||||
for (const auto &real_t : physical_t->realTerminals())
|
||||
{
|
||||
if (real_t && !real_t->cable().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief cableCells
|
||||
* The cells the cables are drawn from : one per terminal of the strip
|
||||
* carrying a cable, spread over the width of its body, all of them
|
||||
* leaving the strip at its bottom edge. The paint and the depth the
|
||||
* strip reserves under it both work on this list, so a box set lower
|
||||
* than the others is never cut off by the drawing area.
|
||||
*/
|
||||
QVector<Helpers::CableCell> cableCells(const AbstractTerminalStripInterface &strip,
|
||||
const TerminalStripLayoutPattern &pattern)
|
||||
{
|
||||
const qreal s_{pattern.m_realistic_px_per_mm};
|
||||
const qreal length_{pattern.m_realistic_length_mm * s_};
|
||||
|
||||
//Left end stop and strip name : the terminals start after them
|
||||
qreal x_{endStopWidthMm(pattern) * s_};
|
||||
if (pattern.m_realistic_group_marker) {
|
||||
x_ += group_marker_width_mm * s_;
|
||||
}
|
||||
|
||||
QVector<Helpers::CableCell> cells;
|
||||
for (const auto &g_ : geometry(&strip))
|
||||
{
|
||||
const qreal w_{g_.width_mm * s_};
|
||||
const int levels_{static_cast<int>(g_.reals.size())};
|
||||
|
||||
//One wire exit by level, spread over the width of the body
|
||||
for (int k = 0 ; k < levels_ ; ++k)
|
||||
{
|
||||
const auto &real_t{g_.reals.at(k)};
|
||||
if (!real_t || real_t->cable().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
Helpers::CableCell cell_;
|
||||
cell_.x = x_ + w_ * (k + 0.5) / levels_;
|
||||
cell_.bottom = length_;
|
||||
cell_.cable = real_t->cable();
|
||||
cell_.wire = real_t->cableWire();
|
||||
cell_.shield = real_t->isShield();
|
||||
cell_.terminal = real_t;
|
||||
cells.append(cell_);
|
||||
}
|
||||
|
||||
x_ += w_;
|
||||
if (g_.separator_after) {
|
||||
x_ += separator_width_mm * s_;
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
} //End anonymous namespace
|
||||
|
||||
QRectF boundingRect(const AbstractTerminalStripInterface *strip,
|
||||
const TerminalStripLayoutPattern &pattern)
|
||||
{
|
||||
qreal width_mm_{2 * endStopWidthMm(pattern) + groupMarkerWidthMm(pattern)};
|
||||
for (const auto &g_ : geometry(strip)) {
|
||||
width_mm_ += g_.width_mm;
|
||||
if (g_.separator_after) {
|
||||
width_mm_ += separator_width_mm;
|
||||
}
|
||||
}
|
||||
const qreal s_{pattern.m_realistic_px_per_mm};
|
||||
const qreal length_{pattern.m_realistic_length_mm * s_};
|
||||
|
||||
//The cables are drawn under the strip, in the zone the schematic
|
||||
//style reserves for them too
|
||||
qreal extra_bottom_{0};
|
||||
if (pattern.m_cable_wire_length > 0 && haveCable(strip))
|
||||
{
|
||||
//4 is the diameter of the circle at the end of the wires.
|
||||
//A box standing in the way of another one is drawn lower than
|
||||
//the end zone, and the room for it is reserved too (same rule,
|
||||
//same cells as the drawing) or it is cut off
|
||||
extra_bottom_ = pattern.m_cable_wire_length
|
||||
+ std::max<qreal>(0, pattern.m_cable_length)
|
||||
+ std::max<qreal>(0, pattern.m_cable_end_length)
|
||||
+ 4
|
||||
+ Helpers::cableBoxesExtraDepth(cableCells(*strip, pattern),
|
||||
pattern);
|
||||
}
|
||||
return QRectF{0, 0, width_mm_ * s_, length_ + extra_bottom_};
|
||||
}
|
||||
|
||||
void paint(QPainter *painter,
|
||||
const AbstractTerminalStripInterface &strip,
|
||||
const TerminalStripLayoutPattern &pattern,
|
||||
TerminalStripLayoutPattern::DrawStyle style,
|
||||
bool preview_draw,
|
||||
const QPointF &hover_pos,
|
||||
QVector<XrefRect> *xref_rects)
|
||||
{
|
||||
//Clamp of the layout, used for the terminals not chosen in the catalog
|
||||
const bool layout_screw_{style == TerminalStripLayoutPattern::DrawStyle::RealisticScrew};
|
||||
const qreal s_{pattern.m_realistic_px_per_mm};
|
||||
const qreal length_{pattern.m_realistic_length_mm * s_};
|
||||
if (s_ <= 0 || length_ <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
painter->save();
|
||||
painter->setRenderHint(QPainter::Antialiasing, true);
|
||||
painter->setFont(pattern.font());
|
||||
painter->setPen(QPen(Qt::black, 0.5));
|
||||
|
||||
if (preview_draw)
|
||||
{
|
||||
painter->save();
|
||||
painter->setPen(Qt::blue);
|
||||
painter->setBrush(Qt::NoBrush);
|
||||
painter->drawRect(boundingRect(&strip, pattern));
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
qreal x_{0};
|
||||
|
||||
//The accessories are a bit shorter than the terminals, centered on them
|
||||
const qreal end_stop_w_{endStopWidthMm(pattern) * s_};
|
||||
const qreal end_stop_h_{length_ * 0.88};
|
||||
const qreal end_stop_y_{(length_ - end_stop_h_) / 2};
|
||||
auto draw_end_stop = [&]()
|
||||
{
|
||||
if (end_stop_w_ <= 0) {
|
||||
return;
|
||||
}
|
||||
const QRectF rect_{x_, end_stop_y_, end_stop_w_, end_stop_h_};
|
||||
drawEndStop(painter, rect_);
|
||||
x_ += end_stop_w_;
|
||||
};
|
||||
|
||||
draw_end_stop();
|
||||
|
||||
if (pattern.m_realistic_group_marker)
|
||||
{
|
||||
const qreal w_{group_marker_width_mm * s_};
|
||||
const qreal h_{length_ * 0.72};
|
||||
drawGroupMarker(painter, QRectF{x_, (length_ - h_) / 2, w_, h_}, strip.name());
|
||||
x_ += w_;
|
||||
}
|
||||
|
||||
QHash<QUuid, QVector<BridgePoint>> bridges_;
|
||||
//The cells the cables are drawn from, worked out once for the whole
|
||||
//strip: the depth the strip reserves under it is built on the same
|
||||
//list (boundingRect()), so a box set lower than another one is
|
||||
//never cut off
|
||||
QVector<Helpers::CableCell> cable_cells_{cableCells(strip, pattern)};
|
||||
|
||||
for (const auto &g_ : geometry(&strip))
|
||||
{
|
||||
const qreal w_{g_.width_mm * s_};
|
||||
const QRectF body_{x_, 0, w_, length_};
|
||||
const auto first_{g_.reals.first()};
|
||||
const int levels_{static_cast<int>(g_.reals.size())};
|
||||
|
||||
//A terminal chosen in the catalog is drawn with its own clamp, so that a strip
|
||||
//mixing screw and spring terminals is drawn as it is. The spring-cage clamps
|
||||
//(opened with a screwdriver) are drawn like the push-in ones.
|
||||
const auto *catalog_entry_{TerminalCatalog::entryByLabel(first_->catalogEntry())};
|
||||
const bool screw_{catalog_entry_ ? catalog_entry_->clamp == TerminalCatalog::Clamp::Screw
|
||||
: layout_screw_};
|
||||
|
||||
QColor fill_, stripe_;
|
||||
Helpers::terminalColors(first_, fill_, stripe_);
|
||||
if (!fill_.isValid()) {
|
||||
fill_ = QColor(150, 150, 150);
|
||||
}
|
||||
|
||||
//Body
|
||||
painter->save();
|
||||
painter->setPen(QPen(fill_.darker(220), 0.5));
|
||||
painter->setBrush(fill_);
|
||||
painter->drawRect(body_);
|
||||
if (stripe_.isValid())
|
||||
{
|
||||
//Two-tone terminal (PE) : the stripe color on the left part, like the real ones
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(stripe_);
|
||||
painter->drawRect(QRectF{body_.left(), body_.top(), w_ * 0.6, length_});
|
||||
}
|
||||
painter->restore();
|
||||
|
||||
//Wire entries : one row by level, at the top and at the bottom
|
||||
const qreal max_r_{max_entry_radius_mm * s_};
|
||||
drawEntriesHalf(painter, body_, length_ * entries_top, length_ * band_top, levels_, max_r_, fill_, screw_);
|
||||
drawEntriesHalf(painter, body_, length_ * (1 - entries_top), length_ * band_bottom, levels_, max_r_, fill_, screw_);
|
||||
|
||||
//Middle band, with one bridge channel by level
|
||||
painter->save();
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(band_color);
|
||||
painter->drawRect(QRectF{x_, length_ * band_top, w_, length_ * (band_bottom - band_top)});
|
||||
painter->setBrush(channel_color);
|
||||
const qreal channel_h_{length_ * (channels_bottom - channels_top) / levels_};
|
||||
for (int k = 0 ; k < levels_ ; ++k) {
|
||||
painter->drawRect(QRectF{x_ + w_ * 0.15, length_ * channels_top + (k + 0.2) * channel_h_,
|
||||
w_ * 0.7, channel_h_ * 0.6});
|
||||
}
|
||||
painter->restore();
|
||||
|
||||
//Specific parts of some types of terminal
|
||||
const auto type_{first_->type()};
|
||||
switch (type_)
|
||||
{
|
||||
case ElementData::TTFuse:
|
||||
drawFuseHolder(painter, x_, w_, length_, levels_, screw_, first_->isLed());
|
||||
break;
|
||||
case ElementData::TTSectional:
|
||||
drawDisconnectKnife(painter, x_, w_, length_, levels_, screw_);
|
||||
break;
|
||||
case ElementData::TTTestSocket:
|
||||
drawTestSockets(painter, x_, w_, length_, s_, levels_, screw_, fill_);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
//Symbol of the type (fuse, led...), at the bottom of the band
|
||||
//A test terminal with levels have its test sockets in place of the symbol
|
||||
const bool have_symbol_{(first_->type() != ElementData::TTGeneric || first_->isLed())
|
||||
&& !(type_ == ElementData::TTTestSocket && levels_ > 1)};
|
||||
const qreal tag_top_{length_ * 0.455};
|
||||
const bool sockets_in_band_{type_ == ElementData::TTTestSocket && levels_ > 1};
|
||||
const qreal tag_bottom_{length_ * (have_symbol_ || sockets_in_band_ ? 0.535 : 0.59)};
|
||||
if (have_symbol_)
|
||||
{
|
||||
painter->save();
|
||||
//Black on the orange knife, white on the dark band and on the fuse holder
|
||||
painter->setPen(QPen(type_ == ElementData::TTSectional ? QColor(Qt::black) : QColor(Qt::white), 0.5));
|
||||
Helpers::drawTypeSymbol(painter, QRectF{x_, length_ * 0.54, w_, length_ * 0.055},
|
||||
first_->type(), first_->isLed());
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
//Marker tag with the number(s) of the terminal
|
||||
QStringList numbers_;
|
||||
for (const auto &real_t : g_.reals) {
|
||||
numbers_ << Helpers::terminalNumber(real_t->label());
|
||||
}
|
||||
const bool sectional_{first_->type() == ElementData::TTSectional};
|
||||
const QRectF tag_{x_ + std::min(0.6 * s_, w_ * 0.12), tag_top_,
|
||||
w_ - 2 * std::min(0.6 * s_, w_ * 0.12), tag_bottom_ - tag_top_};
|
||||
painter->save();
|
||||
painter->setPen(QPen(QColor(120, 120, 120), 0.3));
|
||||
painter->setBrush(sectional_ ? QColor(Qt::black) : QColor(Qt::white));
|
||||
painter->drawRect(tag_);
|
||||
painter->restore();
|
||||
drawVerticalText(painter, tag_, numbers_.join(QStringLiteral(" / ")),
|
||||
sectional_ ? QColor(Qt::white) : QColor(Qt::black));
|
||||
|
||||
//Clickable zones : the whole terminal for a terminal with one level,
|
||||
//the rows of wire entries of the level (top and bottom) for a terminal with levels.
|
||||
//Only the terminals with an xref (element placed on a folio) are clickable.
|
||||
QVector<XrefRect> terminal_rects_;
|
||||
for (int k = 0 ; k < levels_ ; ++k)
|
||||
{
|
||||
if (g_.reals.at(k)->xref().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
const int physical_{g_.physical_index};
|
||||
const int real_{g_.real_indexes.at(k)};
|
||||
if (levels_ == 1) {
|
||||
terminal_rects_.append({body_, physical_, real_});
|
||||
} else {
|
||||
const qreal top_h_{length_ * (band_top - entries_top) / levels_};
|
||||
terminal_rects_.append({QRectF{x_, length_ * entries_top + k * top_h_, w_, top_h_}, physical_, real_});
|
||||
terminal_rects_.append({QRectF{x_, length_ * (1 - entries_top) - (k + 1) * top_h_, w_, top_h_}, physical_, real_});
|
||||
}
|
||||
}
|
||||
if (xref_rects) {
|
||||
*xref_rects << terminal_rects_;
|
||||
}
|
||||
|
||||
//Highlight the terminal under the mouse, like the xref of the schematic style
|
||||
if (!hover_pos.isNull())
|
||||
{
|
||||
int hovered_real_{-1};
|
||||
for (const auto &r_ : std::as_const(terminal_rects_)) {
|
||||
if (r_.rect.contains(hover_pos)) {
|
||||
hovered_real_ = r_.real;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hovered_real_ >= 0)
|
||||
{
|
||||
painter->save();
|
||||
painter->setPen(QPen(QColor(0, 90, 255), std::max<qreal>(1.0, 0.4 * s_)));
|
||||
painter->setBrush(Qt::NoBrush);
|
||||
for (const auto &r_ : std::as_const(terminal_rects_)) {
|
||||
if (r_.real == hovered_real_) {
|
||||
painter->drawRect(r_.rect.adjusted(0.5, 0.5, -0.5, -0.5));
|
||||
}
|
||||
}
|
||||
painter->restore();
|
||||
}
|
||||
}
|
||||
|
||||
//Bridge points : one channel by level
|
||||
for (int k = 0 ; k < levels_ ; ++k)
|
||||
{
|
||||
const auto &real_t{g_.reals.at(k)};
|
||||
if (!real_t->isBridged()) {
|
||||
continue;
|
||||
}
|
||||
QScopedPointer<AbstractBridgeInterface> bridge_{real_t->bridge()};
|
||||
if (!bridge_) {
|
||||
continue;
|
||||
}
|
||||
const qreal channel_h_{length_ * (channels_bottom - channels_top) / levels_};
|
||||
const qreal y_{length_ * channels_top + (k + 0.5) * channel_h_};
|
||||
bridges_[bridge_->uuid()].append(BridgePoint{QPointF{x_ + w_ / 2, y_}, w_});
|
||||
}
|
||||
|
||||
x_ += w_;
|
||||
|
||||
//Separator plate after the terminal
|
||||
if (g_.separator_after)
|
||||
{
|
||||
const qreal sep_w_{separator_width_mm * s_};
|
||||
drawSeparator(painter, QRectF{x_, 0, sep_w_, length_});
|
||||
x_ += sep_w_;
|
||||
}
|
||||
}
|
||||
|
||||
//Bridges : a red comb plugged in the channels
|
||||
for (const auto &points_ : std::as_const(bridges_))
|
||||
{
|
||||
if (points_.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
painter->save();
|
||||
const qreal thickness_{std::max<qreal>(1.0, 0.9 * s_)};
|
||||
painter->setPen(QPen(bridge_color, thickness_, Qt::SolidLine, Qt::FlatCap));
|
||||
painter->drawLine(points_.first().point, points_.last().point);
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(bridge_color.darker(140));
|
||||
for (const auto &p_ : points_) {
|
||||
const qreal r_{std::min(p_.width * 0.18, thickness_ * 0.8)};
|
||||
painter->drawEllipse(p_.point, r_, r_);
|
||||
}
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
draw_end_stop();
|
||||
|
||||
//The cables under the strip : the wires of a cable join it, and the
|
||||
//cable ends together in the box naming where it goes -- drawn exactly
|
||||
//like the schematic style draws them, so both styles tell the same thing
|
||||
if (!cable_cells_.isEmpty())
|
||||
{
|
||||
painter->save();
|
||||
painter->setFont(pattern.font());
|
||||
painter->setPen(QPen(Qt::black, 0.5));
|
||||
Helpers::drawCables(painter, cable_cells_, pattern);
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
} //End namespace Realistic
|
||||
} //End namespace TerminalStripDrawer
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
Copyright 2006-2026 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 REALISTICTERMINALSTRIPDRAWER_H
|
||||
#define REALISTICTERMINALSTRIPDRAWER_H
|
||||
|
||||
#include "terminalstripdrawer.h"
|
||||
|
||||
class QPainter;
|
||||
|
||||
/**
|
||||
* Drawing of a terminal strip at scale, seen from above, like the
|
||||
* mounting drawing of a cabinet. Used by the styles
|
||||
* RealisticPushIn and RealisticScrew of TerminalStripLayoutPattern.
|
||||
*
|
||||
* The strip is drawn from the left to the right :
|
||||
* end stop, group marker carrier (with the name of the strip), terminals, end stop.
|
||||
* The width of each terminal is its real width (mm) multiplied by the scale of the layout.
|
||||
* A terminal with several levels is drawn with one row of wire entries by level,
|
||||
* at the top and at the bottom.
|
||||
*
|
||||
* paint() fill @a xref_rects with the zone of each terminal which have an xref,
|
||||
* with the same indexes than the schematic style : the item use them to go to the
|
||||
* element on double click, to show the xref in a tool tip and to make clickable
|
||||
* links in an exported PDF. The terminal under @a hover_pos is outlined in blue.
|
||||
*/
|
||||
namespace TerminalStripDrawer
|
||||
{
|
||||
namespace Realistic
|
||||
{
|
||||
QRectF boundingRect(const AbstractTerminalStripInterface *strip,
|
||||
const TerminalStripLayoutPattern &pattern);
|
||||
void paint(QPainter *painter,
|
||||
const AbstractTerminalStripInterface &strip,
|
||||
const TerminalStripLayoutPattern &pattern,
|
||||
TerminalStripLayoutPattern::DrawStyle style,
|
||||
bool preview_draw = false,
|
||||
const QPointF &hover_pos = QPointF(),
|
||||
QVector<XrefRect> *xref_rects = nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // REALISTICTERMINALSTRIPDRAWER_H
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@
|
||||
#ifndef TERMINALSTRIPDRAWER_H
|
||||
#define TERMINALSTRIPDRAWER_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QPointer>
|
||||
#include <QUuid>
|
||||
|
||||
@@ -77,6 +78,24 @@ namespace TerminalStripDrawer
|
||||
virtual QString cable() const { return QString(); }
|
||||
virtual QString cableWire() const { return QString(); }
|
||||
virtual bool isShield() const { return false; }
|
||||
//Installation, Location and Label (BMK) of the piece the cable
|
||||
//goes to at its far end -- the same three fields the cable list
|
||||
//shows for a target -- drawn in a box under the cable. Empty
|
||||
//when the terminal has no cable, or when the cable lands on
|
||||
//nothing past the strip's own terminals.
|
||||
virtual QVector<QString> cableTarget() const { return QVector<QString>(); }
|
||||
//Color and width (mm) of the terminal, only used to draw the terminal.
|
||||
//An invalid color means "no override" : the default color of the type is used.
|
||||
//A width <= 0 means "unknown" : the default width of the layout is used.
|
||||
virtual QColor color() const { return QColor(); }
|
||||
//Second color of a two-tone color (like green/yellow), drawn as stripes over color()
|
||||
virtual QColor stripeColor() const { return QColor(); }
|
||||
virtual qreal widthMm() const { return 0; }
|
||||
//Label of the terminal chosen in the catalog (TerminalCatalog), empty if unknown.
|
||||
//The realistic styles draw its clamp (screw or push-in) instead of the one of the layout.
|
||||
virtual QString catalogEntry() const { return QString(); }
|
||||
//A separator plate is mounted after the terminal (drawn by the realistic styles)
|
||||
virtual bool hasSeparatorAfter() const { return false; }
|
||||
};
|
||||
|
||||
class AbstractPhysicalTerminalInterface
|
||||
@@ -113,10 +132,14 @@ namespace TerminalStripDrawer
|
||||
|
||||
void setPreviewDraw(bool draw = true);
|
||||
|
||||
void setDrawStyle(TerminalStripLayoutPattern::DrawStyle style);
|
||||
TerminalStripLayoutPattern::DrawStyle drawStyle() const;
|
||||
|
||||
void setMouseHoverPos(const QPointF &pos);
|
||||
bool mouseHoverXref() const;
|
||||
bool needUpdate();
|
||||
hoverTerminal hoveredXref() const;
|
||||
hoverTerminal xrefAt(const QPointF &pos) const;
|
||||
QVector<XrefRect> xrefRects() const;
|
||||
|
||||
private:
|
||||
@@ -127,6 +150,7 @@ namespace TerminalStripDrawer
|
||||
QSharedPointer <AbstractTerminalStripInterface> m_strip;
|
||||
QSharedPointer<TerminalStripLayoutPattern> m_pattern;
|
||||
bool m_preview_draw { false };
|
||||
TerminalStripLayoutPattern::DrawStyle m_draw_style { TerminalStripLayoutPattern::DrawStyle::Schematic };
|
||||
QPointF m_mouse_hover_pos;
|
||||
QRectF m_united_xref_text_rect;
|
||||
bool m_last_mouse_pos_in_xrefs_rect{false};
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
Copyright 2006-2026 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 TERMINALSTRIPDRAWERHELPERS_H
|
||||
#define TERMINALSTRIPDRAWERHELPERS_H
|
||||
|
||||
#include "terminalstripdrawer.h"
|
||||
|
||||
class QPainter;
|
||||
|
||||
/**
|
||||
* Functions shared by the drawing styles of a terminal strip
|
||||
* (schematic style in terminalstripdrawer.cpp, realistic style in realisticterminalstripdrawer.cpp).
|
||||
*/
|
||||
namespace TerminalStripDrawer
|
||||
{
|
||||
namespace Helpers
|
||||
{
|
||||
//Width (mm) of the reference terminal (2.5 mm2), used when the width of a terminal is unknown
|
||||
qreal referenceTerminalWidthMm();
|
||||
//Fill color and stripe color (can be invalid) of a terminal
|
||||
void terminalColors(const QSharedPointer<AbstractRealTerminalInterface> &real_t,
|
||||
QColor &fill, QColor &stripe);
|
||||
//Black or white, the more readable on background
|
||||
QColor contrastColor(const QColor &background);
|
||||
//Number of a terminal : "1" for "XAUZ1:1"
|
||||
QString terminalNumber(const QString &label);
|
||||
//Symbol of the type of the terminal (fuse, diode, led...) scaled to fit in box
|
||||
void drawTypeSymbol(QPainter *painter, const QRectF &box,
|
||||
ElementData::TerminalType type, bool led);
|
||||
//What is needed to draw the cable under a terminal
|
||||
struct CableCell
|
||||
{
|
||||
qreal x{0}; ///< x of the middle of the terminal, in the strip coordinates
|
||||
qreal bottom{0}; ///< y of the bottom of the terminal
|
||||
QString cable; ///< name of the cable (hose)
|
||||
QString wire; ///< color / number of the wire
|
||||
bool shield{false}; ///< true if the terminal is the shield of the cable
|
||||
QSharedPointer<AbstractRealTerminalInterface> terminal; ///< to ask where the cable goes
|
||||
};
|
||||
//Draw the cables under the terminals, whichever style draws them :
|
||||
//consecutive terminals with the same cable share one cable, which ends
|
||||
//together in a short stub and a box naming where it goes (see drawCables()
|
||||
//in terminalstripdrawer.cpp for the whole drawing)
|
||||
void drawCables(QPainter *painter, const QVector<CableCell> &cells,
|
||||
const TerminalStripLayoutPattern &pattern);
|
||||
//How much deeper than the end zone (m_cable_end_length) the boxes of
|
||||
//the cables need: a box reaching into another one is set lower than
|
||||
//the others, and the strip must reserve the room for it or the lower
|
||||
//box is cut off. The same rule the drawing uses, worked out of the
|
||||
//same cells, so the two can never disagree.
|
||||
qreal cableBoxesExtraDepth(const QVector<CableCell> &cells,
|
||||
const TerminalStripLayoutPattern &pattern);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // TERMINALSTRIPDRAWERHELPERS_H
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "../../project/projectpropertieshandler.h"
|
||||
#include "../../qetgraphicsitem/qgraphicsitemutility.h"
|
||||
#include "../terminalstrip.h"
|
||||
#include "../terminalstripbridge.h"
|
||||
#include "../physicalterminal.h"
|
||||
#include "../realterminal.h"
|
||||
#include "../ui/terminalstripeditorwindow.h"
|
||||
@@ -38,6 +39,7 @@ TerminalStripItem::TerminalStripItem(QPointer<TerminalStrip> strip,
|
||||
setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable);
|
||||
setAcceptHoverEvents(true);
|
||||
setDefaultLayout();
|
||||
connectStrip(strip);
|
||||
}
|
||||
|
||||
TerminalStripItem::TerminalStripItem(QGraphicsItem *parent) :
|
||||
@@ -50,6 +52,7 @@ TerminalStripItem::TerminalStripItem(QGraphicsItem *parent) :
|
||||
void TerminalStripItem::setTerminalStrip(TerminalStrip *strip)
|
||||
{
|
||||
m_strip = strip;
|
||||
connectStrip(strip);
|
||||
m_drawer.setStrip(QSharedPointer<TerminalStripDrawer::TrueTerminalStrip> {
|
||||
new TerminalStripDrawer::TrueTerminalStrip { strip }});
|
||||
m_pending_strip_uuid = QUuid();
|
||||
@@ -142,12 +145,43 @@ void TerminalStripItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
|
||||
if (m_drawer.needUpdate()) {
|
||||
update();
|
||||
}
|
||||
|
||||
//The realistic styles don't write the xref : show it in a tool tip
|
||||
if (m_drawer.drawStyle() != TerminalStripLayoutPattern::DrawStyle::Schematic) {
|
||||
setToolTip(xrefToolTip(m_drawer.xrefAt(hoverMousePos())));
|
||||
}
|
||||
}
|
||||
|
||||
void TerminalStripItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
|
||||
{
|
||||
QetGraphicsItem::hoverLeaveEvent(event);
|
||||
m_drawer.setMouseHoverPos(QPointF{});
|
||||
setToolTip(QString());
|
||||
//Remove the highlight of the xref (or of the terminal) under the mouse
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripItem::xrefToolTip
|
||||
* @param terminal : physical and real index of a terminal of the strip
|
||||
* @return the text of the tool tip of @a terminal : its label and its xref,
|
||||
* or an empty string if @a terminal is invalid.
|
||||
*/
|
||||
QString TerminalStripItem::xrefToolTip(const TerminalStripDrawer::hoverTerminal &terminal) const
|
||||
{
|
||||
if (!m_strip || terminal.physical < 0 || terminal.real < 0) {
|
||||
return QString();
|
||||
}
|
||||
const auto physical_terminal{m_strip->physicalTerminal(terminal.physical)};
|
||||
if (physical_terminal.isNull()) {
|
||||
return QString();
|
||||
}
|
||||
const auto real_terminal{physical_terminal->realTerminal(terminal.real)};
|
||||
if (real_terminal.isNull() || real_terminal->Xref().isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
return tr("%1 : %2\nDouble-click to go to the element")
|
||||
.arg(real_terminal->label(), real_terminal->Xref());
|
||||
}
|
||||
|
||||
void TerminalStripItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
|
||||
@@ -205,6 +239,60 @@ void TerminalStripItem::setLayout(QSharedPointer<TerminalStripLayoutPattern> lay
|
||||
m_drawer.setLayout(layout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripItem::setDrawStyle
|
||||
* Set how this item draw the terminals (schematic, realistic...).
|
||||
* The style is chosen when the item is added to a folio and saved with the item.
|
||||
* @param style
|
||||
*/
|
||||
void TerminalStripItem::setDrawStyle(TerminalStripLayoutPattern::DrawStyle style)
|
||||
{
|
||||
if (style == m_drawer.drawStyle()) {
|
||||
return;
|
||||
}
|
||||
prepareGeometryChange();
|
||||
m_drawer.setDrawStyle(style);
|
||||
update();
|
||||
}
|
||||
|
||||
TerminalStripLayoutPattern::DrawStyle TerminalStripItem::drawStyle() const {
|
||||
return m_drawer.drawStyle();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripItem::connectStrip
|
||||
* Redraw this item as soon as @a strip or one of its terminals changes
|
||||
* (terminal strip editor applied, undo, redo...).
|
||||
* The connections to the previous strip are removed.
|
||||
* @param strip
|
||||
*/
|
||||
void TerminalStripItem::connectStrip(TerminalStrip *strip)
|
||||
{
|
||||
for (const auto &connection_ : std::as_const(m_strip_connections)) {
|
||||
disconnect(connection_);
|
||||
}
|
||||
m_strip_connections.clear();
|
||||
|
||||
if (strip)
|
||||
{
|
||||
m_strip_connections << connect(strip, &TerminalStrip::orderChanged, this, &TerminalStripItem::stripChanged)
|
||||
<< connect(strip, &TerminalStrip::bridgeChanged, this, &TerminalStripItem::stripChanged)
|
||||
<< connect(strip, &TerminalStrip::bridgeColorChanged, this, &TerminalStripItem::stripChanged)
|
||||
<< connect(strip, &TerminalStrip::dataChanged, this, &TerminalStripItem::stripChanged);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripItem::stripChanged
|
||||
* The strip or one of its terminals changed : the size of the drawing
|
||||
* can change (width of the terminals, number of terminals...), redraw it.
|
||||
*/
|
||||
void TerminalStripItem::stripChanged()
|
||||
{
|
||||
prepareGeometryChange();
|
||||
update();
|
||||
}
|
||||
|
||||
void TerminalStripItem::setDefaultLayout()
|
||||
{
|
||||
if (m_strip && m_strip->project()) {
|
||||
|
||||
@@ -54,6 +54,8 @@ class TerminalStripItem : public QetGraphicsItem
|
||||
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void refreshPending();
|
||||
void setLayout(QSharedPointer<TerminalStripLayoutPattern> layout);
|
||||
void setDrawStyle(TerminalStripLayoutPattern::DrawStyle style);
|
||||
TerminalStripLayoutPattern::DrawStyle drawStyle() const;
|
||||
|
||||
/**
|
||||
* @brief The PdfRef struct
|
||||
@@ -73,9 +75,13 @@ class TerminalStripItem : public QetGraphicsItem
|
||||
|
||||
private:
|
||||
void setDefaultLayout();
|
||||
QString xrefToolTip(const TerminalStripDrawer::hoverTerminal &terminal) const;
|
||||
void connectStrip(TerminalStrip *strip);
|
||||
void stripChanged();
|
||||
|
||||
private:
|
||||
QPointer<TerminalStrip> m_strip;
|
||||
QList<QMetaObject::Connection> m_strip_connections;
|
||||
TerminalStripDrawer::TerminalStripDrawer m_drawer;
|
||||
QUuid m_pending_strip_uuid;
|
||||
};
|
||||
|
||||
@@ -153,6 +153,36 @@ namespace TerminalStripDrawer
|
||||
return m_real ? m_real->isShield() : false;
|
||||
}
|
||||
|
||||
QVector<QString> TrueRealTerminal::cableTarget() const
|
||||
{
|
||||
return m_real ? m_real->cableTarget() : QVector<QString>();
|
||||
}
|
||||
|
||||
QColor TrueRealTerminal::color() const
|
||||
{
|
||||
return m_real ? m_real->color() : QColor();
|
||||
}
|
||||
|
||||
QColor TrueRealTerminal::stripeColor() const
|
||||
{
|
||||
return m_real ? m_real->stripeColor() : QColor();
|
||||
}
|
||||
|
||||
qreal TrueRealTerminal::widthMm() const
|
||||
{
|
||||
return m_real ? m_real->widthMm() : 0;
|
||||
}
|
||||
|
||||
QString TrueRealTerminal::catalogEntry() const
|
||||
{
|
||||
return m_real ? m_real->catalogEntry() : QString();
|
||||
}
|
||||
|
||||
bool TrueRealTerminal::hasSeparatorAfter() const
|
||||
{
|
||||
return m_real ? m_real->hasSeparatorAfter() : false;
|
||||
}
|
||||
|
||||
TrueBridge::TrueBridge(QSharedPointer<TerminalStripBridge> bridge) :
|
||||
m_bridge { bridge }
|
||||
{}
|
||||
|
||||
@@ -64,6 +64,12 @@ namespace TerminalStripDrawer
|
||||
QString cable() const override;
|
||||
QString cableWire() const override;
|
||||
bool isShield() const override;
|
||||
QVector<QString> cableTarget() const override;
|
||||
QColor color() const override;
|
||||
QColor stripeColor() const override;
|
||||
qreal widthMm() const override;
|
||||
QString catalogEntry() const override;
|
||||
bool hasSeparatorAfter() const override;
|
||||
|
||||
private:
|
||||
QSharedPointer<RealTerminal> m_real;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "../../qetgraphicsitem/terminalelement.h"
|
||||
#include "../realterminal.h"
|
||||
#include "../physicalterminal.h"
|
||||
#include "../terminalstrip.h"
|
||||
|
||||
/**
|
||||
* @brief AddTerminalToStripCommand::AddTerminalToStripCommand
|
||||
@@ -234,3 +235,72 @@ void MoveTerminalCommand::redo()
|
||||
m_new_strip->addTerminals(m_terminal);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief AddSpareTerminalsCommand::AddSpareTerminalsCommand
|
||||
* @param strip : the strip where the spare terminals are inserted
|
||||
* @param pos : position of the first spare terminal in the strip,
|
||||
* -1 to add them at the end of the strip
|
||||
* @param spare_terminals : spare terminals made by RealTerminal::createSpare()
|
||||
* @param parent : parent undo command
|
||||
*/
|
||||
AddSpareTerminalsCommand::AddSpareTerminalsCommand(TerminalStrip *strip, int pos,
|
||||
const QVector<QSharedPointer<RealTerminal>> &spare_terminals,
|
||||
QUndoCommand *parent) :
|
||||
QUndoCommand{parent},
|
||||
m_strip{strip},
|
||||
m_pos{pos},
|
||||
m_spare_terminals{spare_terminals}
|
||||
{
|
||||
const auto count = spare_terminals.size();
|
||||
const auto strip_name = strip ? strip->name() : QString();
|
||||
setText(strip_name.isEmpty()
|
||||
? QObject::tr("Add %n spare terminals", "", count)
|
||||
: QObject::tr("Add %n spare terminals to terminal group %1", "", count).arg(strip_name));
|
||||
}
|
||||
|
||||
void AddSpareTerminalsCommand::undo()
|
||||
{
|
||||
if (m_strip) {
|
||||
m_strip->removeTerminals(m_physical_terminals);
|
||||
}
|
||||
}
|
||||
|
||||
void AddSpareTerminalsCommand::redo()
|
||||
{
|
||||
if (!m_strip) {
|
||||
return;
|
||||
}
|
||||
//The first time the physical terminals are made,
|
||||
//after an undo the same ones are inserted again
|
||||
if (m_physical_terminals.isEmpty()) {
|
||||
m_physical_terminals = m_strip->insertSpareTerminals(m_pos, m_spare_terminals);
|
||||
} else {
|
||||
m_strip->insertTerminals(m_pos, m_physical_terminals);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ChangeSpareTerminalDataCommand::ChangeSpareTerminalDataCommand
|
||||
* @param spare_terminal : the spare terminal to change
|
||||
* @param new_data : the new data of the terminal
|
||||
* @param parent : parent undo command
|
||||
*/
|
||||
ChangeSpareTerminalDataCommand::ChangeSpareTerminalDataCommand(const QSharedPointer<RealTerminal> &spare_terminal,
|
||||
const ElementData &new_data,
|
||||
QUndoCommand *parent) :
|
||||
QUndoCommand{parent},
|
||||
m_spare_terminal{spare_terminal},
|
||||
m_old_data{spare_terminal->spareData()},
|
||||
m_new_data{new_data}
|
||||
{
|
||||
setText(QObject::tr("Modify the spare terminal %1").arg(spare_terminal->label()));
|
||||
}
|
||||
|
||||
void ChangeSpareTerminalDataCommand::undo() {
|
||||
m_spare_terminal->setSpareData(m_old_data);
|
||||
}
|
||||
|
||||
void ChangeSpareTerminalDataCommand::redo() {
|
||||
m_spare_terminal->setSpareData(m_new_data);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#include <QUndoCommand>
|
||||
#include <QPointer>
|
||||
#include <QVector>
|
||||
#include <QSharedPointer>
|
||||
#include "../../properties/elementdata.h"
|
||||
|
||||
class TerminalElement;
|
||||
class TerminalStrip;
|
||||
@@ -89,4 +91,45 @@ class MoveTerminalCommand : public QUndoCommand
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The AddSpareTerminalsCommand class
|
||||
* Insert spare terminals (on no folio) in a terminal strip,
|
||||
* at a position of the strip.
|
||||
*/
|
||||
class AddSpareTerminalsCommand : public QUndoCommand
|
||||
{
|
||||
public:
|
||||
AddSpareTerminalsCommand(TerminalStrip *strip, int pos,
|
||||
const QVector<QSharedPointer<RealTerminal>> &spare_terminals,
|
||||
QUndoCommand *parent = nullptr);
|
||||
|
||||
void undo() override;
|
||||
void redo() override;
|
||||
|
||||
private:
|
||||
QPointer<TerminalStrip> m_strip;
|
||||
int m_pos = -1;
|
||||
QVector<QSharedPointer<RealTerminal>> m_spare_terminals;
|
||||
QVector<QSharedPointer<PhysicalTerminal>> m_physical_terminals;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The ChangeSpareTerminalDataCommand class
|
||||
* Change the data (label, type, section...) of a spare terminal
|
||||
*/
|
||||
class ChangeSpareTerminalDataCommand : public QUndoCommand
|
||||
{
|
||||
public:
|
||||
ChangeSpareTerminalDataCommand(const QSharedPointer<RealTerminal> &spare_terminal,
|
||||
const ElementData &new_data,
|
||||
QUndoCommand *parent = nullptr);
|
||||
|
||||
void undo() override;
|
||||
void redo() override;
|
||||
|
||||
private:
|
||||
QSharedPointer<RealTerminal> m_spare_terminal;
|
||||
ElementData m_old_data, m_new_data;
|
||||
};
|
||||
|
||||
#endif // ADDTERMINALTOSTRIPCOMMAND_H
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
#include "../terminalstrip.h"
|
||||
#include "../physicalterminal.h"
|
||||
#include "../realterminal.h"
|
||||
#include "../../diagram.h"
|
||||
#include "../../positionorder.h"
|
||||
#include "../../qetgraphicsitem/element.h"
|
||||
#include "../../utils/qetutils.h"
|
||||
|
||||
SortTerminalStripCommand::SortTerminalStripCommand(TerminalStrip *strip, QUndoCommand *parent) :
|
||||
@@ -47,16 +50,73 @@ void SortTerminalStripCommand::redo()
|
||||
|
||||
void SortTerminalStripCommand::sort()
|
||||
{
|
||||
auto label_of = [](const QSharedPointer<PhysicalTerminal> &t) -> QString
|
||||
//The plan position of a physical terminal is the position of the
|
||||
//level that sits on a folio; a terminal whose levels are all
|
||||
//spares has no position and is ordered by label only.
|
||||
auto position_of = [](const QSharedPointer<PhysicalTerminal> &t_) -> QSharedPointer<RealTerminal>
|
||||
{
|
||||
return t->realTerminalCount() ? t->realTerminals().constLast()->label()
|
||||
: QString();
|
||||
if (!t_) {
|
||||
return {};
|
||||
}
|
||||
for (const auto &real_ : t_->realTerminals()) {
|
||||
if (real_ && real_->element()) {
|
||||
return real_;
|
||||
}
|
||||
}
|
||||
return t_->realTerminalCount() ? t_->realTerminals().constFirst()
|
||||
: QSharedPointer<RealTerminal>();
|
||||
};
|
||||
|
||||
std::stable_sort(m_new_order.begin(), m_new_order.end(),
|
||||
[&label_of](const QSharedPointer<PhysicalTerminal> &arg1,
|
||||
[&position_of](const QSharedPointer<PhysicalTerminal> &arg1,
|
||||
const QSharedPointer<PhysicalTerminal> &arg2)
|
||||
{
|
||||
return QETUtils::naturalLessThan(label_of(arg1), label_of(arg2));
|
||||
return planOrderLessThan(position_of(arg1), position_of(arg2));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@brief SortTerminalStripCommand::planOrderLessThan
|
||||
Order two real terminals the way the plan is drawn : folio by folio,
|
||||
then left to right. Terminals with no folio position (spares) come
|
||||
last, and the label only breaks ties, so the result stays stable and
|
||||
predictable whatever the labels look like ("X1:N" and "X1:PE" stand
|
||||
where they are drawn, not at the end).
|
||||
Both the auto import and sort() use this, so the two orders of the
|
||||
same strip can never fight each other.
|
||||
@a a_ terminal to compare
|
||||
@a b_ other terminal to compare
|
||||
@return true if @a a_ comes before @a b_
|
||||
*/
|
||||
bool SortTerminalStripCommand::planOrderLessThan(
|
||||
const QSharedPointer<RealTerminal> &a_,
|
||||
const QSharedPointer<RealTerminal> &b_)
|
||||
{
|
||||
if (!a_ || !b_) {
|
||||
return a_ && !b_; //a missing terminal goes last
|
||||
}
|
||||
|
||||
const Element *ea = a_->element();
|
||||
const Element *eb = b_->element();
|
||||
auto folio_of = [](const Element *e_) -> int
|
||||
{
|
||||
return (e_ && e_->diagram()) ? e_->diagram()->folioIndex() : -1;
|
||||
};
|
||||
|
||||
const int fa = folio_of(ea);
|
||||
const int fb = folio_of(eb);
|
||||
if (fa != fb) {
|
||||
//A terminal with no folio has no place in the plan order
|
||||
if (fa < 0) {
|
||||
return false;
|
||||
}
|
||||
if (fb < 0) {
|
||||
return true;
|
||||
}
|
||||
return fa < fb;
|
||||
}
|
||||
if (fa >= 0 && ea->pos() != eb->pos()) {
|
||||
return PositionOrder::roundedXThenY(ea->pos(), eb->pos());
|
||||
}
|
||||
return QETUtils::naturalLessThan(a_->label(), b_->label());
|
||||
}
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
|
||||
#include <QUndoCommand>
|
||||
#include <QPointer>
|
||||
#include <QSharedPointer>
|
||||
#include <QVector>
|
||||
|
||||
class TerminalStrip;
|
||||
class PhysicalTerminal;
|
||||
class RealTerminal;
|
||||
|
||||
/**
|
||||
* @brief The SortTerminalStripCommand class
|
||||
@@ -38,6 +40,25 @@ class SortTerminalStripCommand : public QUndoCommand
|
||||
void undo() override;
|
||||
void redo() override;
|
||||
|
||||
/**
|
||||
* @brief planOrderLessThan
|
||||
* Order two real terminals the way the plan is drawn : folio by
|
||||
* folio, then left to right (top to bottom only breaks ties
|
||||
* within a column). A spare has no folio and comes last, by
|
||||
* label, and the label breaks every remaining tie (two
|
||||
* terminals at the very same spot), which keeps the result
|
||||
* stable.
|
||||
*
|
||||
* The auto import sorts with it too, so the order it works out
|
||||
* cannot be rewritten by the SortTerminalStripCommand it then
|
||||
* pushes for the strip it just created.
|
||||
* @param a_ : first terminal
|
||||
* @param b_ : second terminal
|
||||
* @return true if @a a_ comes before @a b_
|
||||
*/
|
||||
static bool planOrderLessThan(const QSharedPointer<RealTerminal> &a_,
|
||||
const QSharedPointer<RealTerminal> &b_);
|
||||
|
||||
private:
|
||||
void sort();
|
||||
|
||||
|
||||
@@ -19,7 +19,59 @@
|
||||
#include "terminalstrip.h"
|
||||
#include "../qetgraphicsitem/terminalelement.h"
|
||||
#include "physicalterminal.h"
|
||||
#include "terminalcolor.h"
|
||||
#include "terminalcatalog.h"
|
||||
#include "../qetgraphicsitem/conductor.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../qetgraphicsitem/terminal.h"
|
||||
#include "../qetinformation.h"
|
||||
#include "../diagram.h"
|
||||
#include "../cable/cable.h"
|
||||
#include "../cable/cablemanager.h"
|
||||
#include "../qetproject.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
|
||||
namespace
|
||||
{
|
||||
/**
|
||||
* The cable cores wired to one terminal element, worked out from
|
||||
* the cables of the project (PR #1438). A core knows its
|
||||
* conductor, and CableManager::refreshLabels() writes the cable
|
||||
* and the core into the properties of that conductor, so reading
|
||||
* the conductors of the element is enough.
|
||||
* Each pair is (cable designation, core colour or number).
|
||||
*/
|
||||
QList<QPair<QString, QString>> wiredCores(Element *element)
|
||||
{
|
||||
QList<QPair<QString, QString>> cores;
|
||||
if (!element || !element->diagram()) {
|
||||
return cores;
|
||||
}
|
||||
const auto project = element->diagram()->project();
|
||||
for (const auto terminal : element->terminals())
|
||||
{
|
||||
for (const auto conductor : terminal->conductors())
|
||||
{
|
||||
const auto properties = conductor->ownProperties();
|
||||
const auto cable = CableManager::cableByUuid(project, properties.m_cable_uuid);
|
||||
if (!cable || cable->designation().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
auto wire = cable->colorOfCore(properties.m_cable_slot);
|
||||
if (wire.isEmpty() && properties.m_cable_slot >= 0) {
|
||||
wire = QString::number(properties.m_cable_slot + 1);
|
||||
}
|
||||
const QPair<QString, QString> core(cable->designation(), wire);
|
||||
if (!cores.contains(core)) {
|
||||
cores.append(core);
|
||||
}
|
||||
}
|
||||
}
|
||||
return cores;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal
|
||||
@@ -30,6 +82,118 @@ RealTerminal::RealTerminal(Element *terminal) :
|
||||
m_element(terminal)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::createSpare
|
||||
* Create a spare terminal (a reserved terminal) : a terminal of a strip
|
||||
* which is not drawn on any folio. Its data (label, type, section,
|
||||
* width...) are its own, stored with the terminal strip.
|
||||
* @param data : the data of the terminal, only the terminal type,
|
||||
* function, led and the informations are used.
|
||||
* @param uuid : the uuid of the spare terminal
|
||||
* @return the new spare terminal, not yet added to a strip
|
||||
*/
|
||||
QSharedPointer<RealTerminal> RealTerminal::createSpare(const ElementData &data, const QUuid &uuid)
|
||||
{
|
||||
auto raw_ = new RealTerminal(nullptr);
|
||||
raw_->m_spare = true;
|
||||
raw_->m_spare_uuid = uuid.isNull() ? QUuid::createUuid() : uuid;
|
||||
raw_->m_spare_data.m_type = ElementData::Terminal;
|
||||
raw_->m_spare_data.setTerminalType(data.terminalType());
|
||||
raw_->m_spare_data.setTerminalFunction(data.terminalFunction());
|
||||
raw_->m_spare_data.setTerminalLED(data.terminalLed());
|
||||
raw_->m_spare_data.m_informations = data.m_informations;
|
||||
return raw_->sharedRef();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::spareFromXml
|
||||
* @param xml_element : a real terminal saved by toXml()
|
||||
* @return the spare terminal saved in @a xml_element,
|
||||
* or a null pointer if @a xml_element is not a spare terminal.
|
||||
*/
|
||||
QSharedPointer<RealTerminal> RealTerminal::spareFromXml(const QDomElement &xml_element)
|
||||
{
|
||||
if (xml_element.tagName() != xmlTagName() ||
|
||||
!xml_element.hasAttribute(QStringLiteral("spare_uuid"))) {
|
||||
return QSharedPointer<RealTerminal>();
|
||||
}
|
||||
|
||||
ElementData data_;
|
||||
data_.setTerminalType(ElementData::terminalTypeFromString(
|
||||
xml_element.attribute(QStringLiteral("type"))));
|
||||
data_.setTerminalFunction(ElementData::terminalFunctionFromString(
|
||||
xml_element.attribute(QStringLiteral("function"))));
|
||||
data_.setTerminalLED(QET::infoFlagIsTrue(xml_element.attribute(QStringLiteral("led"))));
|
||||
data_.m_informations.fromXml(xml_element.firstChildElement(QStringLiteral("informations")),
|
||||
QStringLiteral("information"));
|
||||
|
||||
return createSpare(data_, QUuid(xml_element.attribute(QStringLiteral("spare_uuid"))));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::isSpare
|
||||
* @return true if this terminal is a spare terminal, not drawn on any folio
|
||||
* @sa createSpare
|
||||
*/
|
||||
bool RealTerminal::isSpare() const {
|
||||
return m_spare;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::spareData
|
||||
* @return the data of this spare terminal, an empty ElementData
|
||||
* if this terminal is not a spare terminal.
|
||||
*/
|
||||
ElementData RealTerminal::spareData() const {
|
||||
return m_spare_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::setSpareData
|
||||
* Set the data of this spare terminal (nothing is done if this terminal
|
||||
* is not a spare terminal) and tell the strip, so that it is redrawn.
|
||||
* @param data
|
||||
*/
|
||||
void RealTerminal::setSpareData(const ElementData &data)
|
||||
{
|
||||
if (!m_spare) {
|
||||
return;
|
||||
}
|
||||
m_spare_data.setTerminalType(data.terminalType());
|
||||
m_spare_data.setTerminalFunction(data.terminalFunction());
|
||||
m_spare_data.setTerminalLED(data.terminalLed());
|
||||
m_spare_data.m_informations = data.m_informations;
|
||||
|
||||
if (auto strip_ = parentStrip()) {
|
||||
strip_->terminalDataChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::hasData
|
||||
* @return true if this terminal has data : it is linked to an element,
|
||||
* or it is a spare terminal.
|
||||
*/
|
||||
bool RealTerminal::hasData() const {
|
||||
return m_spare || !m_element.isNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::terminalData
|
||||
* @return the data of this terminal : the data of its element,
|
||||
* or its own data if it is a spare terminal.
|
||||
*/
|
||||
ElementData RealTerminal::terminalData() const
|
||||
{
|
||||
if (m_spare) {
|
||||
return m_spare_data;
|
||||
}
|
||||
if (m_element) {
|
||||
return m_element->elementData();
|
||||
}
|
||||
return ElementData();
|
||||
}
|
||||
|
||||
RealTerminal::~RealTerminal()
|
||||
{
|
||||
if (m_physical_terminal) {
|
||||
@@ -79,8 +243,22 @@ QWeakPointer<RealTerminal> RealTerminal::weakRef() {
|
||||
QDomElement RealTerminal::toXml(QDomDocument &parent_document) const
|
||||
{
|
||||
auto root_elmt = parent_document.createElement(this->xmlTagName());
|
||||
if (m_element)
|
||||
if (m_spare)
|
||||
{
|
||||
//A spare terminal is on no folio : its data are saved here
|
||||
root_elmt.setAttribute(QStringLiteral("spare_uuid"), m_spare_uuid.toString());
|
||||
root_elmt.setAttribute(QStringLiteral("type"), ElementData::terminalTypeToString(m_spare_data.terminalType()));
|
||||
root_elmt.setAttribute(QStringLiteral("function"), ElementData::terminalFunctionToString(m_spare_data.terminalFunction()));
|
||||
if (m_spare_data.terminalLed()) {
|
||||
root_elmt.setAttribute(QStringLiteral("led"), QStringLiteral("true"));
|
||||
}
|
||||
auto xml_info = parent_document.createElement(QStringLiteral("informations"));
|
||||
m_spare_data.m_informations.toXml(xml_info, QStringLiteral("information"));
|
||||
root_elmt.appendChild(xml_info);
|
||||
}
|
||||
else if (m_element) {
|
||||
root_elmt.setAttribute(QStringLiteral("element_uuid"), m_element->uuid().toString());
|
||||
}
|
||||
|
||||
return root_elmt;
|
||||
}
|
||||
@@ -134,7 +312,9 @@ int RealTerminal::level() const
|
||||
* @return the label of this real terminal
|
||||
*/
|
||||
QString RealTerminal::label() const {
|
||||
if (!m_element.isNull()) {
|
||||
if (m_spare) {
|
||||
return m_spare_data.m_informations.value(QStringLiteral("label")).toString();
|
||||
} else if (!m_element.isNull()) {
|
||||
return m_element->actualLabel();
|
||||
} else {
|
||||
return QLatin1String();
|
||||
@@ -160,20 +340,54 @@ QString RealTerminal::Xref() const
|
||||
* @return The name of the cable (hose) connected to this terminal.
|
||||
* The value is stored in the informations of the element.
|
||||
*/
|
||||
QString RealTerminal::cable() const {
|
||||
if (m_element) {
|
||||
return m_element->elementData().m_informations.value(cableInfoKey()).toString();
|
||||
QString RealTerminal::cable() const
|
||||
{
|
||||
const auto cores = wiredCores(m_element.data());
|
||||
if (!cores.isEmpty())
|
||||
{
|
||||
QStringList cables;
|
||||
for (const auto &core : cores) {
|
||||
if (!cables.contains(core.first)) {
|
||||
cables.append(core.first);
|
||||
}
|
||||
}
|
||||
return cables.join(QStringLiteral(", "));
|
||||
}
|
||||
|
||||
if (hasData()) {
|
||||
return terminalData().m_informations.value(cableInfoKey()).toString();
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::cableIsAutomatic
|
||||
* @return true when the cable and the core of this terminal come from
|
||||
* a cable of the project wired to it, false when they are the values
|
||||
* typed in by the user in the terminal strip editor.
|
||||
*/
|
||||
bool RealTerminal::cableIsAutomatic() const {
|
||||
return !wiredCores(m_element.data()).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::cableWire
|
||||
* @return The color / number of the wire of the cable, connected to this terminal.
|
||||
*/
|
||||
QString RealTerminal::cableWire() const {
|
||||
if (m_element) {
|
||||
return m_element->elementData().m_informations.value(cableWireInfoKey()).toString();
|
||||
QString RealTerminal::cableWire() const
|
||||
{
|
||||
const auto cores = wiredCores(m_element.data());
|
||||
if (!cores.isEmpty())
|
||||
{
|
||||
QStringList wires;
|
||||
for (const auto &core : cores) {
|
||||
wires.append(core.second);
|
||||
}
|
||||
return wires.join(QStringLiteral(", "));
|
||||
}
|
||||
|
||||
if (hasData()) {
|
||||
return terminalData().m_informations.value(cableWireInfoKey()).toString();
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
@@ -184,9 +398,349 @@ QString RealTerminal::cableWire() const {
|
||||
* and not one of its wires.
|
||||
*/
|
||||
bool RealTerminal::isShield() const {
|
||||
if (m_element) {
|
||||
if (hasData()) {
|
||||
return QET::infoFlagIsTrue(
|
||||
m_element->elementData().m_informations.value(shieldInfoKey()).toString());
|
||||
terminalData().m_informations.value(shieldInfoKey()).toString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::cableTarget
|
||||
* Where the cable of this terminal goes at its far end: the lines to
|
||||
* write in the box under the cable. What the project itself says when
|
||||
* a cable lands on a piece (see automaticCableTarget()), and the text
|
||||
* typed in by hand when none does -- the Target cell of the terminal
|
||||
* strip editor is read only in the first case, so a target the drawing
|
||||
* cannot work out is still worth naming by hand. A text typed in is
|
||||
* written one line per field, Installation, Location and Label (BMK),
|
||||
* the way the cell shows them: only a line break breaks it into fields.
|
||||
* A "/" written inside a field (a name like "ASP/01") is part of that
|
||||
* field and never breaks anything.
|
||||
* @return the lines to write in the box under the cable
|
||||
*/
|
||||
QVector<QString> RealTerminal::cableTarget() const
|
||||
{
|
||||
const auto automatic_{automaticCableTarget()};
|
||||
if (!automatic_.isEmpty()) {
|
||||
return automatic_;
|
||||
}
|
||||
|
||||
if (hasData())
|
||||
{
|
||||
const auto typed_{terminalData().m_informations
|
||||
.value(cableTargetInfoKey()).toString()};
|
||||
if (!typed_.isEmpty())
|
||||
{
|
||||
//One line by field, and only a line break breaks the text
|
||||
//into fields : a "/" written inside a field stays where it is
|
||||
QString text_{typed_};
|
||||
text_.replace(QStringLiteral("\r\n"), QStringLiteral("\n"));
|
||||
text_.replace(QLatin1Char('\r'), QLatin1Char('\n'));
|
||||
|
||||
QVector<QString> lines_;
|
||||
for (const QString &field_
|
||||
: text_.split(QLatin1Char('\n'), Qt::SkipEmptyParts)) {
|
||||
lines_ << field_;
|
||||
}
|
||||
return lines_;
|
||||
}
|
||||
}
|
||||
return QVector<QString>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::cableTargetIsAutomatic
|
||||
* @return true when the target of this terminal is worked out from the
|
||||
* project -- one of its cables lands on a piece worth naming -- and
|
||||
* false when only a text typed in by hand is there to show.
|
||||
*/
|
||||
bool RealTerminal::cableTargetIsAutomatic() const
|
||||
{
|
||||
return !automaticCableTarget().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::automaticCableTarget
|
||||
* Where the cable of this terminal goes at its far end: the
|
||||
* Installation, Location and Label (BMK) of the first piece the
|
||||
* wires of the cable land on, past the strip's own terminals, in
|
||||
* plan order -- the same three fields the cable list shows for a
|
||||
* target, in the same order. An empty field is left out; a terminal
|
||||
* with no cable, or a cable which lands on nothing else, gives no
|
||||
* lines at all.
|
||||
*
|
||||
* The cable is the one the wires drawn to this terminal carry; a
|
||||
* cable name typed by hand, with no wire behind it, is looked up by
|
||||
* its name among the cables of the project.
|
||||
* @return the lines to write in the box under the cable
|
||||
*/
|
||||
QVector<QString> RealTerminal::automaticCableTarget() const
|
||||
{
|
||||
if (m_spare || m_element.isNull()) {
|
||||
return QVector<QString>();
|
||||
}
|
||||
TerminalStrip *strip_ = parentStrip();
|
||||
if (!strip_ || !strip_->project()) {
|
||||
return QVector<QString>();
|
||||
}
|
||||
QETProject *project_{strip_->project()};
|
||||
|
||||
//The cable the wires drawn to this terminal are part of
|
||||
QUuid cable_uuid_;
|
||||
for (Terminal *terminal_ : m_element->terminals())
|
||||
{
|
||||
if (!terminal_) {
|
||||
continue;
|
||||
}
|
||||
for (Conductor *conductor_ : terminal_->conductors())
|
||||
{
|
||||
if (conductor_ && !conductor_->properties().m_cable_uuid.isNull()) {
|
||||
cable_uuid_ = conductor_->properties().m_cable_uuid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!cable_uuid_.isNull()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (cable_uuid_.isNull())
|
||||
{
|
||||
//A cable name typed by hand, no wire behind it: the first
|
||||
//cable of the project drawn under that name
|
||||
const auto name_{cable()};
|
||||
if (name_.isEmpty()) {
|
||||
return QVector<QString>();
|
||||
}
|
||||
for (Cable *cable_ : project_->cables())
|
||||
{
|
||||
if (cable_ && cable_->designation() == name_) {
|
||||
cable_uuid_ = cable_->uuid();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (cable_uuid_.isNull()) {
|
||||
return QVector<QString>();
|
||||
}
|
||||
}
|
||||
|
||||
//The own side: this strip's terminals never are the target
|
||||
QSet<QUuid> own_;
|
||||
for (const auto &physical_ : strip_->physicalTerminal())
|
||||
{
|
||||
if (!physical_) {
|
||||
continue;
|
||||
}
|
||||
for (const auto &real_ : physical_->realTerminals()) {
|
||||
if (real_ && real_->element()) {
|
||||
own_.insert(real_->element()->uuid());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Where the wires of the cable land: one piece whatever the
|
||||
//number of its cores, never a report (the cable continues on
|
||||
//the sheet the report points to) and never the strip's own
|
||||
//terminals. Same rule as the cable list (cablelistrows.cpp).
|
||||
QHash<QUuid, Element *> landed_;
|
||||
for (Diagram *diagram_ : project_->diagrams())
|
||||
{
|
||||
for (Conductor *conductor_ : diagram_->conductors())
|
||||
{
|
||||
if (!conductor_ || conductor_->properties().m_cable_uuid != cable_uuid_) {
|
||||
continue;
|
||||
}
|
||||
Terminal *ends_[2] = {conductor_->terminal1, conductor_->terminal2};
|
||||
for (Terminal *terminal_ : ends_)
|
||||
{
|
||||
if (!terminal_) {
|
||||
continue;
|
||||
}
|
||||
Element *element_ = terminal_->parentElement();
|
||||
if (!element_ || (element_->linkType() & Element::AllReport)) {
|
||||
continue;
|
||||
}
|
||||
if (own_.contains(element_->uuid())) {
|
||||
continue;
|
||||
}
|
||||
landed_.insert(element_->uuid(), element_);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (landed_.isEmpty()) {
|
||||
return QVector<QString>();
|
||||
}
|
||||
|
||||
//The first piece in plan order is the target, as the cable
|
||||
//list chooses it
|
||||
Element *target_{nullptr};
|
||||
for (Element *element_ : std::as_const(landed_)) {
|
||||
if (!target_ || comparPos(element_, target_)) {
|
||||
target_ = element_;
|
||||
}
|
||||
}
|
||||
|
||||
const DiagramContext informations_{target_->elementInformations()};
|
||||
const auto installation_{informations_.value(QETInformation::ELMT_PLANT).toString()};
|
||||
const auto location_{informations_.value(QETInformation::ELMT_LOCATION).toString()};
|
||||
const auto label_{informations_.value(QETInformation::ELMT_LABEL).toString()};
|
||||
|
||||
QVector<QString> lines_;
|
||||
if (!installation_.isEmpty()) {
|
||||
lines_ << installation_;
|
||||
}
|
||||
if (!location_.isEmpty()) {
|
||||
lines_ << location_;
|
||||
}
|
||||
if (!label_.isEmpty()) {
|
||||
lines_ << label_;
|
||||
}
|
||||
return lines_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::color
|
||||
* @return The color set by the user for this terminal, drawn in the
|
||||
* terminal strip. An invalid color mean that the user did not set a color.
|
||||
* The value is stored in the informations of the element.
|
||||
* @sa stripeColor
|
||||
*/
|
||||
QColor RealTerminal::color() const
|
||||
{
|
||||
QColor main_, stripe_;
|
||||
if (hasData()) {
|
||||
TerminalColor::parse(terminalData().m_informations.value(colorInfoKey()).toString(),
|
||||
&main_, &stripe_);
|
||||
}
|
||||
return main_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::stripeColor
|
||||
* @return The second color of a two-tone color (green/yellow for example)
|
||||
* or an invalid color if the color of this terminal is plain or not set.
|
||||
*/
|
||||
QColor RealTerminal::stripeColor() const
|
||||
{
|
||||
QColor main_, stripe_;
|
||||
if (hasData()) {
|
||||
TerminalColor::parse(terminalData().m_informations.value(colorInfoKey()).toString(),
|
||||
&main_, &stripe_);
|
||||
}
|
||||
return stripe_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::section
|
||||
* @return The section in mm2 set by the user for this terminal
|
||||
* or 0 if no section was set.
|
||||
* The value is stored in the informations of the element,
|
||||
* with the dot as decimal separator, whatever the locale.
|
||||
*/
|
||||
qreal RealTerminal::section() const
|
||||
{
|
||||
if (hasData())
|
||||
{
|
||||
bool ok_ = false;
|
||||
const auto section_ = terminalData().m_informations.value(sectionInfoKey()).toString().toDouble(&ok_);
|
||||
if (ok_ && section_ > 0) {
|
||||
return section_;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::widthMm
|
||||
* @return The width in mm of this terminal (the width of the terminal
|
||||
* sold by the manufacturer), set by the user, or 0 if no width was set.
|
||||
* The value is stored in the informations of the element,
|
||||
* with the dot as decimal separator, whatever the locale.
|
||||
*/
|
||||
qreal RealTerminal::widthMm() const
|
||||
{
|
||||
if (hasData())
|
||||
{
|
||||
bool ok_ = false;
|
||||
const auto width_ = terminalData().m_informations.value(widthInfoKey()).toString().toDouble(&ok_);
|
||||
if (ok_ && width_ > 0) {
|
||||
return width_;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::catalogEntry
|
||||
* @return the label of the terminal chosen in the catalog
|
||||
* (TerminalCatalog::WidthEntry::label), for example "Phoenix Contact ST 4",
|
||||
* or an empty string if the width was typed or not set.
|
||||
* Several terminals of the catalog can have the same width,
|
||||
* the width alone doesn't say which one was chosen.
|
||||
*/
|
||||
QString RealTerminal::catalogEntry() const
|
||||
{
|
||||
if (hasData()) {
|
||||
return terminalData().m_informations.value(catalogEntryInfoKey()).toString();
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::applyCatalogEntry
|
||||
* Store in @a informations the terminal chosen in the catalog,
|
||||
* and write its manufacturer and its reference in the manufacturer and
|
||||
* manufacturer reference informations (used by the bill of materials).
|
||||
* These two informations are only written when they are empty,
|
||||
* or when they still hold what the previous entry of the catalog wrote :
|
||||
* a value typed by the user is never overwritten.
|
||||
* @param informations : the informations of the element
|
||||
* @param entry_label : label of an entry of the catalog,
|
||||
* an empty string (or a label unknown by the catalog) removes the entry.
|
||||
*/
|
||||
void RealTerminal::applyCatalogEntry(DiagramContext &informations, const QString &entry_label)
|
||||
{
|
||||
const auto *old_entry = TerminalCatalog::entryByLabel(
|
||||
informations.value(catalogEntryInfoKey()).toString());
|
||||
const auto *new_entry = TerminalCatalog::entryByLabel(entry_label);
|
||||
|
||||
if (new_entry) {
|
||||
informations.addValue(catalogEntryInfoKey(), QString::fromUtf8(new_entry->label));
|
||||
} else if (informations.contains(catalogEntryInfoKey())) {
|
||||
informations.remove(catalogEntryInfoKey());
|
||||
}
|
||||
|
||||
const auto update_ = [&informations](const QString &key, const QString &old_value, const QString &new_value)
|
||||
{
|
||||
const auto current_ = informations.value(key).toString();
|
||||
if (!current_.isEmpty() && current_ != old_value) {
|
||||
return; //Typed by the user, keep it
|
||||
}
|
||||
if (!new_value.isEmpty()) {
|
||||
informations.addValue(key, new_value);
|
||||
} else if (!current_.isEmpty()) {
|
||||
informations.addValue(key, QString());
|
||||
}
|
||||
};
|
||||
|
||||
update_(QETInformation::ELMT_MANUFACTURER,
|
||||
old_entry ? QString::fromUtf8(old_entry->manufacturer) : QString(),
|
||||
new_entry ? QString::fromUtf8(new_entry->manufacturer) : QString());
|
||||
update_(QETInformation::ELMT_MANUFACTURER_REF,
|
||||
old_entry ? TerminalCatalog::modelName(*old_entry) : QString(),
|
||||
new_entry ? TerminalCatalog::modelName(*new_entry) : QString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RealTerminal::hasSeparatorAfter
|
||||
* @return true if a separator plate (partition plate) is mounted
|
||||
* after this terminal, between this terminal and the next one of the strip.
|
||||
* Used to separate groups of terminals (voltages, functions...).
|
||||
*/
|
||||
bool RealTerminal::hasSeparatorAfter() const {
|
||||
if (hasData()) {
|
||||
return QET::infoFlagIsTrue(
|
||||
terminalData().m_informations.value(separatorInfoKey()).toString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -211,8 +765,8 @@ QString RealTerminal::conductor() const {
|
||||
* @return
|
||||
*/
|
||||
ElementData::TerminalType RealTerminal::type() const {
|
||||
if (m_element) {
|
||||
return m_element->elementData().terminalType();
|
||||
if (hasData()) {
|
||||
return terminalData().terminalType();
|
||||
} else {
|
||||
return ElementData::TTGeneric;
|
||||
}
|
||||
@@ -223,8 +777,8 @@ ElementData::TerminalType RealTerminal::type() const {
|
||||
* @return
|
||||
*/
|
||||
ElementData::TerminalFunction RealTerminal::function() const {
|
||||
if (m_element) {
|
||||
return m_element->elementData().terminalFunction();
|
||||
if (hasData()) {
|
||||
return terminalData().terminalFunction();
|
||||
} else {
|
||||
return ElementData::TFGeneric;
|
||||
}
|
||||
@@ -235,8 +789,8 @@ ElementData::TerminalFunction RealTerminal::function() const {
|
||||
* @return
|
||||
*/
|
||||
bool RealTerminal::isLed() const {
|
||||
if (m_element) {
|
||||
return m_element->elementData().terminalLed();
|
||||
if (hasData()) {
|
||||
return terminalData().terminalLed();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -293,6 +847,9 @@ Element *RealTerminal::element() const {
|
||||
* else return a null uuid.
|
||||
*/
|
||||
QUuid RealTerminal::elementUuid() const {
|
||||
if (m_spare) {
|
||||
return m_spare_uuid; //Used to find the terminal (bridges...) like an element uuid
|
||||
}
|
||||
if (!m_element.isNull()) {
|
||||
return m_element->uuid();
|
||||
} else {
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
#define REALTERMINAL_H
|
||||
|
||||
#include <QSharedPointer>
|
||||
#include <QColor>
|
||||
#include <QDomElement>
|
||||
#include <QUuid>
|
||||
#include "../properties/elementdata.h"
|
||||
|
||||
class TerminalStrip;
|
||||
@@ -53,8 +55,19 @@ class RealTerminal
|
||||
|
||||
void setPhysicalTerminal(const QSharedPointer<PhysicalTerminal> &phy_t);
|
||||
|
||||
///Where the wires of a cable land, worked out from the project:
|
||||
///empty when no cable lands on a piece worth naming (cableTarget()
|
||||
///then falls back on the text typed in by hand).
|
||||
QVector<QString> automaticCableTarget() const;
|
||||
|
||||
public:
|
||||
~RealTerminal();
|
||||
static QSharedPointer<RealTerminal> createSpare(const ElementData &data, const QUuid &uuid = QUuid());
|
||||
static QSharedPointer<RealTerminal> spareFromXml(const QDomElement &xml_element);
|
||||
bool isSpare() const;
|
||||
ElementData spareData() const;
|
||||
void setSpareData(const ElementData &data);
|
||||
ElementData terminalData() const;
|
||||
TerminalStrip *parentStrip() const noexcept;
|
||||
QSharedPointer<PhysicalTerminal> physicalTerminal() const noexcept;
|
||||
|
||||
@@ -65,12 +78,36 @@ class RealTerminal
|
||||
QString Xref() const;
|
||||
QString cable() const;
|
||||
QString cableWire() const;
|
||||
bool cableIsAutomatic() const;
|
||||
bool isShield() const;
|
||||
QVector<QString> cableTarget() const;
|
||||
///True when cableTarget() is worked out from the project (read only),
|
||||
///false when it is the text typed in by hand (which may be edited)
|
||||
bool cableTargetIsAutomatic() const;
|
||||
QColor color() const;
|
||||
QColor stripeColor() const;
|
||||
qreal section() const;
|
||||
qreal widthMm() const;
|
||||
QString catalogEntry() const;
|
||||
bool hasSeparatorAfter() const;
|
||||
|
||||
//Keys used to store the cable data in the element informations
|
||||
static QString cableInfoKey() { return QStringLiteral("terminal_cable"); }
|
||||
static QString cableWireInfoKey() { return QStringLiteral("terminal_cable_wire"); }
|
||||
//Target typed in by hand, only read when the cables give none
|
||||
static QString cableTargetInfoKey() { return QStringLiteral("terminal_cable_target"); }
|
||||
static QString shieldInfoKey() { return QStringLiteral("terminal_shield"); }
|
||||
//Keys used to store the color and the section (mm2) of the terminal
|
||||
static QString colorInfoKey() { return QStringLiteral("terminal_color"); }
|
||||
static QString sectionInfoKey() { return QStringLiteral("terminal_section"); }
|
||||
//Key used to store the width (mm) of the terminal, drawn in the terminal strip
|
||||
static QString widthInfoKey() { return QStringLiteral("terminal_width"); }
|
||||
//Key used to store the terminal chosen in the catalog (TerminalCatalog::WidthEntry::label)
|
||||
static QString catalogEntryInfoKey() { return QStringLiteral("terminal_catalog_entry"); }
|
||||
//Key used to store if a separator plate is mounted after the terminal
|
||||
static QString separatorInfoKey() { return QStringLiteral("terminal_separator"); }
|
||||
|
||||
static void applyCatalogEntry(DiagramContext &informations, const QString &entry_label);
|
||||
|
||||
QString conductor() const;
|
||||
|
||||
@@ -89,7 +126,13 @@ class RealTerminal
|
||||
static QString xmlTagName();
|
||||
|
||||
private :
|
||||
bool hasData() const;
|
||||
|
||||
QPointer<Element> m_element;
|
||||
//A spare terminal has no element : it is on no folio, its data are its own
|
||||
bool m_spare = false;
|
||||
QUuid m_spare_uuid;
|
||||
ElementData m_spare_data;
|
||||
QWeakPointer<RealTerminal> m_this_weak;
|
||||
QSharedPointer<PhysicalTerminal> m_physical_terminal;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
Copyright 2006-2026 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 TERMINALCATALOG_H
|
||||
#define TERMINALCATALOG_H
|
||||
|
||||
#include <QLocale>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
|
||||
/**
|
||||
* @brief The TerminalCatalog namespace
|
||||
* Data shared between the terminal strip editor and the element editor :
|
||||
* usual sections (mm2) and widths (mm) of the terminals sold by the manufacturers.
|
||||
* Only the values found in the catalogs of the manufacturers are listed,
|
||||
* any other value can be typed by the user.
|
||||
* For Legrand and ABB Entrelec the value is the pitch ("pas") of the catalog.
|
||||
*/
|
||||
namespace TerminalCatalog
|
||||
{
|
||||
/**
|
||||
* How the wire is clamped in the terminal.
|
||||
* Used by the realistic drawing of the terminal strip.
|
||||
*/
|
||||
enum class Clamp {
|
||||
Screw, ///< Screw clamp (Phoenix Contact UT, Weidmüller WDU...)
|
||||
Spring, ///< Spring-cage clamp, opened with a screwdriver (Phoenix Contact ST)
|
||||
PushIn ///< Push-in clamp (WAGO TOPJOB S)
|
||||
};
|
||||
|
||||
/**
|
||||
* Width (mm) of a terminal sold by a manufacturer.
|
||||
* For ABB Entrelec the value is the spacing (pitch) given by the catalog.
|
||||
*/
|
||||
struct WidthEntry { double mm; const char *label; double section; const char *manufacturer; Clamp clamp; };
|
||||
|
||||
inline const QVector<double> §ions()
|
||||
{
|
||||
static const QVector<double> sections_ {0.5, 0.75, 1, 1.5, 2.5, 4, 6, 10, 16, 25, 35, 50, 70, 95, 120, 150, 185, 240, 300};
|
||||
return sections_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief awgForSection
|
||||
* @param mm2 : a section in mm2
|
||||
* @return the nearest AWG / kcmil size, as text ("14", "1/0", "250 kcmil"),
|
||||
* or an empty string if the section has no usual equivalent.
|
||||
* The correspondence is approximate (the usual table of the manufacturers),
|
||||
* the two systems don't have the same steps.
|
||||
*/
|
||||
inline QString awgForSection(double mm2)
|
||||
{
|
||||
struct Awg { double mm2; const char *awg; };
|
||||
static const Awg table_[] {
|
||||
{0.5, "20"}, {0.75, "18"}, {1, "17"},
|
||||
{1.5, "16"}, {2.5, "14"}, {4, "12"},
|
||||
{6, "10"}, {10, "8"}, {16, "6"},
|
||||
{25, "4"}, {35, "2"}, {50, "1/0"},
|
||||
{70, "2/0"}, {95, "3/0"}, {120, "250 kcmil"},
|
||||
{150, "300 kcmil"}, {185, "350 kcmil"}, {240, "500 kcmil"},
|
||||
{300, "600 kcmil"}
|
||||
};
|
||||
for (const auto &entry : table_) {
|
||||
if (qAbs(entry.mm2 - mm2) < 0.0001) {
|
||||
return QString::fromLatin1(entry.awg);
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief sectionText
|
||||
* @param mm2 : a section in mm2
|
||||
* @param unit_text : the text of the section with its unit, for example "2,5 mm²"
|
||||
* @return @a unit_text followed by the AWG size if there is one : "2,5 mm² (AWG 14)"
|
||||
*/
|
||||
inline QString sectionText(double mm2, const QString &unit_text)
|
||||
{
|
||||
const auto awg_ = awgForSection(mm2);
|
||||
if (awg_.isEmpty()) {
|
||||
return unit_text;
|
||||
}
|
||||
return awg_.contains(QLatin1String("kcmil"))
|
||||
? unit_text + QStringLiteral(" (%1)").arg(awg_)
|
||||
: unit_text + QStringLiteral(" (AWG %1)").arg(awg_);
|
||||
}
|
||||
|
||||
inline const QVector<WidthEntry> &widthEntries()
|
||||
{
|
||||
static const QVector<WidthEntry> entries_ {
|
||||
{4.2, "Phoenix Contact ST 1,5", 1.5, "Phoenix Contact", Clamp::Spring},
|
||||
{5.0, "ABB Entrelec MA 2.5/5", 2.5, "ABB Entrelec", Clamp::Screw},
|
||||
{5.1, "Weidmüller WDU 2.5", 2.5, "Weidmüller", Clamp::Screw},
|
||||
{5.2, "WAGO 2002 (TOPJOB S)", 2.5, "WAGO", Clamp::PushIn},
|
||||
{5.2, "Phoenix Contact ST 2,5 / ST 2,5-QUATTRO", 2.5, "Phoenix Contact", Clamp::Spring},
|
||||
{6.0, "ABB Entrelec M 4/6", 4.0, "ABB Entrelec", Clamp::Screw},
|
||||
{6.0, "Legrand Viking 3 (4 mm²)", 4.0, "Legrand", Clamp::Screw},
|
||||
{6.1, "Weidmüller WDU 4", 4.0, "Weidmüller", Clamp::Screw},
|
||||
{6.2, "Phoenix Contact UT 4", 4.0, "Phoenix Contact", Clamp::Screw},
|
||||
{6.2, "Phoenix Contact ST 4", 4.0, "Phoenix Contact", Clamp::Spring},
|
||||
{7.5, "WAGO 2006 (TOPJOB S)", 6.0, "WAGO", Clamp::PushIn},
|
||||
{7.9, "Weidmüller WDU 6", 6.0, "Weidmüller", Clamp::Screw},
|
||||
{8.0, "ABB Entrelec M 6/8", 6.0, "ABB Entrelec", Clamp::Screw},
|
||||
{8.0, "ABB Entrelec M 4/8", 4.0, "ABB Entrelec", Clamp::Screw},
|
||||
{8.2, "Phoenix Contact UT 6", 6.0, "Phoenix Contact", Clamp::Screw},
|
||||
{10.0, "ABB Entrelec M 10/10", 10.0, "ABB Entrelec", Clamp::Screw},
|
||||
{10.2, "Phoenix Contact UT 10", 10.0, "Phoenix Contact", Clamp::Screw},
|
||||
{11.9, "Weidmüller WDU 16/ZA", 16.0, "Weidmüller", Clamp::Screw},
|
||||
{12.0, "ABB Entrelec M 16/12", 16.0, "ABB Entrelec", Clamp::Screw},
|
||||
{12.0, "Legrand Viking 3 (16 mm²)", 16.0, "Legrand", Clamp::Screw},
|
||||
{12.2, "Phoenix Contact UT 16", 16.0, "Phoenix Contact", Clamp::Screw},
|
||||
{16.0, "ABB Entrelec M 35/16", 35.0, "ABB Entrelec", Clamp::Screw},
|
||||
{16.0, "Phoenix Contact UT 35", 35.0, "Phoenix Contact", Clamp::Screw},
|
||||
{16.0, "Weidmüller WDU 35N", 35.0, "Weidmüller", Clamp::Screw},
|
||||
{26.0, "ABB Entrelec M 35/26.FF", 35.0, "ABB Entrelec", Clamp::Screw},
|
||||
{31.0, "ABB Entrelec M 70/31.AF", 70.0, "ABB Entrelec", Clamp::Screw},
|
||||
{35.0, "ABB Entrelec M 120/35.F", 120.0, "ABB Entrelec", Clamp::Screw},
|
||||
{42.0, "ABB Entrelec M 300/42.FF", 300.0, "ABB Entrelec", Clamp::Screw}
|
||||
};
|
||||
return entries_;
|
||||
}
|
||||
/// Manufacturers of the catalog, in the order of the first entry.
|
||||
inline QStringList manufacturers()
|
||||
{
|
||||
QStringList list_;
|
||||
for (const auto &entry : widthEntries()) {
|
||||
const auto name_ = QString::fromUtf8(entry.manufacturer);
|
||||
if (!list_.contains(name_)) {
|
||||
list_ << name_;
|
||||
}
|
||||
}
|
||||
return list_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief entryByLabel
|
||||
* @param label : the label of an entry, as stored in the element
|
||||
* informations (RealTerminal::catalogEntryInfoKey())
|
||||
* @return the entry of the catalog with this label, or nullptr
|
||||
*/
|
||||
inline const WidthEntry *entryByLabel(const QString &label)
|
||||
{
|
||||
if (label.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
for (const auto &entry : widthEntries()) {
|
||||
if (label == QString::fromUtf8(entry.label)) {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief modelName
|
||||
* @param entry
|
||||
* @return the label of @a entry without the name of its manufacturer :
|
||||
* "ST 4" for "Phoenix Contact ST 4"
|
||||
*/
|
||||
inline QString modelName(const WidthEntry &entry)
|
||||
{
|
||||
const auto label_ = QString::fromUtf8(entry.label);
|
||||
const auto manufacturer_ = QString::fromUtf8(entry.manufacturer);
|
||||
if (label_.startsWith(manufacturer_ + QLatin1Char(' '))) {
|
||||
return label_.mid(manufacturer_.size() + 1);
|
||||
}
|
||||
return label_;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // TERMINALCATALOG_H
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
Copyright 2006-2026 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 TERMINALCOLOR_H
|
||||
#define TERMINALCOLOR_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QCoreApplication>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
#include <QPolygonF>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
/**
|
||||
* Colors of the terminals of a terminal strip.
|
||||
*
|
||||
* A color is stored as a string : "#rrggbb" for a plain color
|
||||
* or "#rrggbb/#rrggbb" for a two-tone color (the second color is drawn
|
||||
* as stripes over the first one, like the green/yellow of the earth).
|
||||
* An empty string means "no color set".
|
||||
*/
|
||||
namespace TerminalColor
|
||||
{
|
||||
struct Entry
|
||||
{
|
||||
const char *english; //Source text of the translation
|
||||
const char *french; //Also accepted when typed or pasted
|
||||
const char *main;
|
||||
const char *stripe; //nullptr if the color is plain
|
||||
};
|
||||
|
||||
inline const QVector<Entry> &palette()
|
||||
{
|
||||
static const QVector<Entry> palette_ {
|
||||
{"Gray", "gris", "#9e9e9e", nullptr},
|
||||
{"Blue", "bleu", "#4a90d9", nullptr},
|
||||
{"Green/Yellow", "vert/jaune", "#ffd800", "#2e9e2e"},
|
||||
{"Brown", "marron", "#8b5a2b", nullptr},
|
||||
{"Black", "noir", "#303030", nullptr},
|
||||
{"Red", "rouge", "#d32f2f", nullptr},
|
||||
{"Orange", "orange", "#f57c00", nullptr},
|
||||
{"Yellow", "jaune", "#ffd800", nullptr},
|
||||
{"Green", "vert", "#2e9e2e", nullptr},
|
||||
{"Violet", "violet", "#7b3fa0", nullptr},
|
||||
{"White", "blanc", "#ffffff", nullptr}
|
||||
};
|
||||
return palette_;
|
||||
}
|
||||
|
||||
inline QString translatedName(const Entry &entry) {
|
||||
return QCoreApplication::translate("TerminalColor", entry.english);
|
||||
}
|
||||
|
||||
inline QString toSpec(const QColor &main, const QColor &stripe = QColor())
|
||||
{
|
||||
if (!main.isValid()) {
|
||||
return QString();
|
||||
}
|
||||
return stripe.isValid() ? main.name() + QLatin1Char('/') + stripe.name()
|
||||
: main.name();
|
||||
}
|
||||
|
||||
inline QString specOf(const Entry &entry) {
|
||||
return toSpec(QColor(QLatin1String(entry.main)),
|
||||
entry.stripe ? QColor(QLatin1String(entry.stripe)) : QColor());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief parse
|
||||
* Read a color from a text : a name of the palette (in english, in french
|
||||
* or translated), a color "#rrggbb" or "#rrggbb/#rrggbb".
|
||||
* An empty text is valid and gives invalid colors (no color).
|
||||
* @return true if the text was understood
|
||||
*/
|
||||
inline bool parse(const QString &text, QColor *main, QColor *stripe)
|
||||
{
|
||||
*main = QColor();
|
||||
*stripe = QColor();
|
||||
|
||||
const auto text_ = text.trimmed();
|
||||
if (text_.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const auto &entry : palette())
|
||||
{
|
||||
if (text_.compare(QLatin1String(entry.english), Qt::CaseInsensitive) == 0 ||
|
||||
text_.compare(QLatin1String(entry.french), Qt::CaseInsensitive) == 0 ||
|
||||
text_.compare(translatedName(entry), Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
*main = QColor(QLatin1String(entry.main));
|
||||
if (entry.stripe) {
|
||||
*stripe = QColor(QLatin1String(entry.stripe));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const auto parts_ = text_.split(QLatin1Char('/'));
|
||||
if (parts_.size() == 2)
|
||||
{
|
||||
const QColor c1(parts_.at(0).trimmed());
|
||||
const QColor c2(parts_.at(1).trimmed());
|
||||
if (c1.isValid() && c2.isValid()) {
|
||||
*main = c1;
|
||||
*stripe = c2;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const QColor color_(text_);
|
||||
if (color_.isValid()) {
|
||||
*main = color_;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The name to display for a color : the translated name
|
||||
* if the color is in the palette, else the color as text.
|
||||
*/
|
||||
inline QString displayName(const QColor &main, const QColor &stripe)
|
||||
{
|
||||
const auto spec_ = toSpec(main, stripe);
|
||||
if (spec_.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
for (const auto &entry : palette()) {
|
||||
if (specOf(entry) == spec_) {
|
||||
return translatedName(entry);
|
||||
}
|
||||
}
|
||||
return spec_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A little pixmap of the color (the stripe color
|
||||
* fills the upper right half for a two-tone color)
|
||||
*/
|
||||
inline QPixmap swatch(const QColor &main, const QColor &stripe, int size = 16)
|
||||
{
|
||||
QPixmap pixmap_(size, size);
|
||||
pixmap_.fill(Qt::transparent);
|
||||
if (!main.isValid()) {
|
||||
return pixmap_;
|
||||
}
|
||||
|
||||
QPainter painter_(&pixmap_);
|
||||
painter_.setRenderHint(QPainter::Antialiasing);
|
||||
painter_.setPen(Qt::NoPen);
|
||||
painter_.setBrush(main);
|
||||
painter_.drawRect(0, 0, size, size);
|
||||
if (stripe.isValid())
|
||||
{
|
||||
painter_.setBrush(stripe);
|
||||
QPolygonF polygon_;
|
||||
polygon_ << QPointF(0, 0) << QPointF(size, 0) << QPointF(size, size);
|
||||
painter_.drawPolygon(polygon_);
|
||||
}
|
||||
painter_.setPen(QPen(Qt::gray, 1));
|
||||
painter_.setBrush(Qt::NoBrush);
|
||||
painter_.drawRect(0, 0, size - 1, size - 1);
|
||||
return pixmap_;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // TERMINALCOLOR_H
|
||||
@@ -100,6 +100,17 @@ TerminalStripData TerminalStrip::data() const {
|
||||
*/
|
||||
void TerminalStrip::setData(const TerminalStripData &data) {
|
||||
m_data = data;
|
||||
emit dataChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStrip::terminalDataChanged
|
||||
* Called when the data of the element of one of the terminals of this strip
|
||||
* is changed (Element::setElementData(), Element::setElementInformations()) :
|
||||
* the items which draw this strip on the folios are redrawn.
|
||||
*/
|
||||
void TerminalStrip::terminalDataChanged() {
|
||||
emit dataChanged();
|
||||
}
|
||||
|
||||
bool TerminalStrip::addTerminal(QSharedPointer<RealTerminal> real_t)
|
||||
@@ -381,6 +392,60 @@ void TerminalStrip::removeTerminals(QVector<QSharedPointer<PhysicalTerminal> > p
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStrip::insertSpareTerminals
|
||||
* Insert in this strip, at position @a pos, one physical terminal
|
||||
* for each spare terminal of @a spare_terminals.
|
||||
* @param pos : position of the first inserted terminal,
|
||||
* out of range (-1...) to add the terminals at the end of the strip.
|
||||
* @param spare_terminals : spare terminals not owned by a strip
|
||||
* (RealTerminal::createSpare())
|
||||
* @return the inserted physical terminals
|
||||
*/
|
||||
QVector<QSharedPointer<PhysicalTerminal>> TerminalStrip::insertSpareTerminals(int pos, const QVector<QSharedPointer<RealTerminal>> &spare_terminals)
|
||||
{
|
||||
QVector<QSharedPointer<PhysicalTerminal>> phy_vector;
|
||||
for (const auto &real_t : spare_terminals)
|
||||
{
|
||||
if (real_t && real_t->isSpare() && !real_t->parentStrip()) {
|
||||
auto raw_phy_ptr = new PhysicalTerminal(nullptr, QVector<QSharedPointer<RealTerminal>>{real_t});
|
||||
phy_vector.append(raw_phy_ptr->sharedRef());
|
||||
}
|
||||
}
|
||||
insertTerminals(pos, phy_vector);
|
||||
return phy_vector;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStrip::insertTerminals
|
||||
* Insert @a phy_t_vector in this strip at position @a pos, in this order.
|
||||
* The physical terminals already owned by a strip are ignored.
|
||||
* @param pos : position of the first inserted terminal,
|
||||
* out of range (-1...) to add the terminals at the end of the strip.
|
||||
* @param phy_t_vector
|
||||
*/
|
||||
void TerminalStrip::insertTerminals(int pos, const QVector<QSharedPointer<PhysicalTerminal>> &phy_t_vector)
|
||||
{
|
||||
if (pos < 0 || pos > m_physical_terminals.size()) {
|
||||
pos = m_physical_terminals.size();
|
||||
}
|
||||
|
||||
bool added_{false};
|
||||
for (const auto &phy_t : phy_t_vector)
|
||||
{
|
||||
if (!phy_t || phy_t->terminalStrip()) {
|
||||
continue;
|
||||
}
|
||||
m_physical_terminals.insert(pos++, phy_t);
|
||||
phy_t->setParentStrip(this);
|
||||
added_ = true;
|
||||
}
|
||||
|
||||
if (added_) {
|
||||
emit orderChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStrip::pos
|
||||
* @param terminal
|
||||
@@ -1011,17 +1076,23 @@ bool TerminalStrip::fromXml(QDomElement &xml_element)
|
||||
auto xml_layout = xml_element.firstChildElement(QStringLiteral("layout"));
|
||||
if (!xml_layout.isNull())
|
||||
{
|
||||
//The folios holding its terminals are built (QET_LAZY_FOLIOS):
|
||||
//The folios holding its terminals are built (lazy folios):
|
||||
//a strip is read with the project, before the database could
|
||||
//tell where any symbol is
|
||||
QSet<QUuid> terminals;
|
||||
const QDomNodeList xml_reals = xml_layout.elementsByTagName(RealTerminal::xmlTagName());
|
||||
for (int i = 0 ; i < xml_reals.size() ; ++i)
|
||||
terminals.insert(QUuid(xml_reals.at(i).toElement().attribute(QStringLiteral("element_uuid"))));
|
||||
for (int i = 0 ; i < xml_reals.size() ; ++i) {
|
||||
//A spare terminal is on no folio
|
||||
const auto element_uuid_ = xml_reals.at(i).toElement().attribute(QStringLiteral("element_uuid"));
|
||||
if (!element_uuid_.isEmpty()) {
|
||||
terminals.insert(QUuid(element_uuid_));
|
||||
}
|
||||
}
|
||||
m_project->buildFoliosHoldingSymbols(terminals);
|
||||
|
||||
//Get all free elements terminal of the project
|
||||
const ElementProvider ep(m_project);
|
||||
//Get all free elements terminal of the project: its own are on
|
||||
//the folios just built
|
||||
const ElementProvider ep(m_project->builtFolios());
|
||||
auto free_terminals = ep.freeTerminal();
|
||||
|
||||
//Read each physical terminal
|
||||
@@ -1032,7 +1103,14 @@ bool TerminalStrip::fromXml(QDomElement &xml_element)
|
||||
//Read each real terminal of the current physical terminal of the loop
|
||||
for (auto &xml_real : QETXML::findInDomElement(xml_physical, RealTerminal::xmlTagName()))
|
||||
{
|
||||
//A spare terminal (on no folio) has its own data
|
||||
if (auto spare_ = RealTerminal::spareFromXml(xml_real)) {
|
||||
real_t_vector.append(spare_);
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto uuid_ = QUuid(xml_real.attribute(QStringLiteral("element_uuid")));
|
||||
bool found = false;
|
||||
for (auto terminal_elmt : std::as_const(free_terminals))
|
||||
{
|
||||
if (terminal_elmt->uuid() == uuid_)
|
||||
@@ -1041,9 +1119,15 @@ bool TerminalStrip::fromXml(QDomElement &xml_element)
|
||||
//Remove the actual terminal element from the vector, they dicrease the size
|
||||
//of the vector and so each iteration have less terminal element to check
|
||||
free_terminals.removeOne(terminal_elmt);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
qWarning() << "TerminalStrip::fromXml: strip" << name()
|
||||
<< "lists terminal" << uuid_.toString()
|
||||
<< "but no free terminal symbol has this uuid; it is left out";
|
||||
}
|
||||
}
|
||||
|
||||
if (!real_t_vector.isEmpty()) {
|
||||
|
||||
@@ -53,6 +53,9 @@ class TerminalStrip : public QObject
|
||||
void orderChanged(); //Emitted when the order of the physical terminal is changed
|
||||
void bridgeChanged();
|
||||
void bridgeColorChanged(QSharedPointer<TerminalStripBridge> bridge);
|
||||
//Emitted when the data of the strip (name...) or of one of its
|
||||
//terminals (width, color, type...) is changed : what is drawn changes
|
||||
void dataChanged();
|
||||
public:
|
||||
|
||||
TerminalStrip(QETProject *project);
|
||||
@@ -72,11 +75,14 @@ class TerminalStrip : public QObject
|
||||
QString comment() const {return m_data.m_comment;}
|
||||
void setDescription(const QString &description);
|
||||
QString description() const {return m_data.m_description;}
|
||||
///Manufacturer of the terminals proposed by the editor, empty = all
|
||||
QString manufacturer() const {return m_data.m_manufacturer;}
|
||||
QUuid uuid() const {return m_data.m_uuid;}
|
||||
QETProject *project() const;
|
||||
|
||||
TerminalStripData data() const;
|
||||
void setData(const TerminalStripData &data);
|
||||
void terminalDataChanged();
|
||||
|
||||
bool addTerminal (QSharedPointer<RealTerminal> real_t);
|
||||
void addTerminals (QVector<QSharedPointer<RealTerminal>> real_t_vector);
|
||||
@@ -89,6 +95,8 @@ class TerminalStrip : public QObject
|
||||
void addTerminals (QVector<QSharedPointer<PhysicalTerminal>> phy_t_vector);
|
||||
bool removeTerminal (QSharedPointer<PhysicalTerminal> phy_t);
|
||||
void removeTerminals (QVector<QSharedPointer<PhysicalTerminal>> phy_t_vector);
|
||||
QVector<QSharedPointer<PhysicalTerminal>> insertSpareTerminals(int pos, const QVector<QSharedPointer<RealTerminal>> &spare_terminals);
|
||||
void insertTerminals(int pos, const QVector<QSharedPointer<PhysicalTerminal>> &phy_t_vector);
|
||||
|
||||
int pos(const QSharedPointer<PhysicalTerminal> &terminal) const;
|
||||
int physicalTerminalCount() const;
|
||||
@@ -110,6 +118,10 @@ class TerminalStrip : public QObject
|
||||
void unBridge(const QVector<QSharedPointer<RealTerminal>> &real_terminals);
|
||||
bool canUnBridge(const QVector <QSharedPointer<RealTerminal>> &real_terminals) const;
|
||||
QSharedPointer<TerminalStripBridge> isBridged(const QSharedPointer<RealTerminal> real_terminal) const;
|
||||
///True when at least one bridge joins terminals of this strip :
|
||||
///the bridge cells of the terminal strip editor are the only thing
|
||||
///drawn from them (see TerminalStripModel::bridgePixmapFor)
|
||||
bool hasBridges() const { return !m_bridge.isEmpty(); }
|
||||
QSharedPointer<TerminalStripBridge> bridgeFor (const QVector<QSharedPointer<RealTerminal>> &terminal_vector) const;
|
||||
|
||||
QSharedPointer<RealTerminal> previousTerminalInLevel(const QSharedPointer<RealTerminal> &real_terminal) const;
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
Copyright 2006-2026 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 "terminalstripautoimport.h"
|
||||
|
||||
#include "../elementprovider.h"
|
||||
#include "../qetgraphicsitem/terminalelement.h"
|
||||
#include "../qetinformation.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../utils/qetutils.h"
|
||||
#include "UndoCommand/addterminalstripcommand.h"
|
||||
#include "UndoCommand/addterminaltostripcommand.h"
|
||||
#include "UndoCommand/sortterminalstripcommand.h"
|
||||
#include "physicalterminal.h"
|
||||
#include "realterminal.h"
|
||||
#include "terminalstrip.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QMap>
|
||||
#include <QUndoStack>
|
||||
|
||||
namespace
|
||||
{
|
||||
/**
|
||||
* Terminals going to one strip : a label prefix plus the plant and
|
||||
* the location typed on the terminals. A plant or location set on
|
||||
* the terminals must match the strip exactly, an empty one accepts
|
||||
* every strip of the same name (in most projects the plant and the
|
||||
* location live on the strip, not on the terminals).
|
||||
*/
|
||||
struct StripGroup
|
||||
{
|
||||
QVector<QSharedPointer<RealTerminal>> free_terminals;
|
||||
QVector<QPair<QSharedPointer<RealTerminal>, TerminalStrip *>> placed_terminals;
|
||||
QString name, plant, location;
|
||||
};
|
||||
|
||||
QString elementInfo(const QSharedPointer<RealTerminal> &terminal, const QString &key)
|
||||
{
|
||||
auto element_ = terminal->element();
|
||||
if (!element_) {
|
||||
return QString();
|
||||
}
|
||||
return element_->elementData().m_informations.value(key).toString().trimmed();
|
||||
}
|
||||
|
||||
//An empty plant is saved and read back as "=" and an empty
|
||||
//location as "+" (the placeholders of the title block) : all
|
||||
//three spellings mean "no plant" / "no location"
|
||||
QString normalizedPlant(const QString &plant_)
|
||||
{
|
||||
const auto value_ = plant_.trimmed();
|
||||
return (value_.isEmpty() || value_ == QLatin1String("="))
|
||||
? QString() : value_;
|
||||
}
|
||||
|
||||
QString normalizedLocation(const QString &location_)
|
||||
{
|
||||
const auto value_ = location_.trimmed();
|
||||
return (value_.isEmpty() || value_ == QLatin1String("+"))
|
||||
? QString() : value_;
|
||||
}
|
||||
|
||||
QString groupKey(const QString &name, const QString &plant, const QString &location) {
|
||||
return name + QChar(0x1f) + plant + QChar(0x1f) + location;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief stripOf
|
||||
* The strip of the group : same name, and same plant and location
|
||||
* when the terminals carry some. Empty plant and location on the
|
||||
* terminals mean "no opinion" : any strip of the name is reused,
|
||||
* the one already holding terminals winning over an empty one.
|
||||
* @return the strip to work on, null when none has to exist yet.
|
||||
*/
|
||||
TerminalStrip *stripOf(QETProject *project, const QString &name,
|
||||
const QString &plant, const QString &location)
|
||||
{
|
||||
const bool strict_ = !plant.isEmpty() || !location.isEmpty();
|
||||
TerminalStrip *best_ = nullptr;
|
||||
int best_score_ = -1;
|
||||
for (auto strip_ : project->terminalStrip())
|
||||
{
|
||||
if (!strip_ || strip_->name() != name) {
|
||||
continue;
|
||||
}
|
||||
const bool same_ =
|
||||
normalizedPlant(strip_->installation()) == plant &&
|
||||
normalizedLocation(strip_->location()) == location;
|
||||
if (strict_ && !same_) {
|
||||
continue;
|
||||
}
|
||||
//Same plant and location first, then the strip already
|
||||
//holding terminals over an empty one
|
||||
const int score_ = (same_ ? 4 : 0)
|
||||
+ (strip_->realTerminals().isEmpty() ? 0 : 1);
|
||||
if (score_ > best_score_) {
|
||||
best_ = strip_;
|
||||
best_score_ = score_;
|
||||
}
|
||||
}
|
||||
return best_;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripAutoImport::importFreeTerminals
|
||||
* Put the terminals of @a project in the strip named after their label
|
||||
* prefix ("X1:2" goes to the strip "X1"), creating the strip when it
|
||||
* does not exist yet. The whole run is one undoable step.
|
||||
*
|
||||
* @a move_placed picks the action : false only places free terminals,
|
||||
* placed ones are never touched. True also moves a placed terminal
|
||||
* whose label prefix or plant and location no longer match its strip.
|
||||
*
|
||||
* In both cases the order the user gave to a strip is never changed,
|
||||
* only a strip created by the run itself is sorted -- the way the plan
|
||||
* is drawn : folio by folio, then left to right (see
|
||||
* SortTerminalStripCommand::planOrderLessThan).
|
||||
*/
|
||||
TerminalStripAutoImport::ImportResult TerminalStripAutoImport::importFreeTerminals(
|
||||
QETProject *project, bool move_placed)
|
||||
{
|
||||
if (!project) {
|
||||
return ImportResult{};
|
||||
}
|
||||
|
||||
//Group every real terminal of the project, the free ones and
|
||||
//the ones already in a strip alike
|
||||
QMap<QString, StripGroup> groups_;
|
||||
const auto collect_ = [&groups_](const QSharedPointer<RealTerminal> &real_,
|
||||
TerminalStrip *current_)
|
||||
{
|
||||
if (!real_ || !real_->element()) {
|
||||
return; //Spare terminals (no element) are left alone
|
||||
}
|
||||
const auto name_ = stripNameForLabel(real_->label());
|
||||
if (name_.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto plant_ = normalizedPlant(
|
||||
elementInfo(real_, QETInformation::ELMT_PLANT));
|
||||
const auto location_ = normalizedLocation(
|
||||
elementInfo(real_, QETInformation::ELMT_LOCATION));
|
||||
|
||||
auto &group_ = groups_[groupKey(name_, plant_, location_)];
|
||||
group_.name = name_;
|
||||
group_.plant = plant_;
|
||||
group_.location = location_;
|
||||
if (current_) {
|
||||
group_.placed_terminals.append({real_, current_});
|
||||
} else {
|
||||
group_.free_terminals.append(real_);
|
||||
}
|
||||
};
|
||||
|
||||
const ElementProvider provider(project);
|
||||
for (auto terminal_ : provider.freeTerminal()) {
|
||||
if (terminal_) {
|
||||
collect_(terminal_->realTerminal(), nullptr);
|
||||
}
|
||||
}
|
||||
for (auto strip_ : project->terminalStrip())
|
||||
{
|
||||
if (!strip_) {
|
||||
continue;
|
||||
}
|
||||
for (const auto &real_ : strip_->realTerminals()) {
|
||||
collect_(real_, strip_);
|
||||
}
|
||||
}
|
||||
if (groups_.isEmpty()) {
|
||||
return ImportResult{};
|
||||
}
|
||||
|
||||
for (auto it_ = groups_.begin(); it_ != groups_.end(); ++it_)
|
||||
{
|
||||
//The plan order : folio by folio, then left to right, so the
|
||||
//strip comes out the way the plan is drawn instead of label
|
||||
//by label ("N" and "PE" stay where they are on the sheet).
|
||||
//SortTerminalStripCommand, pushed below for a strip this run
|
||||
//creates, sorts with the same rule and cannot undo it.
|
||||
auto by_plan_ = [](const QSharedPointer<RealTerminal> &a_,
|
||||
const QSharedPointer<RealTerminal> &b_) {
|
||||
return SortTerminalStripCommand::planOrderLessThan(a_, b_);
|
||||
};
|
||||
std::sort(it_.value().free_terminals.begin(),
|
||||
it_.value().free_terminals.end(), by_plan_);
|
||||
std::sort(it_.value().placed_terminals.begin(),
|
||||
it_.value().placed_terminals.end(),
|
||||
[&by_plan_](const auto &a_, const auto &b_) {
|
||||
return by_plan_(a_.first, b_.first);
|
||||
});
|
||||
}
|
||||
|
||||
auto names_ = groups_.keys();
|
||||
std::sort(names_.begin(), names_.end(), QETUtils::naturalLessThan);
|
||||
|
||||
//First phase : work out what has to change, without touching the
|
||||
//project. When there is nothing to do, nothing is pushed : no
|
||||
//undo step is recorded and the project stays untouched.
|
||||
struct Plan
|
||||
{
|
||||
const StripGroup *group{nullptr};
|
||||
TerminalStrip *strip{nullptr}; //Target strip, null : to create
|
||||
};
|
||||
QVector<Plan> plans;
|
||||
|
||||
for (const auto &key_ : std::as_const(names_))
|
||||
{
|
||||
auto &group_ = groups_[key_];
|
||||
Plan plan_;
|
||||
plan_.group = &group_;
|
||||
plan_.strip = stripOf(project, group_.name, group_.plant, group_.location);
|
||||
|
||||
//Free terminals always, placed ones only when the action
|
||||
//is allowed to re-group them
|
||||
bool changed_ = !group_.free_terminals.isEmpty();
|
||||
if (move_placed) {
|
||||
for (const auto &entry_ : std::as_const(group_.placed_terminals)) {
|
||||
changed_ = changed_ || entry_.second != plan_.strip;
|
||||
}
|
||||
}
|
||||
if (changed_) {
|
||||
plans.append(plan_);
|
||||
}
|
||||
}
|
||||
if (plans.isEmpty()) {
|
||||
return ImportResult{};
|
||||
}
|
||||
|
||||
//Second phase : the changes, as one undoable step
|
||||
int added_ = 0, moved_ = 0, created_strips_ = 0;
|
||||
auto undo_ = project->undoStack();
|
||||
undo_->beginMacro(move_placed
|
||||
? QCoreApplication::translate("TerminalStripAutoImport",
|
||||
"Re-group all terminals")
|
||||
: QCoreApplication::translate("TerminalStripAutoImport",
|
||||
"Assign free terminals"));
|
||||
|
||||
for (const auto &plan_ : std::as_const(plans))
|
||||
{
|
||||
const auto &group_ = *plan_.group;
|
||||
|
||||
bool created_ = false;
|
||||
auto strip_ = plan_.strip;
|
||||
if (!strip_) {
|
||||
strip_ = project->newTerminalStrip(
|
||||
group_.plant, group_.location, group_.name);
|
||||
undo_->push(new AddTerminalStripCommand(strip_, project));
|
||||
++created_strips_;
|
||||
created_ = true;
|
||||
}
|
||||
|
||||
if (!group_.free_terminals.isEmpty()) {
|
||||
undo_->push(new AddTerminalToStripCommand(group_.free_terminals, strip_));
|
||||
added_ += group_.free_terminals.size();
|
||||
}
|
||||
//A terminal whose label prefix or plant and location no
|
||||
//longer match its strip moves to the right one, but only
|
||||
//when the action is allowed to re-group placed terminals
|
||||
if (move_placed)
|
||||
{
|
||||
for (const auto &entry_ : std::as_const(group_.placed_terminals))
|
||||
{
|
||||
if (entry_.second == strip_) {
|
||||
continue;
|
||||
}
|
||||
undo_->push(new MoveTerminalCommand(
|
||||
entry_.first->physicalTerminal(), entry_.second, strip_));
|
||||
++moved_;
|
||||
}
|
||||
}
|
||||
//A strip just created is sorted, a strip already known
|
||||
//keeps the order its user gave it
|
||||
if (created_) {
|
||||
undo_->push(new SortTerminalStripCommand(strip_));
|
||||
}
|
||||
}
|
||||
|
||||
undo_->endMacro();
|
||||
ImportResult result_;
|
||||
result_.terminals = added_ + moved_;
|
||||
result_.strips = created_strips_;
|
||||
return result_;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
Copyright 2006-2026 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 TERMINALSTRIPAUTOIMPORT_H
|
||||
#define TERMINALSTRIPAUTOIMPORT_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
class QETProject;
|
||||
|
||||
/**
|
||||
* The TerminalStripAutoImport namespace works out the terminal strips of a
|
||||
* project from the labels of its free terminals, so that the user does not
|
||||
* have to create the strips and move the terminals one by one.
|
||||
*
|
||||
* The rule : the name of a strip is the label of its terminals up to the
|
||||
* first colon, "X1:2" giving the strip "X1", as numbered by the terminal
|
||||
* numbering dialog. Terminals whose label holds no colon use the whole
|
||||
* label, terminals with no label are left alone.
|
||||
*
|
||||
* The plant and the location work as follows : they are read from the
|
||||
* terminal and only decide which strip to pick when they are set. An
|
||||
* existing strip with the same name is reused as soon as they are empty
|
||||
* (in most projects the plant and the location live on the strip, not on
|
||||
* the terminals), while a set plant or location must match exactly, a
|
||||
* strip being created when no one matches. That way the import never
|
||||
* builds a second strip for a name that already exists.
|
||||
*
|
||||
* Two actions run this import :
|
||||
* - the default one places free terminals only, it never moves or
|
||||
* re-sorts a terminal the user already assigned to a strip;
|
||||
* - "re-group all terminals" also moves a placed terminal whose label
|
||||
* prefix or plant and location no longer match its strip, for the case
|
||||
* where the user corrects a terminal afterwards. Existing strips are
|
||||
* never re-sorted.
|
||||
*
|
||||
* The import is idempotent : a second run adds nothing, the terminals
|
||||
* of a strip the user ordered or completed by hand are never re-ordered,
|
||||
* only new terminals are added (at the end of the strip). Only a strip
|
||||
* created by the import itself is sorted. When nothing has to change,
|
||||
* nothing is pushed to the undo stack : the project stays untouched.
|
||||
*/
|
||||
namespace TerminalStripAutoImport
|
||||
{
|
||||
struct ImportResult
|
||||
{
|
||||
int terminals = 0; ///< Terminals added to a strip, moved ones included
|
||||
int strips = 0; ///< Terminal strips created
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief stripNameForLabel
|
||||
* @param label : the label of a terminal, for example "X1:2"
|
||||
* @return the name of the strip holding this terminal, "X1", or an
|
||||
* empty string when the label holds nothing usable.
|
||||
*/
|
||||
inline QString stripNameForLabel(const QString &label)
|
||||
{
|
||||
const auto label_ = label.trimmed();
|
||||
if (label_.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
const auto colon_ = label_.indexOf(QLatin1Char(':'));
|
||||
if (colon_ > 0) {
|
||||
return label_.left(colon_).trimmed();
|
||||
}
|
||||
if (colon_ == 0) {
|
||||
return QString(); //Label starts with ":" : no name before it
|
||||
}
|
||||
return label_; //No colon : the whole label names the strip
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief importFreeTerminals
|
||||
* Add every free terminal of @a project to the terminal strip named
|
||||
* after its label prefix, creating the strip when it does not exist
|
||||
* yet. Everything happens on the undo stack, as one undoable step.
|
||||
* @param move_placed : false (default, the "assign free terminals"
|
||||
* action) only places free terminals, placed ones are never touched.
|
||||
* True (the "re-group all terminals" action) also moves a placed
|
||||
* terminal whose label prefix or plant and location no longer match
|
||||
* its strip. Existing strips are never re-sorted.
|
||||
* @return What has been done. Empty : nothing was pushed, the
|
||||
* project is untouched.
|
||||
*/
|
||||
ImportResult importFreeTerminals(QETProject *project, bool move_placed = false);
|
||||
}
|
||||
|
||||
#endif // TERMINALSTRIPAUTOIMPORT_H
|
||||
@@ -52,6 +52,9 @@ QDomElement TerminalStripData::toXml(QDomDocument &xml_document) const
|
||||
if (!m_description.isEmpty()) {
|
||||
info_elmt.appendChild(infoToXml(xml_document, QStringLiteral("description"), m_description));
|
||||
}
|
||||
if (!m_manufacturer.isEmpty()) {
|
||||
info_elmt.appendChild(infoToXml(xml_document, QStringLiteral("manufacturer"), m_manufacturer));
|
||||
}
|
||||
|
||||
return root_elmt;
|
||||
}
|
||||
@@ -79,6 +82,7 @@ bool TerminalStripData::fromXml(const QDomElement &xml_element)
|
||||
else if (name == QLatin1String("name")) {m_name = value;}
|
||||
else if (name == QLatin1String("comment")) {m_comment = value;}
|
||||
else if (name == QLatin1String("description")) {m_description = value;}
|
||||
else if (name == QLatin1String("manufacturer")) {m_manufacturer = value;}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -91,6 +95,7 @@ TerminalStripData &TerminalStripData::operator=(const TerminalStripData &other)
|
||||
m_name = other.m_name;
|
||||
m_comment = other.m_comment;
|
||||
m_description = other.m_description;
|
||||
m_manufacturer = other.m_manufacturer;
|
||||
m_uuid = other.m_uuid;
|
||||
|
||||
return *this;
|
||||
|
||||
@@ -44,11 +44,12 @@ class TerminalStripData : public PropertiesInterface
|
||||
private :
|
||||
static QDomElement infoToXml(QDomDocument &xml_doc, const QString &name, const QString &value);
|
||||
|
||||
QString m_installation = QStringLiteral("="),
|
||||
m_location = QStringLiteral("+"),
|
||||
QString m_installation,
|
||||
m_location,
|
||||
m_name,
|
||||
m_comment,
|
||||
m_description;
|
||||
m_description,
|
||||
m_manufacturer; //Manufacturer of the terminals proposed in the editor, empty = all
|
||||
QUuid m_uuid = QUuid::createUuid();
|
||||
|
||||
};
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
#include "../../diagram.h"
|
||||
|
||||
#include "../../qet.h"
|
||||
|
||||
#include <algorithm>
|
||||
void AddTerminalStripItemDialog::openDialog(Diagram *diagram, QWidget *parent)
|
||||
{
|
||||
AddTerminalStripItemDialog d(diagram->project(), parent);
|
||||
@@ -33,6 +35,7 @@ void AddTerminalStripItemDialog::openDialog(Diagram *diagram, QWidget *parent)
|
||||
if (strip_)
|
||||
{
|
||||
auto item_ = new TerminalStripItem(strip_);
|
||||
item_->setDrawStyle(static_cast<TerminalStripLayoutPattern::DrawStyle>(d.selectedDrawStyle()));
|
||||
diagram->addItem(item_);
|
||||
item_->setPos(50, 50);
|
||||
|
||||
@@ -77,6 +80,16 @@ TerminalStrip *AddTerminalStripItemDialog::selectedTerminalStrip() const
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief AddTerminalStripItemDialog::selectedDrawStyle
|
||||
* @return The drawing style chosen for the new item,
|
||||
* as an int value of TerminalStripLayoutPattern::DrawStyle.
|
||||
* The index of the combo box is the value of the enum.
|
||||
*/
|
||||
int AddTerminalStripItemDialog::selectedDrawStyle() const {
|
||||
return std::clamp(ui->m_draw_style_cb->currentIndex(), 0, 2);
|
||||
}
|
||||
|
||||
void AddTerminalStripItemDialog::fillComboBox()
|
||||
{
|
||||
if (m_project)
|
||||
|
||||
@@ -41,6 +41,7 @@ class AddTerminalStripItemDialog : public QDialog
|
||||
explicit AddTerminalStripItemDialog(QETProject *project, QWidget *parent = nullptr);
|
||||
~AddTerminalStripItemDialog();
|
||||
TerminalStrip *selectedTerminalStrip() const;
|
||||
int selectedDrawStyle() const;
|
||||
void fillComboBox();
|
||||
|
||||
private:
|
||||
|
||||
@@ -24,6 +24,35 @@
|
||||
<item>
|
||||
<widget class="QComboBox" name="m_terminal_strip_cb"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_draw_style">
|
||||
<property name="text">
|
||||
<string>Drawing style :</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="m_draw_style_cb">
|
||||
<property name="toolTip">
|
||||
<string>How the terminals are drawn. The realistic styles draw the strip at scale, seen from above, with the real width of each terminal.</string>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Schematic</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Realistic (push-in)</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Realistic (screw)</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
|
||||
@@ -18,12 +18,34 @@
|
||||
#ifndef MODELTERMINALDATA_H
|
||||
#define MODELTERMINALDATA_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include "../../qetgraphicsitem/element.h"
|
||||
#include "../realterminal.h"
|
||||
|
||||
struct modelRealTerminalData
|
||||
{
|
||||
///The Installation, Location and Label (BMK) of the piece the cable
|
||||
///lands on, one line by field : what the strip draws in the box
|
||||
///under its cable (RealTerminal::cableTarget) and what the Target
|
||||
///cell shows, both read line by line. A "/" written inside a field
|
||||
///is part of that field, never a separator.
|
||||
///A target typed in by hand is kept as it was written, and nothing
|
||||
///is shown when the cable lands on nothing worth naming and no text
|
||||
///was ever written in.
|
||||
static QString targetText(const QVector<QString> &target_)
|
||||
{
|
||||
QStringList fields_;
|
||||
for (const QString &field_ : target_)
|
||||
{
|
||||
if (!field_.isEmpty()) {
|
||||
fields_ << field_;
|
||||
}
|
||||
}
|
||||
return fields_.join(QLatin1Char('\n'));
|
||||
}
|
||||
|
||||
static modelRealTerminalData data(const QSharedPointer<RealTerminal> &real_t)
|
||||
{
|
||||
modelRealTerminalData mrtd;
|
||||
@@ -34,14 +56,24 @@ struct modelRealTerminalData
|
||||
mrtd.Xref_ = real_t->Xref();
|
||||
mrtd.cable_ = real_t->cable();
|
||||
mrtd.cable_wire = real_t->cableWire();
|
||||
mrtd.cable_target_ = targetText(real_t->cableTarget());
|
||||
mrtd.cable_target_auto_ = real_t->cableTargetIsAutomatic();
|
||||
mrtd.cable_auto_ = real_t->cableIsAutomatic();
|
||||
mrtd.conductor_ = real_t->conductor();
|
||||
mrtd.led_ = real_t->isLed();
|
||||
mrtd.shield_ = real_t->isShield();
|
||||
mrtd.separator_ = real_t->hasSeparatorAfter();
|
||||
mrtd.color_ = real_t->color();
|
||||
mrtd.stripe_color_ = real_t->stripeColor();
|
||||
mrtd.section_ = real_t->section();
|
||||
mrtd.width_mm_ = real_t->widthMm();
|
||||
mrtd.catalog_entry_ = real_t->catalogEntry();
|
||||
mrtd.type_ = real_t->type();
|
||||
mrtd.function_ = real_t->function();
|
||||
mrtd.element_ = real_t->element();
|
||||
mrtd.real_terminal = real_t.toWeakRef();
|
||||
mrtd.bridged_ = real_t->isBridged();
|
||||
mrtd.spare_ = real_t->isSpare();
|
||||
}
|
||||
|
||||
return mrtd;
|
||||
@@ -52,10 +84,23 @@ struct modelRealTerminalData
|
||||
QString Xref_;
|
||||
QString cable_;
|
||||
QString cable_wire;
|
||||
//Where the cable lands, past this strip : Installation / Location / Label
|
||||
QString cable_target_;
|
||||
//True when cable_target_ comes from the project (read only),
|
||||
//false when it is empty or was typed in by hand (editable)
|
||||
bool cable_target_auto_ = false;
|
||||
bool cable_auto_ = false; //True when cable_ and cable_wire come from the cables of the project
|
||||
QString conductor_;
|
||||
bool led_ = false;
|
||||
bool shield_ = false;
|
||||
bool separator_ = false; //A separator plate is mounted after the terminal
|
||||
QColor color_; //Invalid = no color set by the user
|
||||
QColor stripe_color_; //Second color of a two-tone color, invalid if plain
|
||||
qreal section_ = 0; //In mm2, 0 = no section set by the user
|
||||
qreal width_mm_ = 0; //In mm, 0 = no width set by the user
|
||||
QString catalog_entry_; //Terminal chosen in the catalog, empty if the width was typed
|
||||
bool bridged_ = false;
|
||||
bool spare_ = false; //Spare terminal, on no folio (RealTerminal::isSpare())
|
||||
|
||||
ElementData::TerminalType type_ = ElementData::TerminalType::TTGeneric;
|
||||
ElementData::TerminalFunction function_ = ElementData::TerminalFunction::TFGeneric;
|
||||
|
||||
@@ -31,10 +31,22 @@
|
||||
#include "../UndoCommand/changeterminalstripcolor.h"
|
||||
#include "../physicalterminal.h"
|
||||
#include "../terminalstripbridge.h"
|
||||
#include "../terminalcolor.h"
|
||||
#include "../terminalcatalog.h"
|
||||
#include "../realterminal.h"
|
||||
#include "../../qetinformation.h"
|
||||
#include "../../cable/cable.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QClipboard>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QKeyEvent>
|
||||
#include <QLineEdit>
|
||||
#include <QRegularExpression>
|
||||
#include <QSpinBox>
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
@@ -51,7 +63,18 @@ TerminalStripEditor::TerminalStripEditor(QETProject *project, QWidget *parent) :
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
ui->m_table_widget->setItemDelegate(new TerminalStripModelDelegate{this});
|
||||
auto delegate_ = new TerminalStripModelDelegate{this};
|
||||
ui->m_table_widget->setItemDelegate(delegate_);
|
||||
|
||||
//Manufacturer of the terminals proposed in the Width column
|
||||
ui->m_manufacturer_cb->addItem(tr("All manufacturers"), QString());
|
||||
for (const auto &name_ : TerminalCatalog::manufacturers()) {
|
||||
ui->m_manufacturer_cb->addItem(name_, name_);
|
||||
}
|
||||
connect(ui->m_manufacturer_cb, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, [this, delegate_]() {
|
||||
delegate_->setManufacturer(ui->m_manufacturer_cb->currentData().toString());
|
||||
});
|
||||
|
||||
//Copy, paste and delete of the cells with the keyboard
|
||||
ui->m_table_widget->installEventFilter(this);
|
||||
@@ -73,6 +96,10 @@ TerminalStripEditor::TerminalStripEditor(QETProject *project, QWidget *parent) :
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//The cable cells are worked out from the cables of the project,
|
||||
//so the table follows them while the user wires the drawing
|
||||
watchCableChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,9 +112,88 @@ TerminalStripEditor::~TerminalStripEditor() {
|
||||
void TerminalStripEditor::setProject(QETProject *project)
|
||||
{
|
||||
m_project = project;
|
||||
watchCableChanges();
|
||||
setCurrentStrip(nullptr);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripEditor::watchCableChanges
|
||||
* Follow the cables of the project: the cable cells are worked out from
|
||||
* them, so a cable renamed, given cores or taken away changes what the
|
||||
* cells say, and the window stays open while the user wires the drawing.
|
||||
* The project the editor followed before is let go first, since the
|
||||
* window may be shown on another project.
|
||||
*/
|
||||
void TerminalStripEditor::watchCableChanges()
|
||||
{
|
||||
if (m_watched_project) {
|
||||
//Only the connections made here go to the project
|
||||
m_watched_project->disconnect(nullptr, this);
|
||||
for (Cable *cable : m_watched_project->cables()) {
|
||||
cable->disconnect(nullptr, this);
|
||||
}
|
||||
}
|
||||
m_watched_project = m_project;
|
||||
if (!m_project) {
|
||||
return;
|
||||
}
|
||||
|
||||
connect(m_project, &QETProject::cableAdded, this, [this](Cable *cable) {
|
||||
watchCable(cable);
|
||||
});
|
||||
connect(m_project, &QETProject::cableRemoved, this, [this](Cable *cable) {
|
||||
//The project stopped feeding its labels already, so the
|
||||
//cells show what is left: the typed in value, or nothing
|
||||
if (cable) {
|
||||
cable->disconnect(nullptr, this);
|
||||
}
|
||||
reloadOnCableChange();
|
||||
});
|
||||
for (Cable *cable : m_project->cables()) {
|
||||
watchCable(cable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripEditor::watchCable
|
||||
* Reload the table when @a cable changes. The project runs its own
|
||||
* connection first (made when the cable entered it), so the labels of
|
||||
* the conductors are worked out again before this reload reads them.
|
||||
* @param cable
|
||||
*/
|
||||
void TerminalStripEditor::watchCable(Cable *cable)
|
||||
{
|
||||
if (!cable) {
|
||||
return;
|
||||
}
|
||||
cable->disconnect(nullptr, this);
|
||||
connect(cable, &Cable::changed, this, &TerminalStripEditor::reloadOnCableChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripEditor::reloadOnCableChange
|
||||
* Take up a change of the cables: reload so the cable cells say what
|
||||
* they now must, never at the price of the user's own work -- cell
|
||||
* edits not applied yet, or a cell being typed in, are kept and catch
|
||||
* up with the next reload of any kind.
|
||||
*/
|
||||
void TerminalStripEditor::reloadOnCableChange()
|
||||
{
|
||||
if (!m_current_strip || !m_model) {
|
||||
return;
|
||||
}
|
||||
if (!m_model->modifiedmodelRealTerminalData().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
//A cell being typed in keeps its editor: the reload would close
|
||||
//it under the fingers of the user (the edit itself is picked up
|
||||
//by the next reload once it has been made)
|
||||
if (ui->m_table_widget->isAncestorOf(QApplication::focusWidget())) {
|
||||
return;
|
||||
}
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripEditor::setCurrentStrip
|
||||
* Set the current terminal strip edited to \p strip_
|
||||
@@ -116,6 +222,7 @@ void TerminalStripEditor::setCurrentStrip(TerminalStrip *strip_)
|
||||
ui->m_name_le ->setText(strip_->name());
|
||||
ui->m_comment_le ->setText(strip_->comment());
|
||||
ui->m_description_te ->setPlainText(strip_->description());
|
||||
setManufacturerFilter(strip_->manufacturer());
|
||||
ui->m_move_to_cb->addItem(tr("Independent terminals"), QUuid());
|
||||
|
||||
const auto project_{strip_->project()};
|
||||
@@ -142,6 +249,18 @@ void TerminalStripEditor::setCurrentStrip(TerminalStrip *strip_)
|
||||
{
|
||||
m_model = new TerminalStripModel{strip_, this};
|
||||
ui->m_table_widget->setModel(m_model);
|
||||
//The Target column is worked out from the drawing: it takes
|
||||
//the width of its own text, never a cut off Installation
|
||||
ui->m_table_widget->horizontalHeader()->setSectionResizeMode(
|
||||
TerminalStripModel::CableTarget, QHeaderView::ResizeToContents);
|
||||
//That column holds one line by field: its rows must be tall
|
||||
//enough for as many lines as a cell shows, while a cell of
|
||||
//one line keeps the height the rows have always had
|
||||
auto v_header_ = ui->m_table_widget->verticalHeader();
|
||||
v_header_->setMinimumSectionSize(qMax(
|
||||
v_header_->defaultSectionSize(),
|
||||
ui->m_table_widget->rowHeight(0)));
|
||||
v_header_->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
m_model->buildBridgePixmap(setUpBridgeCellWidth());
|
||||
connect(ui->m_table_widget->selectionModel(), &QItemSelectionModel::selectionChanged, this, &TerminalStripEditor::selectionChanged);
|
||||
}
|
||||
@@ -188,12 +307,14 @@ void TerminalStripEditor::apply()
|
||||
{
|
||||
m_project->undoStack()->beginMacro(tr("Modify terminal strip properties"));
|
||||
|
||||
TerminalStripData data;
|
||||
//Start from the current data : keep the uuid of the strip
|
||||
TerminalStripData data = m_current_strip->data();
|
||||
data.m_installation = ui->m_installation_le->text();
|
||||
data.m_location = ui->m_location_le->text();
|
||||
data.m_name = ui->m_name_le->text();
|
||||
data.m_comment = ui->m_comment_le->text();
|
||||
data.m_description = ui->m_description_te->toPlainText();
|
||||
data.m_manufacturer = ui->m_manufacturer_cb->currentData().toString();
|
||||
|
||||
m_project->undoStack()->push(new ChangeTerminalStripData(m_current_strip, data, nullptr));
|
||||
|
||||
@@ -201,28 +322,59 @@ void TerminalStripEditor::apply()
|
||||
{
|
||||
for (const auto &data_ : m_model->modifiedmodelRealTerminalData())
|
||||
{
|
||||
if (auto element = data_.element_)
|
||||
{
|
||||
auto current_data = element->elementData();
|
||||
current_data.setTerminalType(data_.type_);
|
||||
current_data.setTerminalFunction(data_.function_);
|
||||
current_data.setTerminalLED(data_.led_);
|
||||
current_data.m_informations.addValue(QStringLiteral("label"), data_.label_);
|
||||
//The cable data are only stored when they are used
|
||||
const auto set_info = [¤t_data](const QString &key, const QString &value) {
|
||||
if (!value.isEmpty() || current_data.m_informations.contains(key)) {
|
||||
current_data.m_informations.addValue(key, value);
|
||||
}
|
||||
};
|
||||
const auto real_t = data_.real_terminal.toStrongRef();
|
||||
auto element = data_.element_;
|
||||
//A spare terminal (on no folio) has its own data, not the data of an element
|
||||
if (!real_t || (!element && !data_.spare_)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto old_data = element ? element->elementData() : real_t->spareData();
|
||||
auto current_data = old_data;
|
||||
current_data.setTerminalType(data_.type_);
|
||||
current_data.setTerminalFunction(data_.function_);
|
||||
current_data.setTerminalLED(data_.led_);
|
||||
current_data.m_informations.addValue(QStringLiteral("label"), data_.label_);
|
||||
//The cable data are only stored when they are used
|
||||
const auto set_info = [¤t_data](const QString &key, const QString &value) {
|
||||
if (!value.isEmpty() || current_data.m_informations.contains(key)) {
|
||||
current_data.m_informations.addValue(key, value);
|
||||
}
|
||||
};
|
||||
//A cable worked out from the cables of the project is
|
||||
//never written in the element: it would stay there,
|
||||
//out of date, once the cable is renamed or removed
|
||||
if (!data_.cable_auto_) {
|
||||
set_info(RealTerminal::cableInfoKey(), data_.cable_);
|
||||
set_info(RealTerminal::cableWireInfoKey(), data_.cable_wire);
|
||||
set_info(RealTerminal::shieldInfoKey(), data_.shield_ ? QStringLiteral("true") : QString());
|
||||
|
||||
if (element->elementData() != current_data)
|
||||
m_project->undoStack()->push(new ChangeElementDataCommand(element, current_data));
|
||||
if (data_.level_ != data_.real_terminal.toStrongRef()->level())
|
||||
m_project->undoStack()->push(new ChangeTerminalLevel(m_current_strip, data_.real_terminal, data_.level_));
|
||||
}
|
||||
//The same for the target: a target worked out from the
|
||||
//project is never written in the element, only the one
|
||||
//typed in by hand is stored
|
||||
if (!data_.cable_target_auto_) {
|
||||
set_info(RealTerminal::cableTargetInfoKey(), data_.cable_target_);
|
||||
}
|
||||
set_info(RealTerminal::shieldInfoKey(), data_.shield_ ? QStringLiteral("true") : QString());
|
||||
//The color and the section are stored with the dot as decimal separator, whatever the locale
|
||||
set_info(RealTerminal::colorInfoKey(), TerminalColor::toSpec(data_.color_, data_.stripe_color_));
|
||||
set_info(RealTerminal::sectionInfoKey(), data_.section_ > 0 ? QString::number(data_.section_, 'g', 6) : QString());
|
||||
set_info(RealTerminal::widthInfoKey(), data_.width_mm_ > 0 ? QString::number(data_.width_mm_, 'g', 6) : QString());
|
||||
//The terminal chosen in the catalog, its manufacturer and its reference
|
||||
//(only written when the user didn't type them)
|
||||
RealTerminal::applyCatalogEntry(current_data.m_informations,
|
||||
data_.width_mm_ > 0 ? data_.catalog_entry_ : QString());
|
||||
set_info(RealTerminal::separatorInfoKey(), data_.separator_ ? QStringLiteral("true") : QString());
|
||||
|
||||
if (old_data != current_data)
|
||||
{
|
||||
if (element) {
|
||||
m_project->undoStack()->push(new ChangeElementDataCommand(element, current_data));
|
||||
} else {
|
||||
m_project->undoStack()->push(new ChangeSpareTerminalDataCommand(real_t, current_data));
|
||||
}
|
||||
}
|
||||
if (data_.level_ != real_t->level())
|
||||
m_project->undoStack()->push(new ChangeTerminalLevel(m_current_strip, data_.real_terminal, data_.level_));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,6 +398,21 @@ void TerminalStripEditor::clear()
|
||||
m_model->deleteLater();
|
||||
m_model = nullptr;
|
||||
}
|
||||
setManufacturerFilter(QString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripEditor::setManufacturerFilter
|
||||
* Select @a manufacturer in the manufacturer combo box : only the terminals
|
||||
* of this manufacturer are proposed in the Width column.
|
||||
* Each terminal strip stores its own manufacturer (TerminalStripData),
|
||||
* an empty or unknown manufacturer proposes all the manufacturers.
|
||||
* @param manufacturer
|
||||
*/
|
||||
void TerminalStripEditor::setManufacturerFilter(const QString &manufacturer)
|
||||
{
|
||||
const auto index_ = ui->m_manufacturer_cb->findData(manufacturer);
|
||||
ui->m_manufacturer_cb->setCurrentIndex(index_ > 0 ? index_ : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -290,10 +457,12 @@ void TerminalStripEditor::selectionChanged()
|
||||
ui->m_bridge_terminals_pb ->setDisabled(true);
|
||||
ui->m_unbridge_terminals_pb->setDisabled(true);
|
||||
ui->m_bridge_color_cb ->setDisabled(true);
|
||||
ui->m_add_spare_pb ->setDisabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
ui->m_auto_ordering_pb->setEnabled(true);
|
||||
ui->m_add_spare_pb ->setEnabled(true);
|
||||
|
||||
const auto index_list = ui->m_table_widget->selectionModel()->selectedIndexes();
|
||||
|
||||
@@ -639,14 +808,19 @@ void TerminalStripEditor::on_m_led_cb_activated(int index)
|
||||
namespace {
|
||||
/**
|
||||
* @return true if the cell at @a index contain a free text
|
||||
* (label, cable or wire of the cable), so a text can be pasted in it.
|
||||
* (label, cable, wire of the cable, target of the cable, color, section
|
||||
* or width), so a text can be pasted in it.
|
||||
*/
|
||||
bool cellAcceptText(const QModelIndex &index)
|
||||
{
|
||||
const auto column_ = TerminalStripModel::columnTypeForIndex(index);
|
||||
return column_ == TerminalStripModel::Label
|
||||
|| column_ == TerminalStripModel::Cable
|
||||
|| column_ == TerminalStripModel::CableWire;
|
||||
|| column_ == TerminalStripModel::CableWire
|
||||
|| column_ == TerminalStripModel::CableTarget
|
||||
|| column_ == TerminalStripModel::Color
|
||||
|| column_ == TerminalStripModel::Section
|
||||
|| column_ == TerminalStripModel::Width;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,8 +888,15 @@ void TerminalStripEditor::copySelectionToClipboard()
|
||||
for (auto row = top ; row <= bottom ; ++row)
|
||||
{
|
||||
QStringList cells;
|
||||
for (auto column = left ; column <= right ; ++column) {
|
||||
cells << texts.value((qint64(row) << 32) | qint64(column));
|
||||
for (auto column = left ; column <= right ; ++column)
|
||||
{
|
||||
//A cell may hold several lines (the Target cell holds one
|
||||
//line by field): the text of the clipboard keeps one line by
|
||||
//row, the fields of that cell are joined back the way a
|
||||
//spreadsheet reads them
|
||||
QString cell_{texts.value((qint64(row) << 32) | qint64(column))};
|
||||
cell_.replace(QLatin1Char('\n'), QStringLiteral(" / "));
|
||||
cells << cell_;
|
||||
}
|
||||
lines << cells.join(QLatin1Char('\t'));
|
||||
}
|
||||
@@ -730,7 +911,7 @@ void TerminalStripEditor::copySelectionToClipboard()
|
||||
* the value is set to every selected cell.
|
||||
* - Otherwise the text is read like a table (tabulation between the columns
|
||||
* and new line between the rows) and written from the top left selected cell.
|
||||
* Only the label, cable and cable wire cells are modified.
|
||||
* Only the label, cable, cable wire, color, section and width cells are modified.
|
||||
*/
|
||||
void TerminalStripEditor::pasteFromClipboard()
|
||||
{
|
||||
@@ -984,3 +1165,170 @@ void TerminalStripEditor::on_m_move_to_pb_clicked()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
namespace {
|
||||
/**
|
||||
* @brief The NumberedLabel struct
|
||||
* A terminal label split in a prefix and a number : "X1:12" is "X1:" and 12.
|
||||
*/
|
||||
struct NumberedLabel
|
||||
{
|
||||
QString prefix;
|
||||
int number = 0; ///< 0 if the label has no number
|
||||
int width = 0; ///< Number of digits, to keep the leading zeros ("X1:05")
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief splitLabel
|
||||
* @param label : a terminal label, like "X1:12", "12" or "PE"
|
||||
* @return the label split in a prefix and its last number
|
||||
*/
|
||||
NumberedLabel splitLabel(const QString &label)
|
||||
{
|
||||
static const QRegularExpression re_(QStringLiteral("^(.*?)(\\d+)$"));
|
||||
NumberedLabel numbered_;
|
||||
const auto match_ = re_.match(label.trimmed());
|
||||
if (match_.hasMatch())
|
||||
{
|
||||
numbered_.prefix = match_.captured(1);
|
||||
numbered_.number = match_.captured(2).toInt();
|
||||
const auto digits_ = match_.captured(2);
|
||||
numbered_.width = digits_.startsWith(QLatin1Char('0')) ? digits_.size() : 0;
|
||||
}
|
||||
else {
|
||||
numbered_.prefix = label.trimmed();
|
||||
}
|
||||
return numbered_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief numberedLabel
|
||||
* @return @a prefix followed by @a number, with leading zeros up to @a width digits
|
||||
*/
|
||||
QString numberedLabel(const QString &prefix, int number, int width) {
|
||||
return prefix + QStringLiteral("%1").arg(number, width, 10, QLatin1Char('0'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripEditor::on_m_add_spare_pb_clicked
|
||||
* Add spare terminals (on no folio) after the selected terminal,
|
||||
* or at the end of the strip if no terminal is selected.
|
||||
* The labels continue the number of the selected terminal ("X1:12" ->
|
||||
* "X1:13", "X1:14"...), the user can change or remove them in the table.
|
||||
* The properties of the selected terminal (type, section, width, terminal
|
||||
* of the catalog, color, manufacturer...) can be copied to the spare terminals,
|
||||
* which then are the same part in the bill of materials.
|
||||
*/
|
||||
void TerminalStripEditor::on_m_add_spare_pb_clicked()
|
||||
{
|
||||
if (!m_model || !m_current_strip || !m_project) {
|
||||
return;
|
||||
}
|
||||
|
||||
//The last selected physical terminal in the strip, else the last one of the strip
|
||||
int insert_pos_ = -1;
|
||||
QSharedPointer<PhysicalTerminal> reference_;
|
||||
const auto selected_ = m_model->modelPhysicalTerminalDataForIndex(
|
||||
ui->m_table_widget->selectionModel()->selectedIndexes());
|
||||
for (const auto &data_ : selected_)
|
||||
{
|
||||
const auto phy_t = m_current_strip->physicalTerminal(data_.uuid_);
|
||||
const auto pos_ = m_current_strip->pos(phy_t);
|
||||
if (phy_t && pos_ >= insert_pos_) {
|
||||
insert_pos_ = pos_;
|
||||
reference_ = phy_t;
|
||||
}
|
||||
}
|
||||
if (reference_) {
|
||||
++insert_pos_; //After the selected terminal
|
||||
} else {
|
||||
insert_pos_ = -1; //At the end of the strip
|
||||
if (m_current_strip->physicalTerminalCount()) {
|
||||
reference_ = m_current_strip->physicalTerminal(m_current_strip->physicalTerminalCount() - 1);
|
||||
}
|
||||
}
|
||||
QSharedPointer<RealTerminal> reference_real_;
|
||||
if (reference_ && !reference_->realTerminals().isEmpty()) {
|
||||
reference_real_ = reference_->realTerminals().last();
|
||||
}
|
||||
|
||||
//Proposed numbering : the one of the reference terminal, continued
|
||||
const auto numbered_ = splitLabel(reference_real_ ? reference_real_->label() : QString());
|
||||
|
||||
QDialog dialog(this);
|
||||
dialog.setWindowTitle(tr("Add spare terminals"));
|
||||
auto layout_ = new QFormLayout(&dialog);
|
||||
|
||||
auto count_sb = new QSpinBox(&dialog);
|
||||
count_sb->setRange(1, 999);
|
||||
count_sb->setValue(1);
|
||||
layout_->addRow(tr("Number of terminals :"), count_sb);
|
||||
|
||||
auto numbering_cb = new QCheckBox(tr("Number the terminals"), &dialog);
|
||||
numbering_cb->setChecked(true);
|
||||
layout_->addRow(numbering_cb);
|
||||
|
||||
auto prefix_le = new QLineEdit(numbered_.prefix, &dialog);
|
||||
layout_->addRow(tr("Prefix :"), prefix_le);
|
||||
|
||||
auto first_sb = new QSpinBox(&dialog);
|
||||
first_sb->setRange(0, 999999);
|
||||
first_sb->setValue(numbered_.number + 1);
|
||||
layout_->addRow(tr("First number :"), first_sb);
|
||||
|
||||
connect(numbering_cb, &QCheckBox::toggled, prefix_le, &QWidget::setEnabled);
|
||||
connect(numbering_cb, &QCheckBox::toggled, first_sb, &QWidget::setEnabled);
|
||||
|
||||
auto copy_cb = new QCheckBox(tr("Same terminal as %1 (type, section, width, color, manufacturer...)")
|
||||
.arg(reference_real_ && !reference_real_->label().isEmpty()
|
||||
? reference_real_->label() : tr("the selected terminal")),
|
||||
&dialog);
|
||||
copy_cb->setChecked(!reference_real_.isNull());
|
||||
copy_cb->setEnabled(!reference_real_.isNull());
|
||||
layout_->addRow(copy_cb);
|
||||
|
||||
auto buttons_ = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);
|
||||
connect(buttons_, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
||||
connect(buttons_, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
layout_->addRow(buttons_);
|
||||
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
|
||||
//The data shared by every spare terminal
|
||||
ElementData model_;
|
||||
if (copy_cb->isChecked() && reference_real_)
|
||||
{
|
||||
const auto reference_data_ = reference_real_->terminalData();
|
||||
model_.setTerminalType(reference_data_.terminalType());
|
||||
model_.setTerminalFunction(reference_data_.terminalFunction());
|
||||
model_.setTerminalLED(reference_data_.terminalLed());
|
||||
//What makes the part, not what belongs to this terminal (label, cable...)
|
||||
for (const auto &key_ : {RealTerminal::sectionInfoKey(), RealTerminal::widthInfoKey(),
|
||||
RealTerminal::catalogEntryInfoKey(), RealTerminal::colorInfoKey(),
|
||||
QETInformation::ELMT_MANUFACTURER, QETInformation::ELMT_MANUFACTURER_REF,
|
||||
QETInformation::ELMT_DESIGNATION, QETInformation::ELMT_PLANT,
|
||||
QETInformation::ELMT_LOCATION})
|
||||
{
|
||||
const auto value_ = reference_data_.m_informations.value(key_).toString();
|
||||
if (!value_.isEmpty()) {
|
||||
model_.m_informations.addValue(key_, value_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QVector<QSharedPointer<RealTerminal>> spare_vector;
|
||||
for (int i = 0 ; i < count_sb->value() ; ++i)
|
||||
{
|
||||
auto data_ = model_;
|
||||
if (numbering_cb->isChecked()) {
|
||||
data_.m_informations.addValue(QStringLiteral("label"),
|
||||
numberedLabel(prefix_le->text(), first_sb->value() + i, numbered_.width));
|
||||
}
|
||||
spare_vector.append(RealTerminal::createSpare(data_));
|
||||
}
|
||||
|
||||
m_project->undoStack()->push(new AddSpareTerminalsCommand(m_current_strip, insert_pos_, spare_vector));
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace Ui {
|
||||
}
|
||||
|
||||
class QETProject;
|
||||
class Cable;
|
||||
class TerminalStrip;
|
||||
|
||||
/**
|
||||
@@ -54,9 +55,13 @@ class TerminalStripEditor : public QWidget
|
||||
void pasteFromClipboard();
|
||||
void clearSelectedTexts();
|
||||
void clear();
|
||||
void setManufacturerFilter(const QString &manufacturer);
|
||||
void spanMultiLevelTerminals();
|
||||
void selectionChanged();
|
||||
QSize setUpBridgeCellWidth();
|
||||
void watchCableChanges();
|
||||
void watchCable(Cable *cable);
|
||||
void reloadOnCableChange();
|
||||
TerminalStripModel::Column isSingleColumnSelected() const;
|
||||
QPair<TerminalStripModel::Column, QVector<modelRealTerminalData>> singleColumnData() const;
|
||||
|
||||
@@ -73,10 +78,14 @@ class TerminalStripEditor : public QWidget
|
||||
void on_m_unbridge_terminals_pb_clicked();
|
||||
void on_m_bridge_color_cb_activated(const QColor &col);
|
||||
void on_m_move_to_pb_clicked();
|
||||
void on_m_add_spare_pb_clicked();
|
||||
|
||||
private:
|
||||
Ui::TerminalStripEditor *ui;
|
||||
QPointer<QETProject> m_project;
|
||||
//The project whose cables this editor follows, which is not
|
||||
//always m_project yet when watchCableChanges() runs again
|
||||
QPointer<QETProject> m_watched_project;
|
||||
QPointer<TerminalStrip> m_current_strip;
|
||||
TerminalStripModel *m_model {nullptr};
|
||||
};
|
||||
|
||||
@@ -199,6 +199,30 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="15" column="0">
|
||||
<widget class="QLabel" name="m_manufacturer_label">
|
||||
<property name="text">
|
||||
<string>Manufacturer :</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="15" column="1" colspan="2">
|
||||
<widget class="QComboBox" name="m_manufacturer_cb">
|
||||
<property name="toolTip">
|
||||
<string>Manufacturer of the terminals proposed in the Width column</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="16" column="1" colspan="2">
|
||||
<widget class="QPushButton" name="m_add_spare_pb">
|
||||
<property name="toolTip">
|
||||
<string>Add spare terminals, on no folio, after the selected terminal</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Add spare terminals...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="17" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@@ -260,7 +284,7 @@
|
||||
<item row="5" column="1" colspan="2">
|
||||
<widget class="QSpinBox" name="m_level_sb">
|
||||
<property name="maximum">
|
||||
<number>6</number>
|
||||
<number>3</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -24,13 +24,94 @@
|
||||
#include "../../qetproject.h"
|
||||
#include "../realterminal.h"
|
||||
#include "../terminalstrip.h"
|
||||
#include "../terminalstripautoimport.h"
|
||||
#include "terminalstripcreatordialog.h"
|
||||
#include "terminalstripeditor.h"
|
||||
#include "terminalstripeditorwindow.h"
|
||||
#include "terminalstriptreedockwidget.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QElapsedTimer>
|
||||
#include <QMessageBox>
|
||||
#include <QProgressDialog>
|
||||
#include <QThread>
|
||||
#include <QWindow>
|
||||
|
||||
QPointer<TerminalStripEditorWindow> TerminalStripEditorWindow::window_;
|
||||
|
||||
/**
|
||||
* @brief TerminalStripEditorWindow::instance
|
||||
* @param project : the project to edit
|
||||
* @param parent : parent widget of the window, when it is created
|
||||
* @return the terminal block manager window, created the first time,
|
||||
* showing the terminal strips of @a project.
|
||||
* Opening it reads every terminal of the project (and builds the folios
|
||||
* not built yet) : it can take a while on a big project. A message and a
|
||||
* busy cursor say that it is opening, so that the user does not think
|
||||
* that nothing happens.
|
||||
*/
|
||||
TerminalStripEditorWindow *TerminalStripEditorWindow::instance(QETProject *project, QWidget *parent)
|
||||
{
|
||||
QProgressDialog busy_(tr("Opening the terminal block manager..."), QString(), 0, 0, parent);
|
||||
busy_.setWindowTitle(tr("Terminal block manager"));
|
||||
//Only a title : nothing to minimize, maximize or close
|
||||
busy_.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint);
|
||||
busy_.setWindowModality(Qt::ApplicationModal);
|
||||
busy_.setCancelButton(nullptr);
|
||||
busy_.setMinimumDuration(0);
|
||||
busy_.show();
|
||||
//Paint the message before the long work starts. One pass of the
|
||||
//event loop is not enough : the window is shown by the window system
|
||||
//(Wayland compositor...) a bit later. Wait for it to be exposed,
|
||||
//a short time at most, then paint it.
|
||||
QElapsedTimer timer_;
|
||||
timer_.start();
|
||||
while (timer_.elapsed() < 500)
|
||||
{
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
|
||||
if (busy_.windowHandle() && busy_.windowHandle()->isExposed()) {
|
||||
break;
|
||||
}
|
||||
QThread::msleep(5);
|
||||
}
|
||||
busy_.repaint();
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QApplication::setOverrideCursor(Qt::WaitCursor);
|
||||
|
||||
//The folios not built yet (lazy folios) are built now : show the
|
||||
//progress, and run the event loop between two folios, so that the
|
||||
//window system still gets answers (else the message is said
|
||||
//"not responding")
|
||||
if (project)
|
||||
{
|
||||
project->setFolioBuildObserver([&busy_](int built, int total)
|
||||
{
|
||||
if (busy_.maximum() != total) {
|
||||
busy_.setMaximum(total);
|
||||
}
|
||||
busy_.setLabelText(tr("Reading folio %1 of %2...").arg(built).arg(total));
|
||||
busy_.setValue(built);
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
});
|
||||
}
|
||||
|
||||
static QMutex mutex_;
|
||||
if (!window_) {
|
||||
mutex_.lock();
|
||||
if (!window_)
|
||||
window_ = new TerminalStripEditorWindow{project, parent};
|
||||
mutex_.unlock();
|
||||
} else {
|
||||
window_->setProject(project);
|
||||
}
|
||||
|
||||
if (project) {
|
||||
project->setFolioBuildObserver({});
|
||||
}
|
||||
QApplication::restoreOverrideCursor();
|
||||
return window_;
|
||||
}
|
||||
|
||||
static const int EMPTY_PAGE = 0;
|
||||
static const int FREE_TERMINAL_PAGE = 1;
|
||||
static const int TERMINAL_STRIP_PAGE = 2;
|
||||
@@ -75,6 +156,11 @@ TerminalStripEditorWindow::TerminalStripEditorWindow(QETProject *project, QWidge
|
||||
ui->m_stacked_widget->insertWidget(EMPTY_PAGE, new QWidget(ui->m_stacked_widget));
|
||||
ui->m_stacked_widget->insertWidget(FREE_TERMINAL_PAGE, m_free_terminal_editor);
|
||||
ui->m_stacked_widget->insertWidget(TERMINAL_STRIP_PAGE, m_terminal_strip_editor);
|
||||
|
||||
//The strips are worked out from the labels of the free terminals,
|
||||
//so that the user does not create them one by one : only the
|
||||
//toolbar actions run the import, opening the window changes
|
||||
//nothing
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,6 +183,81 @@ void TerminalStripEditorWindow::setProject(QETProject *project)
|
||||
m_terminal_strip_editor->setProject(project);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripEditorWindow::importFreeTerminals
|
||||
* Run one of the two import actions of the window, after the user has
|
||||
* confirmed it : @a move_placed false assigns the free terminals only,
|
||||
* true re-groups every terminal and also moves placed ones. Both run as
|
||||
* one undoable step and say what has been done.
|
||||
*/
|
||||
void TerminalStripEditorWindow::importFreeTerminals(bool move_placed)
|
||||
{
|
||||
if (!m_project) {
|
||||
return;
|
||||
}
|
||||
//A reload of the free terminal table would lose its unapplied edits
|
||||
if (m_free_terminal_editor && m_free_terminal_editor->hasPendingEdits())
|
||||
{
|
||||
QMessageBox::information(
|
||||
this, tr("Unapplied edits"),
|
||||
tr("Apply or discard the pending edits of the free terminals "
|
||||
"table first."));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto title_ = move_placed ? tr("Re-group all terminals")
|
||||
: tr("Assign free terminals");
|
||||
const auto text_ = move_placed
|
||||
? tr("Free terminals will be added to the terminal strips named "
|
||||
"after their labels, and every terminal whose label prefix "
|
||||
"or plant and location no longer match its strip will be "
|
||||
"moved to the right strip.\n\n"
|
||||
"Terminals you placed by hand in a strip whose name does "
|
||||
"not match their label will be moved too. Existing strips "
|
||||
"are never re-sorted.\n\n"
|
||||
"This action can be undone with a single Undo.")
|
||||
: tr("Every free terminal will be added to the terminal strip "
|
||||
"named after its label (\"X1:2\" goes to the strip \"X1\"), "
|
||||
"creating the strips when needed.\n\n"
|
||||
"Terminals that already belong to a strip are not touched "
|
||||
"and no strip is re-sorted.\n\n"
|
||||
"This action can be undone with a single Undo.");
|
||||
|
||||
if (QMessageBox::question(this, title_, text_,
|
||||
QMessageBox::Yes | QMessageBox::No,
|
||||
QMessageBox::No) != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto result_ = TerminalStripAutoImport::importFreeTerminals(
|
||||
m_project, move_placed);
|
||||
if (result_.terminals == 0 && result_.strips == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_tree_dock->reload();
|
||||
|
||||
if (move_placed) {
|
||||
statusBar()->showMessage(
|
||||
tr("%1 terminal(s) assigned or moved to a terminal strip.",
|
||||
"", result_.terminals).arg(result_.terminals),
|
||||
10000);
|
||||
} else {
|
||||
statusBar()->showMessage(
|
||||
tr("%1 free terminal(s) added to the terminal strips.",
|
||||
"", result_.terminals).arg(result_.terminals),
|
||||
10000);
|
||||
}
|
||||
}
|
||||
|
||||
void TerminalStripEditorWindow::on_m_import_free_terminals_triggered() {
|
||||
importFreeTerminals(false);
|
||||
}
|
||||
|
||||
void TerminalStripEditorWindow::on_m_regroup_terminals_triggered() {
|
||||
importFreeTerminals(true);
|
||||
}
|
||||
|
||||
void TerminalStripEditorWindow::setCurrentStrip(TerminalStrip *strip) {
|
||||
m_tree_dock->setSelectedStrip(strip);
|
||||
}
|
||||
|
||||
@@ -44,18 +44,7 @@ class TerminalStripEditorWindow : public QMainWindow
|
||||
static QPointer<TerminalStripEditorWindow> window_;
|
||||
|
||||
public:
|
||||
static TerminalStripEditorWindow* instance(QETProject *project, QWidget *parent = nullptr) {
|
||||
static QMutex mutex_;
|
||||
if (!window_) {
|
||||
mutex_.lock();
|
||||
if (!window_)
|
||||
window_ = new TerminalStripEditorWindow{project, parent};
|
||||
mutex_.unlock();
|
||||
} else {
|
||||
window_->setProject(project);
|
||||
}
|
||||
return window_;
|
||||
}
|
||||
static TerminalStripEditorWindow* instance(QETProject *project, QWidget *parent = nullptr);
|
||||
|
||||
static void dropInstance () {
|
||||
static QMutex mutex;
|
||||
@@ -80,6 +69,8 @@ class TerminalStripEditorWindow : public QMainWindow
|
||||
void on_m_add_terminal_strip_triggered();
|
||||
void on_m_remove_terminal_triggered();
|
||||
void on_m_reload_triggered();
|
||||
void on_m_import_free_terminals_triggered();
|
||||
void on_m_regroup_terminals_triggered();
|
||||
void on_m_button_box_clicked(QAbstractButton *button);
|
||||
void on_m_stacked_widget_currentChanged(int arg1);
|
||||
|
||||
@@ -87,6 +78,7 @@ class TerminalStripEditorWindow : public QMainWindow
|
||||
void addTreeDockWidget();
|
||||
void currentStripChanged(TerminalStrip *strip);
|
||||
void updateUi();
|
||||
void importFreeTerminals(bool move_placed);
|
||||
|
||||
private:
|
||||
Ui::TerminalStripEditorWindow *ui{nullptr};
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
<addaction name="m_add_terminal_strip"/>
|
||||
<addaction name="m_remove_terminal"/>
|
||||
<addaction name="m_reload"/>
|
||||
<addaction name="m_import_free_terminals"/>
|
||||
<addaction name="m_regroup_terminals"/>
|
||||
</widget>
|
||||
<action name="m_add_terminal_strip">
|
||||
<property name="icon">
|
||||
@@ -85,6 +87,28 @@
|
||||
<string>Reload the terminals</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="m_import_free_terminals">
|
||||
<property name="icon">
|
||||
<iconset theme="list-add"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Assign free terminals</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Add the free terminals to the terminal strips named after their labels ("X1:2" goes to the strip "X1"), creating the strips when needed. Terminals already assigned to a strip are never touched. Asks for confirmation.</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="m_regroup_terminals">
|
||||
<property name="icon">
|
||||
<iconset theme="start"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Re-group all terminals</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Assign the free terminals and also move every terminal whose label prefix or plant and location no longer match its strip. Existing strips are never re-sorted. Asks for confirmation.</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../../../qelectrotech.qrc"/>
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
#include <QFontDialog>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "terminalstriplayouteditor.h"
|
||||
#include "ui_terminalstriplayouteditor.h"
|
||||
#include "../GraphicsItem/properties/terminalstriplayoutpattern.h"
|
||||
@@ -33,6 +35,26 @@ TerminalStripLayoutEditor::TerminalStripLayoutEditor(QSharedPointer<TerminalStri
|
||||
// the application palette.
|
||||
ui->m_graphics_view->setBackgroundBrush(Qt::white);
|
||||
ui->m_graphics_view->scene()->addItem(&m_preview_strip_item);
|
||||
|
||||
//Drawing style
|
||||
//The style is chosen for each item when it is added to a folio,
|
||||
//here it only change the preview, to see the values of each style.
|
||||
connect(ui->m_draw_style_cb, &QComboBox::currentIndexChanged, this, [this](int index) {
|
||||
m_preview_strip_item.prepareGeometryChange();
|
||||
m_preview_strip_item.m_drawer.setDrawStyle(
|
||||
static_cast<TerminalStripLayoutPattern::DrawStyle>(std::clamp(index, 0, 2)));
|
||||
m_preview_strip_item.update();
|
||||
updatePreview();
|
||||
});
|
||||
connect(ui->m_realistic_scale_sb, &QDoubleSpinBox::valueChanged,
|
||||
this, &TerminalStripLayoutEditor::valueEdited);
|
||||
connect(ui->m_realistic_length_sb, &QSpinBox::valueChanged,
|
||||
this, &TerminalStripLayoutEditor::valueEdited);
|
||||
connect(ui->m_realistic_end_stops_cb, &QCheckBox::toggled,
|
||||
this, &TerminalStripLayoutEditor::valueEdited);
|
||||
connect(ui->m_realistic_group_marker_cb, &QCheckBox::toggled,
|
||||
this, &TerminalStripLayoutEditor::valueEdited);
|
||||
|
||||
updateUi();
|
||||
}
|
||||
|
||||
@@ -159,6 +181,12 @@ void TerminalStripLayoutEditor::valueEdited()
|
||||
m_layout.data()->m_cable_length = ui->m_cable_length_sb->value();
|
||||
m_layout.data()->m_cable_end_length = ui->m_cable_end_length_sb->value();
|
||||
|
||||
//Values of the realistic styles
|
||||
m_layout.data()->m_realistic_px_per_mm = ui->m_realistic_scale_sb->value();
|
||||
m_layout.data()->m_realistic_length_mm = ui->m_realistic_length_sb->value();
|
||||
m_layout.data()->m_realistic_end_stops = ui->m_realistic_end_stops_cb->isChecked();
|
||||
m_layout.data()->m_realistic_group_marker = ui->m_realistic_group_marker_cb->isChecked();
|
||||
|
||||
updateUi();
|
||||
m_preview_strip_item.update();
|
||||
}
|
||||
@@ -271,6 +299,12 @@ void TerminalStripLayoutEditor::updateUi()
|
||||
ui->m_cable_length_sb->setValue(data->m_cable_length);
|
||||
ui->m_cable_end_length_sb->setValue(data->m_cable_end_length);
|
||||
|
||||
//Values of the realistic styles
|
||||
ui->m_realistic_scale_sb->setValue(data->m_realistic_px_per_mm);
|
||||
ui->m_realistic_length_sb->setValue(qRound(data->m_realistic_length_mm));
|
||||
ui->m_realistic_end_stops_cb->setChecked(data->m_realistic_end_stops);
|
||||
ui->m_realistic_group_marker_cb->setChecked(data->m_realistic_group_marker);
|
||||
|
||||
m_ui_updating = false;
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
@@ -540,6 +540,88 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="2">
|
||||
<widget class="QLabel" name="label_draw_style">
|
||||
<property name="text">
|
||||
<string>Preview style</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="3" colspan="2">
|
||||
<widget class="QComboBox" name="m_draw_style_cb">
|
||||
<property name="toolTip">
|
||||
<string>Style of the preview only. The style of a terminal strip plan is chosen when it is added to a folio.</string>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Schematic</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Realistic (push-in)</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Realistic (screw)</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="2">
|
||||
<widget class="QLabel" name="label_realistic_scale">
|
||||
<property name="text">
|
||||
<string>Scale (px/mm) / length (mm)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="3">
|
||||
<widget class="QDoubleSpinBox" name="m_realistic_scale_sb">
|
||||
<property name="toolTip">
|
||||
<string>Number of pixels for one millimeter of terminal.</string>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.5</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>50.0</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.5</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="4">
|
||||
<widget class="QSpinBox" name="m_realistic_length_sb">
|
||||
<property name="toolTip">
|
||||
<string>Length of a terminal along the wires, in millimeter.</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>300</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="13" column="3">
|
||||
<widget class="QCheckBox" name="m_realistic_end_stops_cb">
|
||||
<property name="text">
|
||||
<string>End stops</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="13" column="4">
|
||||
<widget class="QCheckBox" name="m_realistic_group_marker_cb">
|
||||
<property name="text">
|
||||
<string>Group marker</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -16,18 +16,26 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "terminalstripmodel.h"
|
||||
#include "../terminalcatalog.h"
|
||||
#include "../terminalstrip.h"
|
||||
#include "../../qetgraphicsitem/element.h"
|
||||
#include "../physicalterminal.h"
|
||||
#include "../realterminal.h"
|
||||
#include "../terminalcolor.h"
|
||||
#include "../terminalstripbridge.h"
|
||||
#include <QColor>
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QLocale>
|
||||
#include <QBrush>
|
||||
#include <QVector>
|
||||
#include <QComboBox>
|
||||
#include <QSpinBox>
|
||||
#include <QPainter>
|
||||
#include <QApplication>
|
||||
#include <QKeyEvent>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QFrame>
|
||||
|
||||
/**
|
||||
* Some const int who describe what a column contain
|
||||
@@ -43,14 +51,51 @@ const int CONDUCTOR_CELL = 7;
|
||||
const int XREF_CELL = 8;
|
||||
const int CABLE_CELL = 9;
|
||||
const int CABLE_WIRE_CELL = 10;
|
||||
const int TYPE_CELL = 11;
|
||||
const int FUNCTION_CELL = 12;
|
||||
const int LED_CELL = 13;
|
||||
const int SHIELD_CELL = 14;
|
||||
const int CABLE_TARGET_CELL = 11;
|
||||
const int TYPE_CELL = 12;
|
||||
const int FUNCTION_CELL = 13;
|
||||
const int LED_CELL = 14;
|
||||
const int SHIELD_CELL = 15;
|
||||
const int COLOR_CELL = 16;
|
||||
const int SECTION_CELL = 17;
|
||||
const int WIDTH_CELL = 18;
|
||||
const int SEPARATOR_CELL = 19;
|
||||
|
||||
const int COLUMN_COUNT = 15;
|
||||
const int COLUMN_COUNT = 20;
|
||||
|
||||
static QVector<bool> UNMODIFIED_CELL_VECTOR{false, false, false, false, false, false, false, false, false, false, false, false, false, false, false};
|
||||
static QVector<bool> UNMODIFIED_CELL_VECTOR(COLUMN_COUNT, false);
|
||||
|
||||
/**
|
||||
* @brief modifiedCellKey
|
||||
* @return the key of the real terminal of @a data in the modified cells
|
||||
*/
|
||||
static const RealTerminal *modifiedCellKey(const modelRealTerminalData &data) {
|
||||
return data.real_terminal.toStrongRef().data();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief parsePositiveNumber
|
||||
* Convert the text written by the user to a number >= 0
|
||||
* (a section in mm2 or a width in mm).
|
||||
* The comma and the dot are both accepted as decimal separator.
|
||||
* @param text
|
||||
* @param ok : true if the text is valid (empty text is valid and mean 0)
|
||||
* @return
|
||||
*/
|
||||
static qreal parsePositiveNumber(const QString &text, bool *ok)
|
||||
{
|
||||
auto text_ = text.trimmed();
|
||||
text_.replace(QLatin1Char(','), QLatin1Char('.'));
|
||||
if (text_.isEmpty()) {
|
||||
*ok = true;
|
||||
return 0;
|
||||
}
|
||||
const auto value_ = text_.toDouble(ok);
|
||||
if (*ok && value_ < 0) {
|
||||
*ok = false;
|
||||
}
|
||||
return *ok ? value_ : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripModel::levelForColumn
|
||||
@@ -91,10 +136,15 @@ TerminalStripModel::Column TerminalStripModel::columnTypeForIndex(const QModelIn
|
||||
case 8 : return XRef;
|
||||
case 9 : return Cable;
|
||||
case 10 : return CableWire;
|
||||
case 11 : return Type;
|
||||
case 12 : return Function;
|
||||
case 13 : return Led;
|
||||
case 14 : return Shield;
|
||||
case 11 : return CableTarget;
|
||||
case 12 : return Type;
|
||||
case 13 : return Function;
|
||||
case 14 : return Led;
|
||||
case 15 : return Shield;
|
||||
case 16 : return Color;
|
||||
case 17 : return Section;
|
||||
case 18 : return Width;
|
||||
case 19 : return Separator;
|
||||
default : return Invalid;
|
||||
}
|
||||
}
|
||||
@@ -137,12 +187,7 @@ int TerminalStripModel::rowCount(const QModelIndex &parent) const
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto count = 0;
|
||||
for (const auto &mptd : m_physical_data) {
|
||||
count += mptd.real_data.size();
|
||||
}
|
||||
|
||||
return count;
|
||||
return m_row_count;
|
||||
}
|
||||
|
||||
int TerminalStripModel::columnCount(const QModelIndex &parent) const
|
||||
@@ -167,11 +212,21 @@ QVariant TerminalStripModel::data(const QModelIndex &index, int role) const
|
||||
case LEVEL_CELL : return mrtd.level_;
|
||||
case LABEL_CELL : return mrtd.label_;
|
||||
case CONDUCTOR_CELL : return mrtd.conductor_;
|
||||
case XREF_CELL : return mrtd.Xref_;
|
||||
case XREF_CELL : return mrtd.spare_ ? tr("Spare") : mrtd.Xref_;
|
||||
case CABLE_CELL : return mrtd.cable_;
|
||||
case CABLE_WIRE_CELL : return mrtd.cable_wire;
|
||||
case CABLE_TARGET_CELL : return mrtd.cable_target_;
|
||||
case TYPE_CELL : return ElementData::translatedTerminalType(mrtd.type_);
|
||||
case FUNCTION_CELL : return ElementData::translatedTerminalFunction(mrtd.function_);
|
||||
case COLOR_CELL : return TerminalColor::displayName(mrtd.color_, mrtd.stripe_color_);
|
||||
case SECTION_CELL : return mrtd.section_ > 0 ? QLocale().toString(mrtd.section_, 'g', 6) : QString();
|
||||
case WIDTH_CELL :
|
||||
if (mrtd.width_mm_ <= 0) {
|
||||
return QString();
|
||||
}
|
||||
//"6,2 - Phoenix Contact ST 4" : the width alone doesn't say which terminal it is
|
||||
return mrtd.catalog_entry_.isEmpty() ? QLocale().toString(mrtd.width_mm_, 'g', 6)
|
||||
: QLocale().toString(mrtd.width_mm_, 'g', 6) + QStringLiteral(" - ") + mrtd.catalog_entry_;
|
||||
default : return QVariant();
|
||||
}
|
||||
}
|
||||
@@ -181,10 +236,19 @@ QVariant TerminalStripModel::data(const QModelIndex &index, int role) const
|
||||
case LABEL_CELL : return mrtd.label_;
|
||||
case CABLE_CELL : return mrtd.cable_;
|
||||
case CABLE_WIRE_CELL : return mrtd.cable_wire;
|
||||
case CABLE_TARGET_CELL : return mrtd.cable_target_;
|
||||
case COLOR_CELL : return TerminalColor::toSpec(mrtd.color_, mrtd.stripe_color_);
|
||||
case SECTION_CELL : return mrtd.section_ > 0 ? QLocale().toString(mrtd.section_, 'g', 6) : QString();
|
||||
case WIDTH_CELL : return mrtd.width_mm_ > 0 ? QLocale().toString(mrtd.width_mm_, 'g', 6) : QString();
|
||||
default: return QVariant();
|
||||
|
||||
}
|
||||
}
|
||||
else if (role == CatalogEntryRole &&
|
||||
index.column() == WIDTH_CELL)
|
||||
{
|
||||
return mrtd.catalog_entry_;
|
||||
}
|
||||
else if (role == Qt::CheckStateRole &&
|
||||
index.column() == LED_CELL)
|
||||
{
|
||||
@@ -195,14 +259,33 @@ QVariant TerminalStripModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
return mrtd.shield_ ? Qt::Checked : Qt::Unchecked;
|
||||
}
|
||||
else if (role == Qt::BackgroundRole && index.column() < COLUMN_COUNT )
|
||||
else if (role == Qt::CheckStateRole &&
|
||||
index.column() == SEPARATOR_CELL)
|
||||
{
|
||||
if (m_modified_cell.contains(mrtd.element_) &&
|
||||
m_modified_cell.value(mrtd.element_).at(index.column()))
|
||||
return mrtd.separator_ ? Qt::Checked : Qt::Unchecked;
|
||||
}
|
||||
else if (role == Qt::ForegroundRole &&
|
||||
index.column() == XREF_CELL && mrtd.spare_)
|
||||
{
|
||||
return QBrush(Qt::gray); //"Spare" : not a reference to a folio
|
||||
}
|
||||
else if (role == Qt::BackgroundRole && index.column() < COLUMN_COUNT
|
||||
&& !m_modified_cell.isEmpty())
|
||||
{
|
||||
const auto key_ = modifiedCellKey(mrtd);
|
||||
if (m_modified_cell.contains(key_) &&
|
||||
m_modified_cell.value(key_).at(index.column()))
|
||||
{
|
||||
return QBrush(Qt::yellow);
|
||||
}
|
||||
}
|
||||
else if (role == Qt::DecorationRole &&
|
||||
index.column() == COLOR_CELL)
|
||||
{
|
||||
if (mrtd.color_.isValid()) {
|
||||
return TerminalColor::swatch(mrtd.color_, mrtd.stripe_color_);
|
||||
}
|
||||
}
|
||||
else if (role == Qt::DecorationRole &&
|
||||
(index.column() == LEVEL_0_CELL ||
|
||||
index.column() == LEVEL_1_CELL ||
|
||||
@@ -222,6 +305,18 @@ bool TerminalStripModel::setData(const QModelIndex &index, const QVariant &value
|
||||
int modified_cell = -1;
|
||||
auto column_ = index.column();
|
||||
|
||||
//A cell worked out from the cables of the project is read only:
|
||||
//the element never holds that value, so writing one in would only
|
||||
//show something the next reload takes away again
|
||||
if ((column_ == CABLE_CELL || column_ == CABLE_WIRE_CELL)
|
||||
&& mrtd.cable_auto_) {
|
||||
return false;
|
||||
}
|
||||
if (column_ == CABLE_TARGET_CELL
|
||||
&& mrtd.cable_target_auto_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (column_ == LEVEL_CELL &&
|
||||
role == Qt::EditRole)
|
||||
{
|
||||
@@ -273,29 +368,111 @@ bool TerminalStripModel::setData(const QModelIndex &index, const QVariant &value
|
||||
modified_ = true;
|
||||
modified_cell = CABLE_WIRE_CELL;
|
||||
}
|
||||
else if (column_ == CABLE_TARGET_CELL &&
|
||||
role == Qt::EditRole &&
|
||||
mrtd.cable_target_ != value.toString())
|
||||
{
|
||||
mrtd.cable_target_ = value.toString();
|
||||
modified_ = true;
|
||||
modified_cell = CABLE_TARGET_CELL;
|
||||
}
|
||||
else if (column_ == SHIELD_CELL)
|
||||
{
|
||||
mrtd.shield_ = value.toBool();
|
||||
modified_ = true;
|
||||
modified_cell = SHIELD_CELL;
|
||||
}
|
||||
else if (column_ == SEPARATOR_CELL)
|
||||
{
|
||||
mrtd.separator_ = value.toBool();
|
||||
modified_ = true;
|
||||
modified_cell = SEPARATOR_CELL;
|
||||
}
|
||||
else if (column_ == COLOR_CELL &&
|
||||
role == Qt::EditRole)
|
||||
{
|
||||
//An empty text remove the color, an unknown text is ignored.
|
||||
//The text can be a name of the palette (gray, blue, green/yellow...),
|
||||
//or a color "#rrggbb" or "#rrggbb/#rrggbb"
|
||||
QColor color_, stripe_;
|
||||
if (TerminalColor::parse(value.toString(), &color_, &stripe_) &&
|
||||
(mrtd.color_ != color_ || mrtd.stripe_color_ != stripe_))
|
||||
{
|
||||
mrtd.color_ = color_;
|
||||
mrtd.stripe_color_ = stripe_;
|
||||
modified_ = true;
|
||||
modified_cell = COLOR_CELL;
|
||||
}
|
||||
}
|
||||
else if (column_ == SECTION_CELL &&
|
||||
role == Qt::EditRole)
|
||||
{
|
||||
bool ok_ = false;
|
||||
const auto section_ = parsePositiveNumber(value.toString(), &ok_);
|
||||
if (ok_ && !qFuzzyCompare(1 + mrtd.section_, 1 + section_))
|
||||
{
|
||||
mrtd.section_ = section_;
|
||||
modified_ = true;
|
||||
modified_cell = SECTION_CELL;
|
||||
}
|
||||
}
|
||||
else if (column_ == WIDTH_CELL &&
|
||||
(role == Qt::EditRole || role == CatalogEntryRole))
|
||||
{
|
||||
bool ok_ = false;
|
||||
qreal width_ = 0;
|
||||
QString entry_;
|
||||
if (role == CatalogEntryRole)
|
||||
{
|
||||
//Chosen in the list : the width and the terminal of the catalog
|
||||
const auto map_ = value.toMap();
|
||||
width_ = map_.value(QStringLiteral("mm")).toDouble(&ok_);
|
||||
ok_ = ok_ && width_ >= 0;
|
||||
entry_ = map_.value(QStringLiteral("entry")).toString();
|
||||
}
|
||||
else
|
||||
{
|
||||
//Typed or pasted : "6.2", or "6,2 - Phoenix Contact ST 4" as copied from this column
|
||||
const auto text_ = value.toString();
|
||||
const auto separator_ = text_.indexOf(QStringLiteral(" - "));
|
||||
width_ = parsePositiveNumber(separator_ < 0 ? text_ : text_.left(separator_), &ok_);
|
||||
if (ok_ && separator_ >= 0) {
|
||||
entry_ = text_.mid(separator_ + 3).trimmed();
|
||||
}
|
||||
}
|
||||
//Only a terminal of the catalog with this width is kept
|
||||
const auto *catalog_entry_ = TerminalCatalog::entryByLabel(entry_);
|
||||
if (!catalog_entry_ || width_ <= 0 ||
|
||||
!qFuzzyCompare(1 + catalog_entry_->mm, 1 + width_)) {
|
||||
entry_.clear();
|
||||
}
|
||||
|
||||
if (ok_ && (!qFuzzyCompare(1 + mrtd.width_mm_, 1 + width_) ||
|
||||
mrtd.catalog_entry_ != entry_))
|
||||
{
|
||||
mrtd.width_mm_ = width_;
|
||||
mrtd.catalog_entry_ = entry_;
|
||||
modified_ = true;
|
||||
modified_cell = WIDTH_CELL;
|
||||
}
|
||||
}
|
||||
|
||||
//Set the modification to the terminal data
|
||||
if (modified_)
|
||||
{
|
||||
replaceDataAtRow(mrtd, index.row());
|
||||
|
||||
if (mrtd.element_)
|
||||
if (const auto key_ = modifiedCellKey(mrtd))
|
||||
{
|
||||
QVector<bool> vector_;
|
||||
if (m_modified_cell.contains(mrtd.element_)) {
|
||||
vector_ = m_modified_cell.value(mrtd.element_);
|
||||
if (m_modified_cell.contains(key_)) {
|
||||
vector_ = m_modified_cell.value(key_);
|
||||
} else {
|
||||
vector_ = UNMODIFIED_CELL_VECTOR;
|
||||
}
|
||||
|
||||
vector_.replace(modified_cell, true);
|
||||
m_modified_cell.insert(mrtd.element_, vector_);
|
||||
m_modified_cell.insert(key_, vector_);
|
||||
}
|
||||
emit dataChanged(index, index);
|
||||
return true;
|
||||
@@ -322,14 +499,27 @@ QVariant TerminalStripModel::headerData(int section, Qt::Orientation orientation
|
||||
case XREF_CELL: return tr("Cross-reference");
|
||||
case CABLE_CELL: return tr("Cable");
|
||||
case CABLE_WIRE_CELL: return tr("Color / wire number of the cable");
|
||||
case CABLE_TARGET_CELL: return tr("Target");
|
||||
case TYPE_CELL: return tr("Type");
|
||||
case FUNCTION_CELL : return tr("Function");
|
||||
case LED_CELL: return tr("led");
|
||||
case SHIELD_CELL: return tr("Shield");
|
||||
case COLOR_CELL: return tr("Color");
|
||||
case SECTION_CELL: return tr("Section (mm²)");
|
||||
case WIDTH_CELL: return tr("Width (mm)");
|
||||
case SEPARATOR_CELL: return tr("Separator plate after");
|
||||
default : return QVariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (role == Qt::ToolTipRole &&
|
||||
orientation == Qt::Horizontal &&
|
||||
section == CABLE_TARGET_CELL)
|
||||
{
|
||||
//Same three fields as the box drawn under the cable, one line
|
||||
//by field: only a line break separates them
|
||||
return tr("Installation, Location and Label (BMK) of the piece the cable lands on, past this strip, one line per field");
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
@@ -339,10 +529,21 @@ Qt::ItemFlags TerminalStripModel::flags(const QModelIndex &index) const
|
||||
Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
||||
|
||||
auto c = index.column();
|
||||
if (c == LABEL_CELL || c == TYPE_CELL || c == FUNCTION_CELL
|
||||
|| c == CABLE_CELL || c == CABLE_WIRE_CELL)
|
||||
//The cable cells are only edited by hand when no cable
|
||||
//of the project is wired to the terminal
|
||||
if ((c == CABLE_CELL || c == CABLE_WIRE_CELL)
|
||||
&& !dataAtRow(index.row()).cable_auto_)
|
||||
flags = flags | Qt::ItemIsEditable;
|
||||
if (c == LED_CELL || c == SHIELD_CELL) {
|
||||
//The target the project works out is read only: only the drawing
|
||||
//decides it. No target to show, or one typed in by hand, may be
|
||||
//written -- the same rule as the cable cells.
|
||||
if (c == CABLE_TARGET_CELL
|
||||
&& !dataAtRow(index.row()).cable_target_auto_)
|
||||
flags = flags | Qt::ItemIsEditable;
|
||||
if (c == LABEL_CELL || c == TYPE_CELL || c == FUNCTION_CELL
|
||||
|| c == COLOR_CELL || c == SECTION_CELL || c == WIDTH_CELL)
|
||||
flags = flags | Qt::ItemIsEditable;
|
||||
if (c == LED_CELL || c == SHIELD_CELL || c == SEPARATOR_CELL) {
|
||||
flags = flags | Qt::ItemIsUserCheckable;
|
||||
}
|
||||
return flags;
|
||||
@@ -364,7 +565,7 @@ QVector<modelRealTerminalData> TerminalStripModel::modifiedmodelRealTerminalData
|
||||
for (auto j = 0 ; j < ptd_.real_data.size() ; ++j)
|
||||
{
|
||||
auto mrtd_ = ptd_.real_data.at(j);
|
||||
if (modified_real_terminal.contains(mrtd_.element_))
|
||||
if (modified_real_terminal.contains(modifiedCellKey(mrtd_)))
|
||||
{
|
||||
returned_vector.append(m_physical_data.at(i).real_data.at(j));
|
||||
}
|
||||
@@ -430,7 +631,7 @@ QVector<modelRealTerminalData> TerminalStripModel::modelRealTerminalDataForIndex
|
||||
for (auto i : set_) {
|
||||
const auto rtd_ = realDataAtIndex(i);
|
||||
if (rtd_.level_ > -1) {
|
||||
vector_.append(realDataAtIndex(i));
|
||||
vector_.append(rtd_);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,6 +739,8 @@ void TerminalStripModel::reload()
|
||||
|
||||
void TerminalStripModel::fillPhysicalTerminalData()
|
||||
{
|
||||
m_row_index.clear();
|
||||
|
||||
//Get all physical terminal
|
||||
if (m_terminal_strip)
|
||||
{
|
||||
@@ -547,41 +750,32 @@ void TerminalStripModel::fillPhysicalTerminalData()
|
||||
mptd.pos_ = phy_t->pos();
|
||||
mptd.uuid_ = phy_t->uuid();
|
||||
|
||||
const auto physical_index = static_cast<int>(m_physical_data.size());
|
||||
for (const auto &real_t : phy_t->realTerminals())
|
||||
{
|
||||
if (!real_t.isNull())
|
||||
{
|
||||
mptd.real_data.append(modelRealTerminalData::data(real_t));
|
||||
m_row_index.append({physical_index,
|
||||
static_cast<int>(mptd.real_data.size()) - 1});
|
||||
}
|
||||
}
|
||||
|
||||
m_physical_data.append(mptd);
|
||||
}
|
||||
}
|
||||
|
||||
m_row_count = m_row_index.size();
|
||||
}
|
||||
|
||||
modelRealTerminalData TerminalStripModel::dataAtRow(int row) const
|
||||
{
|
||||
if (row > rowCount(QModelIndex())) {
|
||||
if (row < 0 || row >= m_row_count) {
|
||||
return modelRealTerminalData();
|
||||
}
|
||||
else
|
||||
{
|
||||
auto current_row = 0;
|
||||
for (const auto &physical_data : std::as_const(m_physical_data))
|
||||
{
|
||||
for (const auto &real_data : physical_data.real_data)
|
||||
{
|
||||
if (current_row == row) {
|
||||
return real_data;
|
||||
} else {
|
||||
++current_row;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modelRealTerminalData();
|
||||
const auto &place_ = m_row_index.at(row);
|
||||
return m_physical_data.at(place_.physical_).real_data.at(place_.real_);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -592,32 +786,12 @@ modelRealTerminalData TerminalStripModel::dataAtRow(int row) const
|
||||
*/
|
||||
void TerminalStripModel::replaceDataAtRow(modelRealTerminalData data, int row)
|
||||
{
|
||||
if (row > rowCount(QModelIndex())) {
|
||||
if (row < 0 || row >= m_row_count) {
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto current_row = 0;
|
||||
auto current_physical = 0;
|
||||
|
||||
for (const auto &physical_data : std::as_const(m_physical_data))
|
||||
{
|
||||
auto current_real = 0;
|
||||
for (int i=0 ; i<physical_data.real_data.count() ; ++i)
|
||||
{
|
||||
if (current_row == row) {
|
||||
auto physical_data = m_physical_data.at(current_physical);
|
||||
physical_data.real_data.replace(current_real, data);
|
||||
m_physical_data.replace(current_physical, physical_data);
|
||||
return;
|
||||
} else {
|
||||
++current_real;
|
||||
++current_row;
|
||||
}
|
||||
}
|
||||
++current_physical;
|
||||
}
|
||||
}
|
||||
const auto &place_ = m_row_index.at(row);
|
||||
m_physical_data[place_.physical_].real_data.replace(place_.real_, data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -635,26 +809,17 @@ modelPhysicalTerminalData TerminalStripModel::physicalDataAtIndex(int index) con
|
||||
return modelPhysicalTerminalData();
|
||||
}
|
||||
|
||||
int current_checked_index = -1;
|
||||
int current_phy = -1;
|
||||
bool match_ = false;
|
||||
|
||||
for (const auto &ptd_ : std::as_const(m_physical_data))
|
||||
{
|
||||
current_checked_index += ptd_.real_data.size();
|
||||
++current_phy;
|
||||
|
||||
if (current_checked_index >= index) {
|
||||
match_ = true;
|
||||
break;
|
||||
}
|
||||
//A row always belongs to the physical terminal it is a level of,
|
||||
//m_row_index says which one (negative index: the first physical
|
||||
//terminal, as the walk this replaces used to give)
|
||||
if (index < 0) {
|
||||
return m_physical_data.first();
|
||||
}
|
||||
|
||||
if (match_) {
|
||||
return m_physical_data.at(current_phy);
|
||||
} else {
|
||||
if (index >= m_row_count) {
|
||||
return modelPhysicalTerminalData();
|
||||
}
|
||||
|
||||
return m_physical_data.at(m_row_index.at(index).physical_);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -664,23 +829,34 @@ modelPhysicalTerminalData TerminalStripModel::physicalDataAtIndex(int index) con
|
||||
*/
|
||||
modelRealTerminalData TerminalStripModel::realDataAtIndex(int index) const
|
||||
{
|
||||
if (m_physical_data.isEmpty()) {
|
||||
if (index < 0 || index >= m_row_count) {
|
||||
return modelRealTerminalData();
|
||||
}
|
||||
|
||||
int current_checked_index = -1;
|
||||
const auto &place_ = m_row_index.at(index);
|
||||
return m_physical_data.at(place_.physical_).real_data.at(place_.real_);
|
||||
}
|
||||
|
||||
for (const auto & ptd_ : std::as_const(m_physical_data))
|
||||
/**
|
||||
* @brief bridgeWalkStep
|
||||
* The three fields the neighbour walk of bridgePixmapFor() reads: level,
|
||||
* bridge and identity. The whole row data would answer as well, but filling
|
||||
* it reads the cables of the project (RealTerminal::cableTarget() and
|
||||
* friends) -- work done for every step of a walk which the view runs for
|
||||
* every bridge cell it paints and measures.
|
||||
* @param real_t : the terminal to look at, may be null (no previous one)
|
||||
* @return the data the walk needs, everything else left as it defaults
|
||||
*/
|
||||
static modelRealTerminalData bridgeWalkStep(const QSharedPointer<RealTerminal> &real_t)
|
||||
{
|
||||
modelRealTerminalData mrtd;
|
||||
if (!real_t.isNull())
|
||||
{
|
||||
for (const auto & rtd_ : std::as_const(ptd_.real_data)) {
|
||||
++current_checked_index;
|
||||
if (current_checked_index == index) {
|
||||
return rtd_;
|
||||
}
|
||||
}
|
||||
mrtd.level_ = real_t->level();
|
||||
mrtd.bridged_ = real_t->isBridged();
|
||||
mrtd.real_terminal = real_t.toWeakRef();
|
||||
}
|
||||
|
||||
return modelRealTerminalData();
|
||||
return mrtd;
|
||||
}
|
||||
|
||||
QPixmap TerminalStripModel::bridgePixmapFor(const QModelIndex &index) const
|
||||
@@ -694,6 +870,15 @@ QPixmap TerminalStripModel::bridgePixmapFor(const QModelIndex &index) const
|
||||
return QPixmap();
|
||||
}
|
||||
|
||||
//Without a bridge on this strip there is nothing to draw in the
|
||||
//bridge cells: the check at the end of this function ends on an
|
||||
//empty pixmap, and every step of the walk below to reach it used
|
||||
//to fill the row data of a terminal (which reads the cables of the
|
||||
//whole project). Skip the walk when there is no bridge at all.
|
||||
if (!m_terminal_strip->hasBridges()) {
|
||||
return QPixmap();
|
||||
}
|
||||
|
||||
auto mrtd = modelRealTerminalDataForIndex(index);
|
||||
|
||||
//Terminal level correspond to the column level of index
|
||||
@@ -732,22 +917,30 @@ QPixmap TerminalStripModel::bridgePixmapFor(const QModelIndex &index) const
|
||||
//Terminal level ins't in the same column level of index
|
||||
//Check if we need to draw a none bridge pixmap
|
||||
|
||||
//Check previous
|
||||
auto phy_t = mrtd.real_terminal.toStrongRef()->physicalTerminal();
|
||||
auto current_real_terminal = mrtd;
|
||||
auto current_phy_uuid = phy_t->uuid();
|
||||
//Check previous. The rows of the table hold the terminals of the
|
||||
//strip in strip order, so the neighbour of a row is the row next
|
||||
//to it: asking the strip for its neighbour walked to the head of
|
||||
//the strip every time, for every bridge cell the view paints and
|
||||
//measures (and on every layout of the table).
|
||||
auto current_row = index.row();
|
||||
auto current_t = mrtd.real_terminal.toStrongRef();
|
||||
auto current_phy_uuid = current_t->physicalTerminal()->uuid();
|
||||
bool already_jumped_to_previous = false;
|
||||
modelRealTerminalData previous_data;
|
||||
|
||||
do {
|
||||
current_real_terminal = modelRealTerminalData::data(m_terminal_strip->previousRealTerminal(current_real_terminal.real_terminal));
|
||||
--current_row;
|
||||
const auto previous_t = current_row >= 0
|
||||
? dataAtRow(current_row).real_terminal.toStrongRef()
|
||||
: QSharedPointer<RealTerminal>();
|
||||
const auto current_real_terminal = bridgeWalkStep(previous_t);
|
||||
|
||||
if (current_real_terminal.level_ == -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
//We are in the same physical terminal as previous loop
|
||||
if (current_phy_uuid == current_real_terminal.real_terminal.toStrongRef()->physicalTerminal()->uuid())
|
||||
if (current_phy_uuid == previous_t->physicalTerminal()->uuid())
|
||||
{
|
||||
if (current_real_terminal.bridged_ &&
|
||||
current_real_terminal.level_ == level_column) {
|
||||
@@ -759,7 +952,7 @@ QPixmap TerminalStripModel::bridgePixmapFor(const QModelIndex &index) const
|
||||
break;
|
||||
} else {
|
||||
already_jumped_to_previous = true;
|
||||
current_phy_uuid = current_real_terminal.real_terminal.toStrongRef()->physicalTerminal()->uuid();
|
||||
current_phy_uuid = previous_t->physicalTerminal()->uuid();
|
||||
if (current_real_terminal.bridged_ &&
|
||||
current_real_terminal.level_ == level_column) {
|
||||
previous_data = current_real_terminal;
|
||||
@@ -769,20 +962,24 @@ QPixmap TerminalStripModel::bridgePixmapFor(const QModelIndex &index) const
|
||||
} while(true);
|
||||
|
||||
//Check next
|
||||
current_real_terminal = mrtd;
|
||||
current_phy_uuid = phy_t->uuid();
|
||||
current_row = index.row();
|
||||
current_phy_uuid = current_t->physicalTerminal()->uuid();
|
||||
bool already_jumped_to_next = false;
|
||||
modelRealTerminalData next_data;
|
||||
|
||||
do {
|
||||
current_real_terminal = modelRealTerminalData::data(m_terminal_strip->nextRealTerminal(current_real_terminal.real_terminal));
|
||||
++current_row;
|
||||
const auto next_t = current_row < m_row_count
|
||||
? dataAtRow(current_row).real_terminal.toStrongRef()
|
||||
: QSharedPointer<RealTerminal>();
|
||||
const auto current_real_terminal = bridgeWalkStep(next_t);
|
||||
|
||||
if (current_real_terminal.level_ == -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
//We are in the same physical terminal as previous loop
|
||||
if (current_phy_uuid == current_real_terminal.real_terminal.toStrongRef()->physicalTerminal()->uuid())
|
||||
if (current_phy_uuid == next_t->physicalTerminal()->uuid())
|
||||
{
|
||||
if (current_real_terminal.bridged_ &&
|
||||
current_real_terminal.level_ == level_column) {
|
||||
@@ -794,7 +991,7 @@ QPixmap TerminalStripModel::bridgePixmapFor(const QModelIndex &index) const
|
||||
break;
|
||||
} else {
|
||||
already_jumped_to_next = true;
|
||||
current_phy_uuid = current_real_terminal.real_terminal.toStrongRef()->physicalTerminal()->uuid();
|
||||
current_phy_uuid = next_t->physicalTerminal()->uuid();
|
||||
if (current_real_terminal.bridged_ &&
|
||||
current_real_terminal.level_ == level_column) {
|
||||
next_data = current_real_terminal;
|
||||
@@ -819,10 +1016,27 @@ QPixmap TerminalStripModel::bridgePixmapFor(const QModelIndex &index) const
|
||||
* and a spinbox to edit the level of a terminal
|
||||
**********************************************************/
|
||||
|
||||
namespace {
|
||||
QString widthItemText(double mm, const QString &label = QString())
|
||||
{
|
||||
const auto text_ = QCoreApplication::translate("TerminalStripModelDelegate", "%1 mm").arg(QLocale().toString(mm, 'g', 6));
|
||||
return label.isEmpty() ? text_ : text_ + QStringLiteral(" - ") + label;
|
||||
}
|
||||
}
|
||||
|
||||
TerminalStripModelDelegate::TerminalStripModelDelegate(QObject *parent) :
|
||||
QStyledItemDelegate(parent)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripModelDelegate::setManufacturer
|
||||
* @param manufacturer : manufacturer of the terminals proposed
|
||||
* in the Width column, an empty string proposes all the manufacturers.
|
||||
*/
|
||||
void TerminalStripModelDelegate::setManufacturer(const QString &manufacturer) {
|
||||
m_manufacturer = manufacturer;
|
||||
}
|
||||
|
||||
QWidget *TerminalStripModelDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||
{
|
||||
if (index.column() == TYPE_CELL) {
|
||||
@@ -850,10 +1064,215 @@ QWidget *TerminalStripModelDelegate::createEditor(QWidget *parent, const QStyleO
|
||||
|
||||
return qcb;
|
||||
}
|
||||
if (index.column() == SECTION_CELL) {
|
||||
auto qcb = new QComboBox(parent);
|
||||
qcb->setObjectName(QStringLiteral("terminal_section"));
|
||||
qcb->addItem(tr("None (default)"), 0.0);
|
||||
//Usual sections, in mm2
|
||||
for (const auto section_ : TerminalCatalog::sections()) {
|
||||
qcb->addItem(TerminalCatalog::sectionText(section_, tr("%1 mm²").arg(QLocale().toString(section_, 'g', 6))), section_);
|
||||
}
|
||||
|
||||
return qcb;
|
||||
}
|
||||
if (index.column() == WIDTH_CELL) {
|
||||
auto qcb = new QComboBox(parent);
|
||||
qcb->setObjectName(QStringLiteral("terminal_width"));
|
||||
qcb->addItem(tr("None (default)"), 0.0);
|
||||
|
||||
// Show the terminals of the selected manufacturer (all if none),
|
||||
// compatible with the section selected in this row.
|
||||
const auto manufacturer_ok = [this](const TerminalCatalog::WidthEntry &entry) {
|
||||
return m_manufacturer.isEmpty() || m_manufacturer == QString::fromUtf8(entry.manufacturer);
|
||||
};
|
||||
const auto section_index = index.sibling(index.row(), SECTION_CELL);
|
||||
bool section_ok = false;
|
||||
const auto section = parsePositiveNumber(section_index.data(Qt::EditRole).toString(), §ion_ok);
|
||||
bool has_compatible_entry = false;
|
||||
if (section_ok && section > 0.0) {
|
||||
for (const auto &entry : TerminalCatalog::widthEntries()) {
|
||||
if (manufacturer_ok(entry) && qFuzzyCompare(1 + entry.section, 1 + section)) {
|
||||
has_compatible_entry = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the section has no catalog match (or is unspecified), keep all widths available.
|
||||
for (const auto &entry : TerminalCatalog::widthEntries()) {
|
||||
if (manufacturer_ok(entry) &&
|
||||
(!has_compatible_entry || qFuzzyCompare(1 + entry.section, 1 + section))) {
|
||||
qcb->addItem(widthItemText(entry.mm, QString::fromUtf8(entry.label)), entry.mm);
|
||||
qcb->setItemData(qcb->count() - 1, QString::fromUtf8(entry.label), TerminalStripModel::CatalogEntryRole);
|
||||
}
|
||||
}
|
||||
|
||||
return qcb;
|
||||
}
|
||||
if (index.column() == COLOR_CELL) {
|
||||
auto qcb = new QComboBox(parent);
|
||||
qcb->setObjectName(QStringLiteral("terminal_color"));
|
||||
qcb->addItem(tr("None (default)"), QString());
|
||||
for (const auto &entry : TerminalColor::palette())
|
||||
{
|
||||
const auto spec_ = TerminalColor::specOf(entry);
|
||||
QColor main_, stripe_;
|
||||
TerminalColor::parse(spec_, &main_, &stripe_);
|
||||
qcb->addItem(QIcon(TerminalColor::swatch(main_, stripe_)),
|
||||
TerminalColor::translatedName(entry),
|
||||
spec_);
|
||||
}
|
||||
|
||||
return qcb;
|
||||
}
|
||||
if (index.column() == CABLE_TARGET_CELL) {
|
||||
//The Target cell holds one line by field, so it takes a multi
|
||||
//line editor : Return writes a new line (the cell gives its
|
||||
//text back when the focus goes somewhere else), Ctrl+Return
|
||||
//hands it over at once, Escape throws the text away
|
||||
auto te = new QPlainTextEdit(parent);
|
||||
te->setObjectName(QStringLiteral("terminal_cable_target"));
|
||||
te->setFrameStyle(QFrame::NoFrame);
|
||||
te->setTabChangesFocus(true);
|
||||
te->setToolTip(tr("One line per field: Installation, Location, Label (BMK).\n"
|
||||
"Ctrl+Return gives the cell back, Escape throws the text away."));
|
||||
te->installEventFilter(const_cast<TerminalStripModelDelegate *>(this));
|
||||
|
||||
return te;
|
||||
}
|
||||
|
||||
return QStyledItemDelegate::createEditor(parent, option, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripModelDelegate::eventFilter
|
||||
* The filter taken on by the Target cell editor: only the key which is
|
||||
* handled here is the one giving the text back to the table. The filter
|
||||
* of the base class is left aside for that cell on purpose, it is written
|
||||
* for one line editors and would take Return away from the text.
|
||||
* @param object the editor of the cell
|
||||
* @param event the event sent to it
|
||||
* @return true when the event is taken by this delegate
|
||||
*/
|
||||
bool TerminalStripModelDelegate::eventFilter(QObject *object, QEvent *event)
|
||||
{
|
||||
if (object && object->objectName() == QLatin1String("terminal_cable_target"))
|
||||
{
|
||||
if (event && event->type() == QEvent::KeyPress)
|
||||
{
|
||||
const auto key_event = static_cast<QKeyEvent *>(event);
|
||||
if ((key_event->key() == Qt::Key_Return ||
|
||||
key_event->key() == Qt::Key_Enter) &&
|
||||
key_event->modifiers().testFlag(Qt::ControlModifier))
|
||||
{
|
||||
auto editor_ = qobject_cast<QWidget *>(object);
|
||||
emit commitData(editor_);
|
||||
emit closeEditor(editor_, QAbstractItemDelegate::NoHint);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return QStyledItemDelegate::eventFilter(object, event);
|
||||
}
|
||||
|
||||
void TerminalStripModelDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
|
||||
{
|
||||
if (index.isValid() &&
|
||||
editor->objectName() == QLatin1String("terminal_color"))
|
||||
{
|
||||
if (auto qcb = dynamic_cast<QComboBox *>(editor))
|
||||
{
|
||||
const auto spec_ = index.data(Qt::EditRole).toString();
|
||||
auto pos_ = qcb->findData(spec_);
|
||||
if (pos_ < 0)
|
||||
{
|
||||
//The color is not in the palette (typed or pasted), keep it in the list
|
||||
QColor main_, stripe_;
|
||||
TerminalColor::parse(spec_, &main_, &stripe_);
|
||||
qcb->addItem(QIcon(TerminalColor::swatch(main_, stripe_)), spec_, spec_);
|
||||
pos_ = qcb->count() - 1;
|
||||
}
|
||||
qcb->setCurrentIndex(pos_);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (index.isValid() &&
|
||||
editor->objectName() == QLatin1String("terminal_section"))
|
||||
{
|
||||
if (auto qcb = dynamic_cast<QComboBox *>(editor))
|
||||
{
|
||||
bool ok_ = false;
|
||||
const auto section_ = parsePositiveNumber(index.data(Qt::EditRole).toString(), &ok_);
|
||||
auto pos_ = -1;
|
||||
for (auto i = 0 ; i < qcb->count() ; ++i)
|
||||
{
|
||||
if (qFuzzyCompare(1 + qcb->itemData(i).toDouble(), 1 + section_)) {
|
||||
pos_ = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pos_ < 0)
|
||||
{
|
||||
//The section is not in the list (typed or pasted), keep it in the list
|
||||
qcb->addItem(TerminalCatalog::sectionText(section_, tr("%1 mm²").arg(QLocale().toString(section_, 'g', 6))), section_);
|
||||
pos_ = qcb->count() - 1;
|
||||
}
|
||||
qcb->setCurrentIndex(pos_);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (index.isValid() &&
|
||||
editor->objectName() == QLatin1String("terminal_width"))
|
||||
{
|
||||
if (auto qcb = dynamic_cast<QComboBox *>(editor))
|
||||
{
|
||||
bool ok_ = false;
|
||||
const auto width_ = parsePositiveNumber(index.data(Qt::EditRole).toString(), &ok_);
|
||||
const auto entry_ = index.data(TerminalStripModel::CatalogEntryRole).toString();
|
||||
//The terminal chosen in the catalog. A width typed or set before
|
||||
//the catalog entry was stored is not tied to a terminal of the
|
||||
//catalog : it must not take the first one with the same width.
|
||||
auto pos_ = entry_.isEmpty() ? -1 : qcb->findData(entry_, TerminalStripModel::CatalogEntryRole);
|
||||
for (auto i = 0 ; pos_ < 0 && entry_.isEmpty() && i < qcb->count() ; ++i)
|
||||
{
|
||||
if (qcb->itemData(i, TerminalStripModel::CatalogEntryRole).toString().isEmpty() &&
|
||||
qFuzzyCompare(1 + qcb->itemData(i).toDouble(), 1 + width_)) {
|
||||
pos_ = i;
|
||||
}
|
||||
}
|
||||
if (pos_ < 0)
|
||||
{
|
||||
//The width or the terminal is not in the list
|
||||
//(typed, pasted, or another manufacturer), keep it in the list
|
||||
qcb->addItem(widthItemText(width_, entry_), width_);
|
||||
if (!entry_.isEmpty()) {
|
||||
qcb->setItemData(qcb->count() - 1, entry_, TerminalStripModel::CatalogEntryRole);
|
||||
}
|
||||
pos_ = qcb->count() - 1;
|
||||
}
|
||||
qcb->setCurrentIndex(pos_);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (index.isValid() &&
|
||||
editor->objectName() == QLatin1String("terminal_cable_target"))
|
||||
{
|
||||
if (auto te = dynamic_cast<QPlainTextEdit *>(editor)) {
|
||||
//The cell shows one line by field: the editor gets them
|
||||
//back exactly as they are, nothing joined, nothing split
|
||||
te->setPlainText(index.data(Qt::EditRole).toString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
QStyledItemDelegate::setEditorData(editor, index);
|
||||
}
|
||||
|
||||
void TerminalStripModelDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
|
||||
{
|
||||
if (index.isValid())
|
||||
@@ -870,6 +1289,35 @@ void TerminalStripModelDelegate::setModelData(QWidget *editor, QAbstractItemMode
|
||||
model->setData(index, qcb->currentData(), Qt::EditRole);
|
||||
}
|
||||
}
|
||||
else if (editor->objectName() == QLatin1String("terminal_section"))
|
||||
{
|
||||
if (auto qcb = dynamic_cast<QComboBox *>(editor)) {
|
||||
model->setData(index, qcb->currentData().toDouble(), Qt::EditRole);
|
||||
}
|
||||
}
|
||||
else if (editor->objectName() == QLatin1String("terminal_width"))
|
||||
{
|
||||
if (auto qcb = dynamic_cast<QComboBox *>(editor)) {
|
||||
QVariantMap map_;
|
||||
map_.insert(QStringLiteral("mm"), qcb->currentData().toDouble());
|
||||
map_.insert(QStringLiteral("entry"), qcb->currentData(TerminalStripModel::CatalogEntryRole).toString());
|
||||
model->setData(index, map_, TerminalStripModel::CatalogEntryRole);
|
||||
}
|
||||
}
|
||||
else if (editor->objectName() == QLatin1String("terminal_color"))
|
||||
{
|
||||
if (auto qcb = dynamic_cast<QComboBox *>(editor)) {
|
||||
model->setData(index, qcb->currentData(), Qt::EditRole);
|
||||
}
|
||||
}
|
||||
else if (editor->objectName() == QLatin1String("terminal_cable_target"))
|
||||
{
|
||||
if (auto te = dynamic_cast<QPlainTextEdit *>(editor)) {
|
||||
//The lines are handed over as they were written, so a
|
||||
//" / " inside a field is part of it and never a separator
|
||||
model->setData(index, te->toPlainText(), Qt::EditRole);
|
||||
}
|
||||
}
|
||||
else {
|
||||
QStyledItemDelegate::setModelData(editor, model, index);
|
||||
}
|
||||
|
||||
@@ -58,16 +58,25 @@ class TerminalStripModel : public QAbstractTableModel
|
||||
XRef = 8,
|
||||
Cable = 9,
|
||||
CableWire = 10,
|
||||
Type = 11,
|
||||
Function = 12,
|
||||
Led = 13,
|
||||
Shield = 14,
|
||||
CableTarget = 11,
|
||||
Type = 12,
|
||||
Function = 13,
|
||||
Led = 14,
|
||||
Shield = 15,
|
||||
Color = 16,
|
||||
Section = 17,
|
||||
Width = 18,
|
||||
Separator = 19,
|
||||
Invalid = 99
|
||||
};
|
||||
|
||||
static int levelForColumn(TerminalStripModel::Column column);
|
||||
static TerminalStripModel::Column columnTypeForIndex(const QModelIndex &index);
|
||||
|
||||
///Role of the Width column : the label of the terminal chosen in the catalog.
|
||||
///setData() with this role takes a QVariantMap {"mm": width, "entry": label}
|
||||
static constexpr int CatalogEntryRole = Qt::UserRole + 1;
|
||||
|
||||
Q_OBJECT
|
||||
public:
|
||||
TerminalStripModel(TerminalStrip *terminal_strip, QObject *parent = nullptr);
|
||||
@@ -98,9 +107,24 @@ class TerminalStripModel : public QAbstractTableModel
|
||||
QPixmap bridgePixmapFor(const QModelIndex &index) const;
|
||||
|
||||
private:
|
||||
//Where the data of one row of the table sit in m_physical_data.
|
||||
//The view asks for every cell of every row, over and over (paint,
|
||||
//flags, row height), so looking a row up must be a lookup and not
|
||||
//a walk through the whole strip each time.
|
||||
struct RowPlace
|
||||
{
|
||||
int physical_; //index in m_physical_data
|
||||
int real_; //index in the real_data of that physical terminal
|
||||
};
|
||||
|
||||
QPointer<TerminalStrip> m_terminal_strip;
|
||||
QHash<QPointer<Element>, QVector<bool>> m_modified_cell;
|
||||
//Modified cells of each real terminal (spare terminals have no element)
|
||||
QHash<const RealTerminal *, QVector<bool>> m_modified_cell;
|
||||
QVector<modelPhysicalTerminalData> m_physical_data;
|
||||
//One RowPlace by row, filled with m_physical_data (see fillPhysicalTerminalData)
|
||||
QVector<RowPlace> m_row_index;
|
||||
//Number of rows held in m_row_index, so rowCount() needs no walk
|
||||
int m_row_count = 0;
|
||||
struct BridgePixmap
|
||||
{
|
||||
QPixmap top_,
|
||||
@@ -124,12 +148,22 @@ class TerminalStripModelDelegate : public QStyledItemDelegate
|
||||
QWidget *parent,
|
||||
const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index) const override;
|
||||
void setEditorData(
|
||||
QWidget *editor,
|
||||
const QModelIndex &index) const override;
|
||||
void setModelData(
|
||||
QWidget *editor,
|
||||
QAbstractItemModel *model,
|
||||
const QModelIndex &index) const override;
|
||||
bool eventFilter(QObject *object, QEvent *event) override;
|
||||
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
|
||||
void setManufacturer(const QString &manufacturer);
|
||||
|
||||
private:
|
||||
///Manufacturer of the terminals proposed in the Width column, empty = all
|
||||
QString m_manufacturer;
|
||||
};
|
||||
|
||||
#endif // TERMINALSTRIPMODEL_H
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
#include "assignvariables.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include "../diagram.h"
|
||||
#include "../diagramposition.h"
|
||||
#include "../qetapp.h"
|
||||
@@ -229,6 +231,7 @@ namespace autonum
|
||||
FormulaContext context;
|
||||
const BorderTitleBlock &border = diagram->border_and_titleblock;
|
||||
context.folio = border.folio();
|
||||
context.folio_title = border.title();
|
||||
context.folio_index = diagram->folioIndex();
|
||||
context.folio_total = border.folioTotal();
|
||||
context.plant = border.plant();
|
||||
@@ -412,9 +415,9 @@ namespace autonum
|
||||
m_assigned_label.replace("%M", m_context.plant);
|
||||
m_assigned_label.replace("%LM", m_context.locmach);
|
||||
|
||||
QSettings settings;
|
||||
if (m_context.has_element)
|
||||
{
|
||||
QSettings settings;
|
||||
if (settings.value("border-columns_0", true).toBool()){
|
||||
m_assigned_label.replace("%c", QString::number(m_context.element_position.number() - 1));
|
||||
}else{
|
||||
@@ -435,6 +438,12 @@ namespace autonum
|
||||
assignTitleBlockVar();
|
||||
assignProjectVar();
|
||||
assignSequence();
|
||||
|
||||
//The folio title last: it is free text, and a title such as
|
||||
//"Schema %c" must stay as typed, not have the variables in it
|
||||
//replaced. %T on its own only, not the start of a longer word.
|
||||
static const QRegularExpression folio_title(QStringLiteral("%T(?![A-Za-z0-9_])"));
|
||||
m_assigned_label.replace(folio_title, m_context.folio_title);
|
||||
}
|
||||
|
||||
void AssignVariables::assignTitleBlockVar()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user