mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-22 14:24:13 +02:00
Compare commits
269 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06d31d9921 | |||
| 139993a691 | |||
| 2bedfaa394 | |||
| 8ce3b214c1 | |||
| de248eb20a | |||
| 76a36bc5e9 | |||
| 0e5d4ad990 | |||
| f4a7d5ebbb | |||
| 537e3c3c10 | |||
| dc30b26dab | |||
| 04ad1c890a | |||
| 672ba7c409 | |||
| 025bd0a57e | |||
| d05cc654ad | |||
| 992058348b | |||
| 1bf076eca8 | |||
| 8f2cd9f039 | |||
| 214b0e3b7d | |||
| 52adc8b3f8 | |||
| 74f908aa1e | |||
| f828e464c4 | |||
| 25ffa414af | |||
| 9c3c19a316 | |||
| 0a577eaabf | |||
| aebe82d6c2 | |||
| eba05bed43 | |||
| 7a2b390b78 | |||
| aa7f328aeb | |||
| 3a863d4b09 | |||
| d2bacfd526 | |||
| 5641b031f9 | |||
| adf3ea301c | |||
| ee227a007e | |||
| c32ebc5936 | |||
| 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
|
||||
|
||||
|
||||
+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"
|
||||
|
||||
@@ -291,6 +291,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 +386,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 +713,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 +764,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 +833,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 +955,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 +1125,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 |
+2211
-1475
File diff suppressed because it is too large
Load Diff
+3835
-2911
File diff suppressed because it is too large
Load Diff
+4521
-3596
File diff suppressed because it is too large
Load Diff
+2192
-1469
File diff suppressed because it is too large
Load Diff
+2680
-1790
File diff suppressed because it is too large
Load Diff
+4855
-3924
File diff suppressed because it is too large
Load Diff
+2711
-1802
File diff suppressed because it is too large
Load Diff
+3447
-2539
File diff suppressed because it is too large
Load Diff
+2163
-1477
File diff suppressed because it is too large
Load Diff
+2284
-1396
File diff suppressed because it is too large
Load Diff
+2198
-1501
File diff suppressed because it is too large
Load Diff
+3962
-3093
File diff suppressed because it is too large
Load Diff
+5603
-4635
File diff suppressed because it is too large
Load Diff
+4993
-4055
File diff suppressed because it is too large
Load Diff
+4491
-3513
File diff suppressed because it is too large
Load Diff
+2182
-1473
File diff suppressed because it is too large
Load Diff
+2192
-1469
File diff suppressed because it is too large
Load Diff
+2185
-1462
File diff suppressed because it is too large
Load Diff
+4017
-3128
File diff suppressed because it is too large
Load Diff
@@ -9936,6 +9936,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 +10319,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 +21805,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>
|
||||
|
||||
+2163
-1477
File diff suppressed because it is too large
Load Diff
+2760
-1854
File diff suppressed because it is too large
Load Diff
+6703
-5743
File diff suppressed because it is too large
Load Diff
+2270
-1407
File diff suppressed because it is too large
Load Diff
+2167
-1470
File diff suppressed because it is too large
Load Diff
+2170
-1477
File diff suppressed because it is too large
Load Diff
+4151
-3274
File diff suppressed because it is too large
Load Diff
+9803
-8706
File diff suppressed because it is too large
Load Diff
+2178
-1478
File diff suppressed because it is too large
Load Diff
+2170
-1477
File diff suppressed because it is too large
Load Diff
+2192
-1469
File diff suppressed because it is too large
Load Diff
+5111
-4232
File diff suppressed because it is too large
Load Diff
+4633
-3642
File diff suppressed because it is too large
Load Diff
+3922
-3025
File diff suppressed because it is too large
Load Diff
+24748
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
|
||||
@@ -48,6 +51,9 @@ PropertiesEditorDockWidget::~PropertiesEditorDockWidget()
|
||||
*/
|
||||
void PropertiesEditorDockWidget::clear()
|
||||
{
|
||||
if (!m_editor_list.isEmpty())
|
||||
rememberHeight();
|
||||
|
||||
foreach (PropertiesEditorWidget *editor, m_editor_list)
|
||||
{
|
||||
m_editor_list.removeOne(editor);
|
||||
@@ -94,8 +100,11 @@ 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();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -118,9 +127,84 @@ 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);
|
||||
|
||||
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;
|
||||
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())
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#define SELECTIONPROPERTIESDOCKWIDGET_H
|
||||
|
||||
#include <QDockWidget>
|
||||
#include <QPointer>
|
||||
|
||||
class PropertiesEditorWidget;
|
||||
|
||||
@@ -45,7 +46,13 @@ class PropertiesEditorDockWidget : public QDockWidget
|
||||
QList <PropertiesEditorWidget *> m_editor_list;
|
||||
|
||||
private:
|
||||
void rememberHeight();
|
||||
void restoreHeight();
|
||||
|
||||
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};
|
||||
|
||||
@@ -0,0 +1,800 @@
|
||||
/*
|
||||
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};
|
||||
};
|
||||
|
||||
} //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};
|
||||
return QRectF{0, 0, width_mm_ * s_, pattern.m_realistic_length_mm * s_};
|
||||
}
|
||||
|
||||
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_;
|
||||
|
||||
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();
|
||||
|
||||
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
|
||||
@@ -16,8 +16,12 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "terminalstripdrawer.h"
|
||||
#include "terminalstripdrawerhelpers.h"
|
||||
#include "realisticterminalstripdrawer.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QPolygonF>
|
||||
#include <QPainterPath>
|
||||
#include <QFontMetricsF>
|
||||
#include <QHash>
|
||||
|
||||
@@ -405,14 +409,491 @@ void drawCables(QPainter *painter, const QVector<CableCell> &cells, const Termin
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Width and color of a terminal.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The width of the terminals of the layout is the width of a terminal
|
||||
* of 5.2 mm (the usual width of the terminals of 2.5 mm2).
|
||||
* The width set by the user, in mm, is converted according to this reference.
|
||||
*/
|
||||
constexpr qreal reference_terminal_width_mm{5.2};
|
||||
|
||||
/**
|
||||
* @brief terminalRect
|
||||
* @param pattern
|
||||
* @param index : level of the terminal (0 to 3)
|
||||
* @param real_t : can be null
|
||||
* @return The rectangle of the terminal : the one of the layout,
|
||||
* with the width adapted to the width (in mm) of @a real_t if it is set.
|
||||
*/
|
||||
QRectF terminalRect(const TerminalStripLayoutPattern &pattern,
|
||||
int index,
|
||||
const QSharedPointer<AbstractRealTerminalInterface> &real_t)
|
||||
{
|
||||
auto rect_{pattern.m_terminal_rect[index]};
|
||||
if (real_t && real_t->widthMm() > 0) {
|
||||
rect_.setWidth(rect_.width() * real_t->widthMm() / reference_terminal_width_mm);
|
||||
}
|
||||
return rect_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The TerminalColors struct
|
||||
* Colors used to fill the rectangle of a terminal.
|
||||
* An invalid fill color means that the rectangle is not filled.
|
||||
* The stripe color (if valid) is drawn as stripes over the fill color.
|
||||
*/
|
||||
struct TerminalColors
|
||||
{
|
||||
QColor fill;
|
||||
QColor stripe;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief terminalColors
|
||||
* @param real_t : can be null
|
||||
* @return The colors of the terminal : the color set by the user,
|
||||
* else the default color of the type of the terminal.
|
||||
*/
|
||||
TerminalColors terminalColors(const QSharedPointer<AbstractRealTerminalInterface> &real_t)
|
||||
{
|
||||
if (!real_t) {
|
||||
return {};
|
||||
}
|
||||
if (real_t->color().isValid()) {
|
||||
return {real_t->color(), real_t->stripeColor()};
|
||||
}
|
||||
switch (real_t->type()) {
|
||||
case ElementData::TTGround: return {QColor(255, 216, 0), QColor(46, 158, 46)}; //Green/Yellow
|
||||
default: return {QColor(150, 150, 150), QColor()}; //Grey housing, like a real terminal block
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief terminalBodyPath
|
||||
* @param rect : rectangle of the terminal
|
||||
* @return The outline of a terminal block seen from the front : a body of the full width
|
||||
* with, at the top and at the bottom, a slightly narrower cap (the wire entry),
|
||||
* linked to the body by a small slope.
|
||||
* Two neighbouring terminals touch each other by their bodies and leave a small notch between their caps.
|
||||
*/
|
||||
QPainterPath terminalBodyPath(const QRectF &rect)
|
||||
{
|
||||
QPainterPath path_;
|
||||
if (rect.width() <= 0 || rect.height() <= 0) {
|
||||
return path_;
|
||||
}
|
||||
|
||||
const qreal inset_ = qMin(qreal(1.0), rect.width() * 0.1);
|
||||
const qreal cap_ = qMin(qreal(6.0), rect.height() * 0.06);
|
||||
const qreal slope_ = qMin(qreal(2.0), rect.height() * 0.03);
|
||||
const qreal l_ = rect.left(), r_ = rect.right(), t_ = rect.top(), b_ = rect.bottom();
|
||||
|
||||
QPolygonF outline_;
|
||||
outline_ << QPointF(l_ + inset_, t_)
|
||||
<< QPointF(r_ - inset_, t_)
|
||||
<< QPointF(r_ - inset_, t_ + cap_)
|
||||
<< QPointF(r_, t_ + cap_ + slope_)
|
||||
<< QPointF(r_, b_ - cap_ - slope_)
|
||||
<< QPointF(r_ - inset_, b_ - cap_)
|
||||
<< QPointF(r_ - inset_, b_)
|
||||
<< QPointF(l_ + inset_, b_)
|
||||
<< QPointF(l_ + inset_, b_ - cap_)
|
||||
<< QPointF(l_, b_ - cap_ - slope_)
|
||||
<< QPointF(l_, t_ + cap_ + slope_)
|
||||
<< QPointF(l_ + inset_, t_ + cap_);
|
||||
path_.addPolygon(outline_);
|
||||
path_.closeSubpath();
|
||||
return path_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The Zone struct
|
||||
* A vertical zone (relative to the top of a terminal) already used by a text or a symbol.
|
||||
*/
|
||||
struct Zone
|
||||
{
|
||||
qreal top{0};
|
||||
qreal bottom{0};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief drawTerminalBody Draw a terminal housing seen from the front,
|
||||
* like in a terminal line-up diagram : a grey body with wire entries (caps) at both ends,
|
||||
* and clamping screws drawn in the free zones of the body.
|
||||
* @param painter
|
||||
* @param rect : rectangle of the terminal
|
||||
* @param used_zones : zones used by the texts and the symbols (in the frame of the strip),
|
||||
* the screws are drawn out of them.
|
||||
* @param bottom_line : draw the line between the body and the lower wire entry.
|
||||
* Must be false if a symbol (fuse, led...) is drawn over this line.
|
||||
* The details are deliberately schematic and scale with the terminal width.
|
||||
*/
|
||||
void drawTerminalBody(QPainter *painter, const QRectF &rect, const QVector<Zone> &used_zones, bool bottom_line)
|
||||
{
|
||||
if (rect.width() <= 0 || rect.height() <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
painter->drawPath(terminalBodyPath(rect));
|
||||
|
||||
if (rect.width() < 5.0 || rect.height() < 12.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const qreal cap_ = qMin(qreal(6.0), rect.height() * 0.06);
|
||||
const qreal slope_ = qMin(qreal(2.0), rect.height() * 0.03);
|
||||
const qreal body_top_ = rect.top() + cap_ + slope_;
|
||||
const qreal body_bottom_ = rect.bottom() - cap_ - slope_;
|
||||
|
||||
//Limit between the body and the wire entries
|
||||
painter->drawLine(QPointF(rect.left(), body_top_),
|
||||
QPointF(rect.right(), body_top_));
|
||||
if (bottom_line) {
|
||||
painter->drawLine(QPointF(rect.left(), body_bottom_),
|
||||
QPointF(rect.right(), body_bottom_));
|
||||
}
|
||||
|
||||
//Free zones of the body : what remains when the used zones are removed
|
||||
QVector<Zone> free_zones_;
|
||||
free_zones_.append(Zone{body_top_, body_bottom_});
|
||||
const qreal margin_ = 2.5;
|
||||
for (const auto &used_ : used_zones)
|
||||
{
|
||||
QVector<Zone> new_free_;
|
||||
for (const auto &free_ : std::as_const(free_zones_))
|
||||
{
|
||||
if (used_.bottom + margin_ <= free_.top || used_.top - margin_ >= free_.bottom) {
|
||||
new_free_.append(free_);
|
||||
continue;
|
||||
}
|
||||
if (used_.top - margin_ > free_.top) {
|
||||
new_free_.append(Zone{free_.top, used_.top - margin_});
|
||||
}
|
||||
if (used_.bottom + margin_ < free_.bottom) {
|
||||
new_free_.append(Zone{used_.bottom + margin_, free_.bottom});
|
||||
}
|
||||
}
|
||||
free_zones_ = new_free_;
|
||||
}
|
||||
|
||||
//In each free zone, draw the clamping parts like on a terminal block seen from the front :
|
||||
//screw, castle tab, big center circle, castle tab, screw.
|
||||
//If the zone is too small, only the center circle or one screw is drawn.
|
||||
const qreal screw_r_ = qMin(qreal(5.0), rect.width() * 0.3);
|
||||
const qreal center_r_ = qMin(screw_r_ * 1.5, rect.width() * 0.4);
|
||||
if (screw_r_ < 1.0) {
|
||||
return;
|
||||
}
|
||||
const qreal screw_h_ = screw_r_ * 2 + 3;
|
||||
const qreal center_h_ = center_r_ * 2 + 2;
|
||||
const qreal tab_h_ = 4.0;
|
||||
const qreal tab_w_ = qMin(rect.width() * 0.3, qreal(5.0));
|
||||
const qreal cx_ = rect.center().x();
|
||||
|
||||
auto draw_screw = [&](qreal y_, qreal r_, bool window_)
|
||||
{
|
||||
const QPointF c_{cx_, y_};
|
||||
if (window_) {
|
||||
const qreal half_{qMin(r_ + 1.5, rect.width() / 2 - 0.8)};
|
||||
if (half_ > r_ + 0.3) {
|
||||
painter->drawRoundedRect(QRectF(cx_ - half_, c_.y() - half_, half_ * 2, half_ * 2), 1.0, 1.0);
|
||||
}
|
||||
}
|
||||
painter->drawEllipse(c_, r_, r_);
|
||||
painter->drawLine(QPointF(c_.x() - r_ * 0.6, c_.y()), QPointF(c_.x() + r_ * 0.6, c_.y()));
|
||||
};
|
||||
auto draw_tab = [&](qreal y_)
|
||||
{
|
||||
painter->drawRect(QRectF(cx_ - tab_w_, y_ - tab_h_ / 2, tab_w_ * 2, tab_h_));
|
||||
};
|
||||
|
||||
for (const auto &free_ : std::as_const(free_zones_))
|
||||
{
|
||||
const qreal h_ = free_.bottom - free_.top;
|
||||
if (h_ >= screw_h_ * 2 + center_h_ + tab_h_ * 2 + 2)
|
||||
{
|
||||
const qreal top_y_ = free_.top + screw_h_ / 2;
|
||||
const qreal bottom_y_ = free_.bottom - screw_h_ / 2;
|
||||
const qreal middle_y_ = (top_y_ + bottom_y_) / 2;
|
||||
draw_screw(top_y_, screw_r_, true);
|
||||
draw_tab((top_y_ + screw_h_ / 2 + middle_y_ - center_h_ / 2) / 2);
|
||||
draw_screw(middle_y_, center_r_, false);
|
||||
draw_tab((middle_y_ + center_h_ / 2 + bottom_y_ - screw_h_ / 2) / 2);
|
||||
draw_screw(bottom_y_, screw_r_, true);
|
||||
}
|
||||
else if (h_ >= center_h_ + 2) {
|
||||
draw_screw((free_.top + free_.bottom) / 2, center_r_, false);
|
||||
}
|
||||
else if (h_ >= screw_h_) {
|
||||
draw_screw((free_.top + free_.bottom) / 2, screw_r_, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief markerTagLength
|
||||
* Like the real marker tags, every tag of a strip have the same size,
|
||||
* whatever the text written on it ("1", "10"...).
|
||||
* The length is the one needed by the longest number of the strip
|
||||
* (at least two digits), limited to the text zone.
|
||||
* @param metrics : metrics of the font used to draw the text
|
||||
* @param longest_number : the longest number of the strip
|
||||
* @param zone_length : length of the text zone, along the text
|
||||
* @return the length of the marker tag along the text
|
||||
*/
|
||||
qreal markerTagLength(const QFontMetricsF &metrics, const QString &longest_number, qreal zone_length)
|
||||
{
|
||||
const qreal text_ = qMax(metrics.horizontalAdvance(QStringLiteral("00")),
|
||||
metrics.horizontalAdvance(longest_number));
|
||||
return qMin(zone_length, text_ + 4.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief markerTagZone
|
||||
* @return The vertical zone (in the frame of the strip) really covered by the marker tag of a text,
|
||||
* which is smaller than the rectangle of the text.
|
||||
* @param metrics : metrics of the font used to draw the text
|
||||
* @param text_y : y of the rectangle of the text
|
||||
* @param text_height : height of the rectangle of the text
|
||||
* @param text : text drawn
|
||||
* @param option : option used to draw the text
|
||||
* @param vertical : true if the text is rotated
|
||||
* @param tag_length : length of the marker tags of the strip, along the text
|
||||
*/
|
||||
Zone markerTagZone(const QFontMetricsF &metrics, qreal text_y, qreal text_height,
|
||||
const QString &text, const QTextOption &option, bool vertical,
|
||||
qreal tag_length)
|
||||
{
|
||||
if (!vertical)
|
||||
{
|
||||
const qreal cross_ = qMin(text_height, metrics.height() + 2);
|
||||
return Zone{text_y + (text_height - cross_) / 2, text_y + (text_height + cross_) / 2};
|
||||
}
|
||||
Q_UNUSED(text)
|
||||
//Same length for every terminal, so the body of every terminal is drawn the same way
|
||||
const qreal along_ = qMin(text_height, tag_length);
|
||||
if (option.alignment() & Qt::AlignRight) {
|
||||
return Zone{text_y, text_y + along_};
|
||||
}
|
||||
if (option.alignment() & Qt::AlignHCenter) {
|
||||
return Zone{text_y + (text_height - along_) / 2, text_y + (text_height + along_) / 2};
|
||||
}
|
||||
return Zone{text_y + text_height - along_, text_y + text_height};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief drawMarkerTag
|
||||
* Draw the marker tag of a terminal (white, or black and ribbed for an isolating terminal)
|
||||
* behind a text. The tag fits the text and follows its alignment.
|
||||
* Must be called with the painter in the frame of @a text_rect (the one used to draw the text).
|
||||
* @param painter
|
||||
* @param text_rect : rectangle used to draw the text
|
||||
* @param text : text drawn
|
||||
* @param option : option used to draw the text
|
||||
* @param vertical : true if the text is rotated
|
||||
* @param sectional : true for an isolating terminal
|
||||
* @param tag_length : length of the marker tags of the strip, along the text
|
||||
*/
|
||||
void drawMarkerTag(QPainter *painter, const QRectF &text_rect, const QString &text,
|
||||
const QTextOption &option, bool vertical, bool sectional,
|
||||
qreal tag_length)
|
||||
{
|
||||
if (text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const QFontMetricsF metrics_{painter->font()};
|
||||
//Vertical text : fixed length, like a real marker tag.
|
||||
//Horizontal text : the tag take the whole width of the terminal.
|
||||
const qreal along_ = vertical ? qMin(text_rect.width(), tag_length) : text_rect.width();
|
||||
const qreal cross_ = vertical ?
|
||||
text_rect.height() - 2 * qMin(qreal(1.2), text_rect.height() * 0.12) :
|
||||
qMin(text_rect.height(), metrics_.height() + 2);
|
||||
qreal x_{text_rect.left()};
|
||||
if (option.alignment() & Qt::AlignRight) {
|
||||
x_ = text_rect.right() - along_;
|
||||
} else if (option.alignment() & Qt::AlignHCenter) {
|
||||
x_ = text_rect.left() + (text_rect.width() - along_) / 2;
|
||||
}
|
||||
const QRectF tag_rect_{x_, text_rect.top() + (text_rect.height() - cross_) / 2, along_, cross_};
|
||||
|
||||
painter->save();
|
||||
painter->setBrush(sectional ? Qt::black : Qt::white);
|
||||
painter->drawRoundedRect(tag_rect_, 0.8, 0.8);
|
||||
if (sectional)
|
||||
{
|
||||
//Ribbed grip of the isolating terminals
|
||||
painter->setPen(QPen(QColor(90, 90, 90), 0));
|
||||
for (auto rib_x_ = tag_rect_.left() + 1.5 ; rib_x_ < tag_rect_.right() - 0.5 ; rib_x_ += 1.5) {
|
||||
painter->drawLine(QPointF(rib_x_, tag_rect_.top() + 0.5),
|
||||
QPointF(rib_x_, tag_rect_.bottom() - 0.5));
|
||||
}
|
||||
}
|
||||
painter->restore();
|
||||
painter->setPen(sectional ? Qt::white : Qt::black);
|
||||
|
||||
//The text is longer than the tag ("100", "PE"...) : reduce the font, like on a real tag.
|
||||
const qreal available_ = along_ - 2.0;
|
||||
const qreal text_length_ = metrics_.horizontalAdvance(text);
|
||||
if (text_length_ > available_ && available_ > 0)
|
||||
{
|
||||
auto font_{painter->font()};
|
||||
const auto size_{font_.pixelSize() > 0 ? qreal(font_.pixelSize()) : font_.pointSizeF()};
|
||||
const auto new_size_{std::max<qreal>(3.0, size_ * available_ / text_length_)};
|
||||
if (font_.pixelSize() > 0) {
|
||||
font_.setPixelSize(std::max(1, static_cast<int>(new_size_)));
|
||||
} else {
|
||||
font_.setPointSizeF(new_size_);
|
||||
}
|
||||
painter->setFont(font_);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief terminalNumber
|
||||
* @param label : label of a terminal, for example "XAUZ1:1"
|
||||
* @return The number of the terminal, the part after the last ':' ("1").
|
||||
* The strip name is already written in the header of the strip.
|
||||
* If the label have no ':', return the label.
|
||||
*/
|
||||
QString terminalNumber(const QString &label)
|
||||
{
|
||||
const auto index_ = label.lastIndexOf(QLatin1Char(':'));
|
||||
return index_ < 0 ? label : label.mid(index_ + 1).trimmed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief fitTextToCell
|
||||
* The text of a terminal is drawn vertically (or not) inside the width of the terminal.
|
||||
* If the font is too big for a narrow terminal, the text overlaps the next ones.
|
||||
* Clip the painter to the width of the terminal and reduce the font if needed.
|
||||
* Must be called with the painter at the left of the terminal, before any rotation.
|
||||
* @param painter
|
||||
* @param cell_width : width of the terminal
|
||||
* @param text_is_vertical : true if the text is rotated (the height of the font is then along the width of the terminal)
|
||||
*/
|
||||
void fitTextToCell(QPainter *painter, qreal cell_width, bool text_is_vertical)
|
||||
{
|
||||
painter->setClipRect(QRectF(0, -100000, cell_width, 200000), Qt::IntersectClip);
|
||||
|
||||
if (!text_is_vertical || cell_width <= 0) {
|
||||
return;
|
||||
}
|
||||
auto font_{painter->font()};
|
||||
const auto available_{std::max<qreal>(1.0, cell_width - 1.0)};
|
||||
const QFontMetricsF metrics_{font_};
|
||||
if (metrics_.height() > available_ && metrics_.height() > 0)
|
||||
{
|
||||
const auto size_{font_.pixelSize() > 0 ? qreal(font_.pixelSize()) : font_.pointSizeF()};
|
||||
const auto new_size_{std::max<qreal>(3.0, size_ * available_ / metrics_.height())};
|
||||
if (font_.pixelSize() > 0) {
|
||||
font_.setPixelSize(static_cast<int>(new_size_));
|
||||
} else {
|
||||
font_.setPointSizeF(new_size_);
|
||||
}
|
||||
painter->setFont(font_);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief drawStripes
|
||||
* Draw diagonal stripes of @a color inside @a rect.
|
||||
*/
|
||||
void drawStripes(QPainter *painter, const QRectF &rect, const QColor &color)
|
||||
{
|
||||
painter->save();
|
||||
painter->setClipPath(terminalBodyPath(rect));
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(color);
|
||||
|
||||
const auto band_{std::max<qreal>(2, rect.width() / 2)};
|
||||
for (auto x = rect.left() - rect.height() ; x < rect.right() ; x += 2 * band_)
|
||||
{
|
||||
QPolygonF polygon_;
|
||||
polygon_ << QPointF(x, rect.bottom())
|
||||
<< QPointF(x + band_, rect.bottom())
|
||||
<< QPointF(x + band_ + rect.height(), rect.top())
|
||||
<< QPointF(x + rect.height(), rect.top());
|
||||
painter->drawPolygon(polygon_);
|
||||
}
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The color (black or white) the more readable on @a background.
|
||||
* If @a background is invalid, return black.
|
||||
*/
|
||||
QColor contrastColor(const QColor &background)
|
||||
{
|
||||
if (!background.isValid()) {
|
||||
return Qt::black;
|
||||
}
|
||||
const auto luminance_{0.299 * background.redF() + 0.587 * background.greenF() + 0.114 * background.blueF()};
|
||||
return luminance_ < 0.5 ? QColor(Qt::white) : QColor(Qt::black);
|
||||
}
|
||||
|
||||
} //End anonymous namespace
|
||||
|
||||
/*
|
||||
* Helpers : give to the other drawing styles access to
|
||||
* the functions of the anonymous namespace.
|
||||
*/
|
||||
qreal Helpers::referenceTerminalWidthMm() {
|
||||
return reference_terminal_width_mm;
|
||||
}
|
||||
|
||||
void Helpers::terminalColors(const QSharedPointer<AbstractRealTerminalInterface> &real_t,
|
||||
QColor &fill, QColor &stripe)
|
||||
{
|
||||
const auto colors_{::TerminalStripDrawer::terminalColors(real_t)};
|
||||
fill = colors_.fill;
|
||||
stripe = colors_.stripe;
|
||||
}
|
||||
|
||||
QColor Helpers::contrastColor(const QColor &background) {
|
||||
return ::TerminalStripDrawer::contrastColor(background);
|
||||
}
|
||||
|
||||
QString Helpers::terminalNumber(const QString &label) {
|
||||
return ::TerminalStripDrawer::terminalNumber(label);
|
||||
}
|
||||
|
||||
void Helpers::drawTypeSymbol(QPainter *painter, const QRectF &box,
|
||||
ElementData::TerminalType type, bool led) {
|
||||
::TerminalStripDrawer::drawTypeSymbol(painter, box, type, led);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripDrawer::paint
|
||||
* @param painter
|
||||
*/
|
||||
void TerminalStripDrawer::paint(QPainter *painter)
|
||||
{
|
||||
if (m_strip && m_pattern && m_draw_style != TerminalStripLayoutPattern::DrawStyle::Schematic)
|
||||
{
|
||||
//The realistic styles don't write the xrefs, but each terminal
|
||||
//is a clickable zone, like the xref text of the schematic style.
|
||||
m_united_xref_text_rect = QRectF();
|
||||
m_xref_rects.clear();
|
||||
m_hovered_xref = hoverTerminal{};
|
||||
Realistic::paint(painter, *m_strip, *m_pattern, m_draw_style, m_preview_draw,
|
||||
m_mouse_hover_pos, &m_xref_rects);
|
||||
for (const auto &xref_rect : std::as_const(m_xref_rects))
|
||||
{
|
||||
m_united_xref_text_rect = m_united_xref_text_rect.isNull() ?
|
||||
xref_rect.rect :
|
||||
m_united_xref_text_rect.united(xref_rect.rect);
|
||||
if (m_hovered_xref.physical < 0 && !m_mouse_hover_pos.isNull()
|
||||
&& xref_rect.rect.contains(m_mouse_hover_pos)) {
|
||||
m_hovered_xref.physical = xref_rect.physical;
|
||||
m_hovered_xref.real = xref_rect.real;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_strip && m_pattern)
|
||||
{
|
||||
m_united_xref_text_rect = QRectF();
|
||||
@@ -492,6 +973,42 @@ void TerminalStripDrawer::paint(QPainter *painter)
|
||||
QVector<CableCell> cable_cells;
|
||||
|
||||
m_hovered_xref = hoverTerminal{};
|
||||
|
||||
//Length of the longest xref of the strip, used to reserve the same
|
||||
//zone in the body of every terminal.
|
||||
qreal xref_zone_length_{0};
|
||||
{
|
||||
const QFontMetricsF xref_metrics_{painter->font()};
|
||||
for (const auto &physical_t : m_strip->physicalTerminal()) {
|
||||
for (const auto &real_t : physical_t->realTerminals()) {
|
||||
if (real_t && !real_t->xref().isEmpty()) {
|
||||
xref_zone_length_ = qMax(xref_zone_length_,
|
||||
xref_metrics_.horizontalAdvance(real_t->xref()) + 4.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
xref_zone_length_ = qMin(xref_zone_length_, qreal(xref_text_height));
|
||||
}
|
||||
|
||||
//Length of the marker tags of the strip, calibrated on the longest number
|
||||
qreal tag_length_{0};
|
||||
{
|
||||
QString longest_number_;
|
||||
const QFontMetricsF tag_metrics_{painter->font()};
|
||||
for (const auto &physical_t : m_strip->physicalTerminal()) {
|
||||
for (const auto &real_t : physical_t->realTerminals()) {
|
||||
if (!real_t) {
|
||||
continue;
|
||||
}
|
||||
const auto number_{terminalNumber(real_t->label())};
|
||||
if (tag_metrics_.horizontalAdvance(number_) > tag_metrics_.horizontalAdvance(longest_number_)) {
|
||||
longest_number_ = number_;
|
||||
}
|
||||
}
|
||||
}
|
||||
tag_length_ = markerTagLength(tag_metrics_, longest_number_, terminals_text_height);
|
||||
}
|
||||
|
||||
int physical_index = 0;
|
||||
//Loop over physical terminals
|
||||
for (const auto &physical_t : m_strip->physicalTerminal())
|
||||
@@ -509,7 +1026,7 @@ void TerminalStripDrawer::paint(QPainter *painter)
|
||||
break;
|
||||
}
|
||||
|
||||
terminal_rect = m_pattern->m_terminal_rect[index_];
|
||||
terminal_rect = terminalRect(*m_pattern, index_, real_terminal_vector[i]);
|
||||
|
||||
//Cable (hose) of this terminal
|
||||
CableCell cable_cell;
|
||||
@@ -523,17 +1040,71 @@ void TerminalStripDrawer::paint(QPainter *painter)
|
||||
}
|
||||
const bool have_cable{m_pattern->m_cable_wire_length > 0 && !cable_cell.cable.isEmpty()};
|
||||
cable_cells.append(cable_cell);
|
||||
//Draw terminal rect
|
||||
painter->drawRect(terminal_rect);
|
||||
//Draw terminal rect, filled with the color of the terminal (if any)
|
||||
const auto colors_{terminalColors(real_terminal_vector[i])};
|
||||
const auto fill_color_{colors_.fill};
|
||||
const auto text_color_{contrastColor(fill_color_)};
|
||||
painter->save();
|
||||
if (fill_color_.isValid()) {
|
||||
painter->setBrush(QBrush(fill_color_));
|
||||
}
|
||||
QVector<Zone> used_zones_;
|
||||
if (real_terminal_vector[i])
|
||||
{
|
||||
const QFontMetricsF metrics_{painter->font()};
|
||||
used_zones_.append(markerTagZone(metrics_, terminals_text_y, terminals_text_height,
|
||||
terminalNumber(real_terminal_vector[i]->label()),
|
||||
terminals_text_option,
|
||||
terminals_text_orientation == Qt::Vertical,
|
||||
tag_length_));
|
||||
//The zone of the xref have the length of the longest xref of the strip,
|
||||
//so the body of every terminal is drawn the same way.
|
||||
if (xref_text_orientation == Qt::Vertical)
|
||||
{
|
||||
if (xref_text_option.alignment() & Qt::AlignRight) {
|
||||
used_zones_.append(Zone{xref_text_y, xref_text_y + xref_zone_length_});
|
||||
} else if (xref_text_option.alignment() & Qt::AlignHCenter) {
|
||||
used_zones_.append(Zone{xref_text_y + (xref_text_height - xref_zone_length_) / 2,
|
||||
xref_text_y + (xref_text_height + xref_zone_length_) / 2});
|
||||
} else {
|
||||
used_zones_.append(Zone{xref_text_y + xref_text_height - xref_zone_length_,
|
||||
xref_text_y + xref_text_height});
|
||||
}
|
||||
} else {
|
||||
const qreal cross_ = qMin(xref_text_height, metrics_.height() + 2);
|
||||
used_zones_.append(Zone{xref_text_y + (xref_text_height - cross_) / 2,
|
||||
xref_text_y + (xref_text_height + cross_) / 2});
|
||||
}
|
||||
}
|
||||
const bool have_symbol_{m_pattern->m_type_symbol_height > 0 && real_terminal_vector[i]
|
||||
&& (real_terminal_vector[i]->type() != ElementData::TTGeneric
|
||||
|| real_terminal_vector[i]->isLed())};
|
||||
if (have_symbol_) {
|
||||
used_zones_.append(Zone{m_pattern->m_type_symbol_y,
|
||||
m_pattern->m_type_symbol_y + m_pattern->m_type_symbol_height});
|
||||
}
|
||||
drawTerminalBody(painter, terminal_rect, used_zones_, !have_symbol_);
|
||||
if (colors_.stripe.isValid())
|
||||
{
|
||||
drawStripes(painter, terminal_rect, colors_.stripe);
|
||||
painter->setBrush(Qt::NoBrush);
|
||||
drawTerminalBody(painter, terminal_rect, used_zones_, !have_symbol_); //Draw again the outline and the details, hidden by stripes
|
||||
}
|
||||
painter->restore();
|
||||
|
||||
//Draw the symbol of the terminal type (fuse, ground, led...)
|
||||
if (m_pattern->m_type_symbol_height > 0 && real_terminal_vector[i])
|
||||
{
|
||||
painter->save();
|
||||
if (fill_color_.isValid()) {
|
||||
painter->setPen(text_color_);
|
||||
}
|
||||
drawTypeSymbol(painter,
|
||||
QRectF{0, m_pattern->m_type_symbol_y,
|
||||
terminal_rect.width(), m_pattern->m_type_symbol_height},
|
||||
real_terminal_vector[i]->type(),
|
||||
real_terminal_vector[i]->isLed());
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
//Draw the connections, above and under the terminal
|
||||
@@ -570,6 +1141,10 @@ void TerminalStripDrawer::paint(QPainter *painter)
|
||||
|
||||
//Draw text
|
||||
painter->save();
|
||||
if (fill_color_.isValid()) {
|
||||
painter->setPen(text_color_);
|
||||
}
|
||||
fitTextToCell(painter, terminal_rect.width(), terminals_text_orientation == Qt::Vertical);
|
||||
text_rect.setRect(0, terminals_text_y, terminal_rect.width(), terminals_text_height);
|
||||
if (terminals_text_orientation == Qt::Vertical)
|
||||
{
|
||||
@@ -579,8 +1154,14 @@ void TerminalStripDrawer::paint(QPainter *painter)
|
||||
}
|
||||
|
||||
const auto shared_real_terminal{real_terminal_vector[i]};
|
||||
if (shared_real_terminal && terminal_rect.width() >= 4.0) {
|
||||
drawMarkerTag(painter, text_rect, terminalNumber(shared_real_terminal->label()),
|
||||
terminals_text_option, terminals_text_orientation == Qt::Vertical,
|
||||
shared_real_terminal->type() == ElementData::TTSectional,
|
||||
tag_length_);
|
||||
}
|
||||
painter->drawText(text_rect,
|
||||
shared_real_terminal ? shared_real_terminal->label() : QLatin1String(),
|
||||
shared_real_terminal ? terminalNumber(shared_real_terminal->label()) : QString(),
|
||||
terminals_text_option);
|
||||
|
||||
if (m_preview_draw)
|
||||
@@ -594,6 +1175,10 @@ void TerminalStripDrawer::paint(QPainter *painter)
|
||||
//Draw xref
|
||||
xref_rect.setRect(0, xref_text_y, terminal_rect.width(), xref_text_height);
|
||||
painter->save();
|
||||
if (fill_color_.isValid()) {
|
||||
painter->setPen(text_color_);
|
||||
}
|
||||
fitTextToCell(painter, terminal_rect.width(), xref_text_orientation == Qt::Vertical);
|
||||
if (xref_text_orientation == Qt::Vertical)
|
||||
{
|
||||
painter->translate(xref_rect.bottomLeft());
|
||||
@@ -664,6 +1249,25 @@ void TerminalStripDrawer::paint(QPainter *painter)
|
||||
painter->translate(terminal_rect.width(),0);
|
||||
x_offset += terminal_rect.width();
|
||||
}
|
||||
|
||||
//Separator plate after the physical terminal : a thick line
|
||||
//on the full height of the terminal, after its last level
|
||||
const bool separator_after_ = std::any_of(real_terminal_vector.cbegin(), real_terminal_vector.cend(),
|
||||
[](const QSharedPointer<AbstractRealTerminalInterface> &real_t) {
|
||||
return real_t && real_t->hasSeparatorAfter();
|
||||
});
|
||||
if (separator_after_ && real_t_count > 0 && offset_ >= 0 && offset_ < 4)
|
||||
{
|
||||
const auto &rect_{m_pattern->m_terminal_rect.at(offset_)};
|
||||
painter->save();
|
||||
QPen separator_pen_{painter->pen()};
|
||||
separator_pen_.setColor(Qt::black);
|
||||
separator_pen_.setWidthF(3);
|
||||
separator_pen_.setCapStyle(Qt::FlatCap);
|
||||
painter->setPen(separator_pen_);
|
||||
painter->drawLine(QPointF{0, rect_.top() - 3}, QPointF{0, rect_.top() + rect_.height() + 3});
|
||||
painter->restore();
|
||||
}
|
||||
physical_index++;
|
||||
}
|
||||
painter->restore();
|
||||
@@ -694,6 +1298,10 @@ void TerminalStripDrawer::paint(QPainter *painter)
|
||||
|
||||
QRectF TerminalStripDrawer::boundingRect() const
|
||||
{
|
||||
if (m_pattern && m_draw_style != TerminalStripLayoutPattern::DrawStyle::Schematic) {
|
||||
return Realistic::boundingRect(m_strip.data(), *m_pattern);
|
||||
}
|
||||
|
||||
QRectF rect_{0, 0, width(), height()};
|
||||
|
||||
if (m_pattern)
|
||||
@@ -761,6 +1369,35 @@ void TerminalStripDrawer::setPreviewDraw(bool draw) {
|
||||
m_preview_draw = draw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripDrawer::setDrawStyle
|
||||
* Set how the terminals are drawn (schematic, realistic...).
|
||||
* The values used by each style come from the layout.
|
||||
*/
|
||||
void TerminalStripDrawer::setDrawStyle(TerminalStripLayoutPattern::DrawStyle style) {
|
||||
m_draw_style = style;
|
||||
}
|
||||
|
||||
TerminalStripLayoutPattern::DrawStyle TerminalStripDrawer::drawStyle() const {
|
||||
return m_draw_style;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalStripDrawer::xrefAt
|
||||
* @param pos : position in the coordinates of the item
|
||||
* @return the physical and real index of the terminal whose xref zone
|
||||
* (drawn by the last call of paint()) contains @a pos, or -1 if none.
|
||||
*/
|
||||
hoverTerminal TerminalStripDrawer::xrefAt(const QPointF &pos) const
|
||||
{
|
||||
for (const auto &xref_rect : m_xref_rects) {
|
||||
if (xref_rect.rect.contains(pos)) {
|
||||
return hoverTerminal{xref_rect.physical, xref_rect.real};
|
||||
}
|
||||
}
|
||||
return hoverTerminal{};
|
||||
}
|
||||
|
||||
void TerminalStripDrawer::setMouseHoverPos(const QPointF &pos)
|
||||
{
|
||||
m_last_mouse_pos_in_xrefs_rect = m_united_xref_text_rect.contains(m_mouse_hover_pos);
|
||||
@@ -848,7 +1485,7 @@ qreal TerminalStripDrawer::width() const
|
||||
break;
|
||||
}
|
||||
|
||||
width_ += m_pattern->m_terminal_rect[index_].width();
|
||||
width_ += terminalRect(*m_pattern, index_, real_terminal_vector[i]).width();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#ifndef TERMINALSTRIPDRAWER_H
|
||||
#define TERMINALSTRIPDRAWER_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QPointer>
|
||||
#include <QUuid>
|
||||
|
||||
@@ -77,6 +78,18 @@ namespace TerminalStripDrawer
|
||||
virtual QString cable() const { return QString(); }
|
||||
virtual QString cableWire() const { return QString(); }
|
||||
virtual bool isShield() const { return false; }
|
||||
//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 +126,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 +144,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,48 @@
|
||||
/*
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#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,31 @@ namespace TerminalStripDrawer
|
||||
return m_real ? m_real->isShield() : false;
|
||||
}
|
||||
|
||||
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,11 @@ namespace TerminalStripDrawer
|
||||
QString cable() const override;
|
||||
QString cableWire() const override;
|
||||
bool isShield() 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,7 +19,10 @@
|
||||
#include "terminalstrip.h"
|
||||
#include "../qetgraphicsitem/terminalelement.h"
|
||||
#include "physicalterminal.h"
|
||||
#include "terminalcolor.h"
|
||||
#include "terminalcatalog.h"
|
||||
#include "../qetgraphicsitem/conductor.h"
|
||||
#include "../qetinformation.h"
|
||||
|
||||
/**
|
||||
* @brief RealTerminal
|
||||
@@ -30,6 +33,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 +194,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 +263,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();
|
||||
@@ -161,8 +292,8 @@ QString RealTerminal::Xref() const
|
||||
* 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();
|
||||
if (hasData()) {
|
||||
return terminalData().m_informations.value(cableInfoKey()).toString();
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
@@ -172,8 +303,8 @@ QString RealTerminal::cable() const {
|
||||
* @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();
|
||||
if (hasData()) {
|
||||
return terminalData().m_informations.value(cableWireInfoKey()).toString();
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
@@ -184,9 +315,156 @@ 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::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 +489,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 +501,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 +513,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 +571,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;
|
||||
@@ -55,6 +57,12 @@ class RealTerminal
|
||||
|
||||
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;
|
||||
|
||||
@@ -66,11 +74,28 @@ class RealTerminal
|
||||
QString cable() const;
|
||||
QString cableWire() const;
|
||||
bool isShield() 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"); }
|
||||
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 +114,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;
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
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_)
|
||||
{
|
||||
auto by_label_ = [](const QSharedPointer<RealTerminal> &a_,
|
||||
const QSharedPointer<RealTerminal> &b_) {
|
||||
return QETUtils::naturalLessThan(a_->label(), b_->label());
|
||||
};
|
||||
std::sort(it_.value().free_terminals.begin(),
|
||||
it_.value().free_terminals.end(), by_label_);
|
||||
std::sort(it_.value().placed_terminals.begin(),
|
||||
it_.value().placed_terminals.end(),
|
||||
[&by_label_](const auto &a_, const auto &b_) {
|
||||
return by_label_(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,6 +18,7 @@
|
||||
#ifndef MODELTERMINALDATA_H
|
||||
#define MODELTERMINALDATA_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QString>
|
||||
#include "../../qetgraphicsitem/element.h"
|
||||
#include "../realterminal.h"
|
||||
@@ -37,11 +38,18 @@ struct modelRealTerminalData
|
||||
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;
|
||||
@@ -55,7 +63,14 @@ struct modelRealTerminalData
|
||||
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,21 @@
|
||||
#include "../UndoCommand/changeterminalstripcolor.h"
|
||||
#include "../physicalterminal.h"
|
||||
#include "../terminalstripbridge.h"
|
||||
#include "../terminalcolor.h"
|
||||
#include "../terminalcatalog.h"
|
||||
#include "../realterminal.h"
|
||||
#include "../../qetinformation.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 +62,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);
|
||||
@@ -116,6 +138,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()};
|
||||
@@ -188,12 +211,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 +226,48 @@ 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);
|
||||
}
|
||||
};
|
||||
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_));
|
||||
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);
|
||||
}
|
||||
};
|
||||
set_info(RealTerminal::cableInfoKey(), data_.cable_);
|
||||
set_info(RealTerminal::cableWireInfoKey(), data_.cable_wire);
|
||||
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 +291,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 +350,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 +701,17 @@ 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, 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::Color
|
||||
|| column_ == TerminalStripModel::Section
|
||||
|| column_ == TerminalStripModel::Width;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -730,7 +795,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 +1049,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));
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ class TerminalStripEditor : public QWidget
|
||||
void pasteFromClipboard();
|
||||
void clearSelectedTexts();
|
||||
void clear();
|
||||
void setManufacturerFilter(const QString &manufacturer);
|
||||
void spanMultiLevelTerminals();
|
||||
void selectionChanged();
|
||||
QSize setUpBridgeCellWidth();
|
||||
@@ -73,6 +74,7 @@ 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;
|
||||
|
||||
@@ -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,12 +16,17 @@
|
||||
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>
|
||||
@@ -47,10 +52,46 @@ const int TYPE_CELL = 11;
|
||||
const int FUNCTION_CELL = 12;
|
||||
const int LED_CELL = 13;
|
||||
const int SHIELD_CELL = 14;
|
||||
const int COLOR_CELL = 15;
|
||||
const int SECTION_CELL = 16;
|
||||
const int WIDTH_CELL = 17;
|
||||
const int SEPARATOR_CELL = 18;
|
||||
|
||||
const int COLUMN_COUNT = 15;
|
||||
const int COLUMN_COUNT = 19;
|
||||
|
||||
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
|
||||
@@ -95,6 +136,10 @@ TerminalStripModel::Column TerminalStripModel::columnTypeForIndex(const QModelIn
|
||||
case 12 : return Function;
|
||||
case 13 : return Led;
|
||||
case 14 : return Shield;
|
||||
case 15 : return Color;
|
||||
case 16 : return Section;
|
||||
case 17 : return Width;
|
||||
case 18 : return Separator;
|
||||
default : return Invalid;
|
||||
}
|
||||
}
|
||||
@@ -167,11 +212,20 @@ 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 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 +235,18 @@ 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 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 +257,32 @@ QVariant TerminalStripModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
return mrtd.shield_ ? Qt::Checked : Qt::Unchecked;
|
||||
}
|
||||
else if (role == Qt::CheckStateRole &&
|
||||
index.column() == SEPARATOR_CELL)
|
||||
{
|
||||
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 )
|
||||
{
|
||||
if (m_modified_cell.contains(mrtd.element_) &&
|
||||
m_modified_cell.value(mrtd.element_).at(index.column()))
|
||||
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 ||
|
||||
@@ -279,23 +359,97 @@ bool TerminalStripModel::setData(const QModelIndex &index, const QVariant &value
|
||||
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;
|
||||
@@ -326,6 +480,10 @@ QVariant TerminalStripModel::headerData(int section, Qt::Orientation orientation
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -340,9 +498,10 @@ Qt::ItemFlags TerminalStripModel::flags(const QModelIndex &index) const
|
||||
|
||||
auto c = index.column();
|
||||
if (c == LABEL_CELL || c == TYPE_CELL || c == FUNCTION_CELL
|
||||
|| c == CABLE_CELL || c == CABLE_WIRE_CELL)
|
||||
|| c == CABLE_CELL || c == CABLE_WIRE_CELL
|
||||
|| c == COLOR_CELL || c == SECTION_CELL || c == WIDTH_CELL)
|
||||
flags = flags | Qt::ItemIsEditable;
|
||||
if (c == LED_CELL || c == SHIELD_CELL) {
|
||||
if (c == LED_CELL || c == SHIELD_CELL || c == SEPARATOR_CELL) {
|
||||
flags = flags | Qt::ItemIsUserCheckable;
|
||||
}
|
||||
return flags;
|
||||
@@ -364,7 +523,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));
|
||||
}
|
||||
@@ -819,10 +978,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 +1026,155 @@ 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;
|
||||
}
|
||||
|
||||
return QStyledItemDelegate::createEditor(parent, option, index);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
QStyledItemDelegate::setEditorData(editor, index);
|
||||
}
|
||||
|
||||
void TerminalStripModelDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
|
||||
{
|
||||
if (index.isValid())
|
||||
@@ -870,6 +1191,27 @@ 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 {
|
||||
QStyledItemDelegate::setModelData(editor, model, index);
|
||||
}
|
||||
|
||||
@@ -62,12 +62,20 @@ class TerminalStripModel : public QAbstractTableModel
|
||||
Function = 12,
|
||||
Led = 13,
|
||||
Shield = 14,
|
||||
Color = 15,
|
||||
Section = 16,
|
||||
Width = 17,
|
||||
Separator = 18,
|
||||
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);
|
||||
@@ -99,7 +107,8 @@ class TerminalStripModel : public QAbstractTableModel
|
||||
|
||||
private:
|
||||
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;
|
||||
struct BridgePixmap
|
||||
{
|
||||
@@ -124,12 +133,21 @@ 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;
|
||||
|
||||
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()
|
||||
|
||||
@@ -69,6 +69,8 @@ namespace autonum
|
||||
{
|
||||
/// border_and_titleblock.folio(), folioIndex(), folioTotal()...
|
||||
QString folio;
|
||||
/// the folio's title, %T
|
||||
QString folio_title;
|
||||
int folio_index = 0;
|
||||
int folio_total = 0;
|
||||
QString plant;
|
||||
|
||||
@@ -92,12 +92,6 @@
|
||||
</item>
|
||||
<item row="2" column="4">
|
||||
<widget class="QSpinBox" name="m_conductor_increase_sb">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>55</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Increment: the value added to the counter each time a new number is dialled</string>
|
||||
</property>
|
||||
@@ -179,12 +173,6 @@
|
||||
</item>
|
||||
<item row="3" column="4">
|
||||
<widget class="QSpinBox" name="m_element_increase_sb">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>55</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Increment: the value added to the counter each time a new number is dialled</string>
|
||||
</property>
|
||||
@@ -255,12 +243,6 @@
|
||||
</item>
|
||||
<item row="4" column="4">
|
||||
<widget class="QSpinBox" name="m_folio_increase_sb">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>55</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Increment: the value added to the counter each time a new number is dialled</string>
|
||||
</property>
|
||||
|
||||
@@ -321,8 +321,8 @@ TitleBlockProperties BorderTitleBlock::localTitleBlock() const
|
||||
/**
|
||||
@brief BorderTitleBlock::current
|
||||
@return the properties of the titleblock: for a folio placed in a
|
||||
project the project's store holds them (DB-ACCESSORS-PLAN.md stage
|
||||
4.4), for any other this object's own copy.
|
||||
project the project's store holds them, for any other this object's
|
||||
own copy.
|
||||
*/
|
||||
TitleBlockProperties BorderTitleBlock::current() const
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user