Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1793dcb9a8 | |||
| cc7aaf3538 | |||
| a6285c7ef7 | |||
| f87381a910 | |||
| bd5cff4211 | |||
| 296f142036 | |||
| 4d991d0419 | |||
| a85bd6a49b | |||
| ef8e41a5c3 | |||
| 6f80da66d5 | |||
| a065867502 | |||
| b98d41357a | |||
| b2f4ef5d25 | |||
| bd8470afa8 | |||
| 7c5f8c6a4b | |||
| 6b27043f33 | |||
| ed5a510592 | |||
| 3abf187688 | |||
| 825a1a594d | |||
| 38e1b3fe49 | |||
| fd3e213222 | |||
| ef4e186e42 | |||
| 975ab31cc1 | |||
| 2699a0aec8 | |||
| 76ff5c4dd9 | |||
| b395527915 | |||
| 7741bb33b2 | |||
| f02b576cb8 | |||
| e0ef24fe68 | |||
| 70c665199f | |||
| 6b577ee757 | |||
| 74e878207a | |||
| 3e66d403c8 | |||
| a7fa4dfdcf | |||
| 1e1039dd32 | |||
| b98589d3c7 | |||
| f997550e07 | |||
| d9f148ab2f | |||
| 9fdd7b5fc0 | |||
| e410896f5d | |||
| 10bf0411bb | |||
| 823468826d | |||
| b659c4acad | |||
| c69e5747a1 | |||
| 899f10533d | |||
| 5b2fdaeb00 | |||
| 5027ffda9b | |||
| 3ca5d4ab29 | |||
| e3cb994e0e | |||
| 0d08a4e265 | |||
| ae6b992ae4 | |||
| b72794bea1 | |||
| 11357eb504 | |||
| 85346305e5 |
@@ -11,356 +11,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
# =============================================================================
|
||||
# Job 1: Qt5 build (stable track)
|
||||
# =============================================================================
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install MSYS2
|
||||
uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: UCRT64
|
||||
update: true
|
||||
cache: true
|
||||
install: >-
|
||||
git
|
||||
mingw-w64-ucrt-x86_64-ccache
|
||||
mingw-w64-ucrt-x86_64-gcc
|
||||
mingw-w64-ucrt-x86_64-cmake
|
||||
mingw-w64-ucrt-x86_64-ninja
|
||||
mingw-w64-ucrt-x86_64-qt5-base
|
||||
mingw-w64-ucrt-x86_64-qt5-svg
|
||||
mingw-w64-ucrt-x86_64-qt5-tools
|
||||
mingw-w64-ucrt-x86_64-qt5-translations
|
||||
mingw-w64-ucrt-x86_64-qt5-pdf
|
||||
mingw-w64-ucrt-x86_64-sqlite3
|
||||
mingw-w64-ucrt-x86_64-pkg-config
|
||||
mingw-w64-ucrt-x86_64-kwidgetsaddons
|
||||
mingw-w64-ucrt-x86_64-kcoreaddons
|
||||
mingw-w64-ucrt-x86_64-extra-cmake-modules
|
||||
mingw-w64-ucrt-x86_64-nsis
|
||||
mingw-w64-ucrt-x86_64-angleproject
|
||||
|
||||
- name: Cache ccache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: C:\Users\runneradmin\AppData\Local\ccache
|
||||
key: ccache-windows-${{ github.ref_name }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
ccache-windows-${{ github.ref_name }}-
|
||||
ccache-windows-
|
||||
|
||||
- name: Configure ccache
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
/ucrt64/bin/ccache --set-config=max_size=500M
|
||||
/ucrt64/bin/ccache --set-config=compression=true
|
||||
/ucrt64/bin/ccache -z
|
||||
echo "=== ccache config ==="
|
||||
/ucrt64/bin/ccache -p
|
||||
|
||||
- name: Patch NSIS Welcome page — fix title font size
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
WELCOME_NSH=$(find /ucrt64 -path "*/Modern UI 2/Pages/Welcome.nsh" | head -1)
|
||||
if [ -z "$WELCOME_NSH" ]; then
|
||||
echo "WARNING: Welcome.nsh not found, skipping font patch"
|
||||
else
|
||||
echo "Patching: $WELCOME_NSH"
|
||||
sed -i '/WelcomePage\.Title\.Font/s/"[0-9]\+" "700"/"10" "700"/' "$WELCOME_NSH"
|
||||
grep 'WelcomePage.Title.Font' "$WELCOME_NSH"
|
||||
echo " OK font size patched to 10"
|
||||
fi
|
||||
|
||||
FINISH_NSH=$(find /ucrt64 -path "*/Modern UI 2/Pages/Finish.nsh" | head -1)
|
||||
if [ -z "$FINISH_NSH" ]; then
|
||||
echo "WARNING: Finish.nsh not found, skipping font patch"
|
||||
else
|
||||
echo "Patching: $FINISH_NSH"
|
||||
sed -i '/FinishPage\.Title\.Font/s/"[0-9]\+" "700"/"10" "700"/' "$FINISH_NSH"
|
||||
grep 'FinishPage.Title.Font' "$FINISH_NSH"
|
||||
echo " OK font size patched to 10"
|
||||
fi
|
||||
|
||||
- name: Force Qt5 — remove Qt6 cmake + tools
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf /ucrt64/lib/cmake/Qt6
|
||||
pacman -R --noconfirm mingw-w64-ucrt-x86_64-qt6-tools 2>/dev/null || true
|
||||
echo "=== windeployqt binaries ==="
|
||||
ls /ucrt64/bin/windeployqt* || echo "NO windeployqt found!"
|
||||
|
||||
- name: Build with cmake
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
mkdir build && cd build
|
||||
NPROC=$(nproc)
|
||||
echo "Available CPUs: $NPROC"
|
||||
|
||||
cmake -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_PREFIX_PATH=/ucrt64 \
|
||||
-DQt5_DIR=/ucrt64/lib/cmake/Qt5 \
|
||||
-DQT_VERSION_MAJOR=5 \
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_Qt6=ON \
|
||||
-DPACKAGE_TESTS=OFF \
|
||||
-DCMAKE_POLICY_DEFAULT_CMP0077=NEW \
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
|
||||
-DQET_EXPORT_PROJECT_DB=ON \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=/ucrt64/bin/ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=/ucrt64/bin/ccache \
|
||||
-DSQLite3_INCLUDE_DIR=/ucrt64/include \
|
||||
-DSQLite3_LIBRARY=/ucrt64/lib/libsqlite3.dll.a \
|
||||
..
|
||||
ninja -j"$NPROC"
|
||||
|
||||
- name: Show ccache stats
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
echo "=== ccache statistics ==="
|
||||
/ucrt64/bin/ccache -s
|
||||
|
||||
- name: Verify exe was built
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
EXE=$(find "$GITHUB_WORKSPACE/build" -maxdepth 3 -iname "qelectrotech.exe" | head -1)
|
||||
if [ -z "$EXE" ]; then
|
||||
echo "ERROR: no qelectrotech.exe found in build/"
|
||||
find "$GITHUB_WORKSPACE/build" -maxdepth 3 -name "*.exe" || true
|
||||
exit 1
|
||||
fi
|
||||
SIZE=$(stat -c%s "$EXE")
|
||||
echo "Exe found: $EXE ($SIZE bytes)"
|
||||
[ "$SIZE" -gt 100000 ] || { echo "ERROR: exe too small"; exit 1; }
|
||||
|
||||
- name: Deploy — copy exe + windeployqt + DLLs
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
NSIS_ROOT="$GITHUB_WORKSPACE/nsis_root"
|
||||
FILES="$NSIS_ROOT/files"
|
||||
BIN="$FILES/bin"
|
||||
mkdir -p "$BIN"
|
||||
|
||||
EXE=$(find "$GITHUB_WORKSPACE/build" -maxdepth 3 -iname "qelectrotech.exe" | head -1)
|
||||
echo "Copying exe: $EXE -> $BIN/QElectroTech.exe"
|
||||
cp "$EXE" "$BIN/QElectroTech.exe"
|
||||
|
||||
cd "$BIN"
|
||||
/ucrt64/bin/windeployqt-qt5 \
|
||||
--release \
|
||||
--no-translations \
|
||||
--no-compiler-runtime \
|
||||
./QElectroTech.exe || true
|
||||
|
||||
echo "=== 3-pass transitive DLL scan ==="
|
||||
set +e
|
||||
for PASS in 1 2 3; do
|
||||
echo "-- Pass $PASS --"
|
||||
for bin_file in "$BIN"/*.dll "$BIN"/*.exe "$BIN"/sqldrivers/*.dll "$BIN"/platforms/*.dll "$BIN"/imageformats/*.dll; do
|
||||
[ -f "$bin_file" ] || continue
|
||||
while IFS= read -r line; do
|
||||
dll_path=$(echo "$line" | awk '{print $3}')
|
||||
[ -f "$dll_path" ] || continue
|
||||
dll_name=$(basename "$dll_path")
|
||||
dst="$BIN/$dll_name"
|
||||
if [ ! -f "$dst" ]; then
|
||||
cp "$dll_path" "$dst"
|
||||
echo " Copied (pass $PASS): $dll_name"
|
||||
fi
|
||||
done < <(ldd "$bin_file" 2>/dev/null | grep -i '/ucrt64/bin/')
|
||||
done
|
||||
done
|
||||
set -e
|
||||
|
||||
DLL_COUNT=$(find "$BIN" -name "*.dll" | wc -l)
|
||||
echo "=== $DLL_COUNT DLLs present after scan ==="
|
||||
ls -lh "$BIN/QElectroTech.exe" || { echo "ERROR: exe missing from bin/"; exit 1; }
|
||||
[ "$DLL_COUNT" -gt 5 ] || { echo "ERROR: too few DLLs"; exit 1; }
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
|
||||
cp /ucrt64/bin/libgcc_s_seh-1.dll "$BIN/"
|
||||
cp /ucrt64/bin/libstdc++-6.dll "$BIN/"
|
||||
cp /ucrt64/bin/libwinpthread-1.dll "$BIN/"
|
||||
SQLITE=$(find /ucrt64/bin -name "libsqlite3*.dll" | head -1)
|
||||
if [ -n "$SQLITE" ]; then
|
||||
cp "$SQLITE" "$BIN/"
|
||||
echo "SQLite3 copied: $(basename $SQLITE)"
|
||||
else
|
||||
echo "WARNING: libsqlite3 not found in /ucrt64/bin/"
|
||||
fi
|
||||
|
||||
cp "$GITHUB_WORKSPACE/build-aux/windows/QET64.nsi" "$NSIS_ROOT/"
|
||||
cp "$GITHUB_WORKSPACE/build-aux/windows/lang_extra.nsh" "$NSIS_ROOT/"
|
||||
cp "$GITHUB_WORKSPACE/build-aux/windows/lang_extra_fr.nsh" "$NSIS_ROOT/"
|
||||
cp "$GITHUB_WORKSPACE/build-aux/windows/lang_extra_missing.nsh" "$NSIS_ROOT/"
|
||||
cp -r "$GITHUB_WORKSPACE/build-aux/windows/nsis_base/." "$NSIS_ROOT/"
|
||||
|
||||
curl -fsSL \
|
||||
"https://raw.githubusercontent.com/qelectrotech/qelectrotech-source-mirror/refs/heads/master/misc/Lancer%20QET.bat" \
|
||||
-o "$FILES/Lancer QET.bat"
|
||||
cp -r "$GITHUB_WORKSPACE/elements" "$FILES/elements" || true
|
||||
cp -r "$GITHUB_WORKSPACE/titleblocks" "$FILES/titleblocks" || true
|
||||
cp -r "$GITHUB_WORKSPACE/examples" "$FILES/examples" || true
|
||||
cp -r "$GITHUB_WORKSPACE/fonts" "$FILES/fonts" || true
|
||||
|
||||
cp -r "$GITHUB_WORKSPACE/lang" "$FILES/lang" || true
|
||||
find "$GITHUB_WORKSPACE/build" -name "*.qm" -exec cp {} "$FILES/lang/" \; 2>/dev/null || true
|
||||
echo "=== .qm files in files/lang/ ==="
|
||||
ls "$FILES/lang/"*.qm 2>/dev/null | wc -l || echo "0 .qm files"
|
||||
|
||||
for f in LICENSE ChangeLog CREDIT README ELEMENTS.LICENSE; do
|
||||
cp "$GITHUB_WORKSPACE/$f" "$FILES/$f" 2>/dev/null || true
|
||||
done
|
||||
|
||||
echo "=== Verification of key files in files/ ==="
|
||||
for f in LICENSE ChangeLog CREDIT README ELEMENTS.LICENSE \
|
||||
qet_uninstall_file_associations.reg register_filetypes.bat "Lancer QET.bat"; do
|
||||
[ -f "$FILES/$f" ] \
|
||||
&& echo " OK : $f" \
|
||||
|| echo " MISSING: $f"
|
||||
done
|
||||
for d in ico elements lang titleblocks fonts examples bin; do
|
||||
[ -d "$FILES/$d" ] \
|
||||
&& echo " OK : $d/" \
|
||||
|| echo " MISSING: $d/"
|
||||
done
|
||||
|
||||
- name: Extract version for installer name
|
||||
shell: msys2 {0}
|
||||
id: qet_version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
GITCOMMIT=$(git -C "$GITHUB_WORKSPACE" rev-parse --short HEAD)
|
||||
|
||||
A=$(git -C "$GITHUB_WORKSPACE" rev-list HEAD --count)
|
||||
HEAD=$(( A + 473 ))
|
||||
|
||||
VERSION=$(grep 'return QVersionNumber{' "$GITHUB_WORKSPACE/sources/qetversion.cpp" \
|
||||
| head -1 \
|
||||
| awk -F '{' '{ print $2 }' \
|
||||
| awk -F '}' '{ print $1 }' \
|
||||
| sed -e 's/,/./g' -e 's/ //g')
|
||||
[ -z "$VERSION" ] && VERSION="dev"
|
||||
|
||||
FULL_VERSION="${VERSION}-r${HEAD}-${GITCOMMIT}_x86_64-win64"
|
||||
echo "version=$FULL_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "base_version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "gitcommit=$GITCOMMIT" >> "$GITHUB_OUTPUT"
|
||||
echo "head=$HEAD" >> "$GITHUB_OUTPUT"
|
||||
echo "VERSION : $VERSION"
|
||||
echo "GITCOMMIT : $GITCOMMIT"
|
||||
echo "HEAD (rev) : $HEAD"
|
||||
echo "FULL : $FULL_VERSION"
|
||||
|
||||
- name: Patch QET64.nsi — version + exe name + absolute paths
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${{ steps.qet_version.outputs.version }}"
|
||||
NSI="$GITHUB_WORKSPACE/nsis_root/QET64.nsi"
|
||||
FILES_WIN=$(cygpath -w "$GITHUB_WORKSPACE/nsis_root/files")
|
||||
SCRIPT="$GITHUB_WORKSPACE/build-aux/windows/patch_nsi.py"
|
||||
|
||||
python3 "$SCRIPT" "$NSI" "$VERSION" "$FILES_WIN"
|
||||
|
||||
echo "=== Verification ==="
|
||||
grep 'SOFT_VERSION' "$NSI" | head -1
|
||||
grep -m2 'nsis_root' "$NSI" | head -2
|
||||
echo "=== Contents of nsis_root/files/ ==="
|
||||
ls "$GITHUB_WORKSPACE/nsis_root/files/"
|
||||
|
||||
- name: Build NSIS installer
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
NSIS_ROOT="$GITHUB_WORKSPACE/nsis_root"
|
||||
cd "$NSIS_ROOT"
|
||||
echo "=== CWD : $(pwd) ==="
|
||||
MSYS2_ARG_CONV_EXCL="*" makensis /V4 QET64.nsi
|
||||
RC=$?
|
||||
echo "=== Contents of nsis_root after makensis ==="
|
||||
ls "$NSIS_ROOT/"
|
||||
[ $RC -eq 0 ] || { echo "ERROR: makensis failed (exit $RC)"; exit 1; }
|
||||
|
||||
- name: Move installer to dist/
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$GITHUB_WORKSPACE/dist"
|
||||
INSTALLER=$(find "$GITHUB_WORKSPACE/nsis_root" -maxdepth 1 -iname "installer_*.exe" | head -1)
|
||||
if [ -z "$INSTALLER" ]; then
|
||||
echo "ERROR: no installer .exe found in nsis_root/"
|
||||
ls "$GITHUB_WORKSPACE/nsis_root/"
|
||||
exit 1
|
||||
fi
|
||||
echo "Moving: $INSTALLER -> dist/"
|
||||
mv "$INSTALLER" "$GITHUB_WORKSPACE/dist/"
|
||||
|
||||
- name: Upload build logs on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: build-logs
|
||||
path: |
|
||||
build/CMakeFiles/*.log
|
||||
nsis_root/files/bin/
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Zip portable (readytouse)
|
||||
id: zip_portable
|
||||
shell: pwsh
|
||||
run: |
|
||||
$version = "${{ steps.qet_version.outputs.base_version }}"
|
||||
$head = "${{ steps.qet_version.outputs.head }}"
|
||||
$zipName = "qelectrotech-${version}+git${head}-x86-win64-readytouse.zip"
|
||||
$src = "$env:GITHUB_WORKSPACE\nsis_root\files"
|
||||
$dst = "$env:GITHUB_WORKSPACE\dist\$zipName"
|
||||
$7z = "C:\Program Files\7-Zip\7z.exe"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path "$env:GITHUB_WORKSPACE\dist" | Out-Null
|
||||
& $7z a -tzip -mx=5 -mmt=on $dst "$src\*"
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
$sizeMB = [math]::Round((Get-Item $dst).Length / 1MB, 1)
|
||||
Write-Output "ZIP created: $zipName ($sizeMB MB)"
|
||||
"zip_name=$zipName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
|
||||
- name: Upload portable (files/ without installer)
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: qelectrotech-${{ steps.qet_version.outputs.base_version }}+git${{ steps.qet_version.outputs.head }}-x86-win64-readytouse
|
||||
path: dist/${{ steps.zip_portable.outputs.zip_name }}
|
||||
retention-days: 40
|
||||
|
||||
- name: Upload NSIS installer
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: qelectrotech-windows-installer
|
||||
path: dist/Installer_*.exe
|
||||
retention-days: 40
|
||||
|
||||
- name: Upload portable (nom fixe pour le workflow MSI)
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: qelectrotech-windows-portable
|
||||
path: nsis_root/files/
|
||||
retention-days: 40
|
||||
|
||||
# =============================================================================
|
||||
# Job 2: Qt6 build (EXPERIMENTAL track)
|
||||
# Job 1: Windows build (Qt6/KF6 — sole track since the Qt5 track was removed)
|
||||
#
|
||||
# Version label: the C++ source (sources/qetversion.cpp) intentionally stays
|
||||
# Qt-agnostic — QT_VERSION-based detection inside the binary proved unreliable
|
||||
@@ -368,6 +19,9 @@ jobs:
|
||||
# certainty which Qt major version it configured (-DQT_VERSION_MAJOR=6), so it
|
||||
# is safe to label the *package* "0.200.1" at this level, without touching
|
||||
# QetVersion::currentVersion() or making the binary self-detect its Qt build.
|
||||
#
|
||||
# Job id kept as "build-windows-qt6" (not renamed to "build-windows") in case
|
||||
# branch protection / required status checks reference this exact job name.
|
||||
# =============================================================================
|
||||
build-windows-qt6:
|
||||
runs-on: windows-latest
|
||||
@@ -598,8 +252,8 @@ jobs:
|
||||
|
||||
# Deliberately NOT parsed from sources/qetversion.cpp: the CI job
|
||||
# already knows for certain it configured Qt6 (-DQT_VERSION_MAJOR=6),
|
||||
# so the "0.200.1" experimental-track label is set here explicitly
|
||||
# rather than relying on in-binary Qt version detection.
|
||||
# so the "0.200.1" version label is set here explicitly rather than
|
||||
# relying on in-binary Qt version detection.
|
||||
VERSION="0.200.1"
|
||||
|
||||
FULL_VERSION="${VERSION}-qt6-r${HEAD}-${GITCOMMIT}_x86_64-win64"
|
||||
@@ -708,43 +362,47 @@ jobs:
|
||||
retention-days: 40
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 3 : Publie les assets nightly (exe + zip, Qt5 et Qt6) sur la release
|
||||
# Job 2 : Publie les assets nightly (exe + zip) sur la release
|
||||
# Ne tourne que sur push master (pas sur les PRs)
|
||||
# ---------------------------------------------------------------------------
|
||||
publish-nightly-assets:
|
||||
needs: [build-windows, build-windows-qt6]
|
||||
needs: [build-windows-qt6]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download installer artifacts (Qt5 + Qt6)
|
||||
- name: Download installer artifact
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: qelectrotech-windows-installer*
|
||||
path: downloaded/installer/
|
||||
merge-multiple: true
|
||||
|
||||
- name: Download portable artifacts (Qt5 + Qt6)
|
||||
- name: Download portable artifact
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: qelectrotech-*-readytouse
|
||||
path: downloaded/portable/
|
||||
merge-multiple: true
|
||||
|
||||
- name: Delete old nightly assets (.exe and .zip)
|
||||
# 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)
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
gh release view nightly --repo "$REPO" --json assets \
|
||||
--jq '.assets[] | select(.name | test("\\.(exe|zip)$")) | .name' \
|
||||
--jq '.assets[] | select(.name | test("qt6")) | select(.name | test("\\.(exe|zip)$")) | .name' \
|
||||
| while read -r name; do
|
||||
echo "Deleting old asset: $name"
|
||||
echo "Deleting old Qt6 asset: $name"
|
||||
gh release delete-asset nightly "$name" --repo "$REPO" --yes
|
||||
done
|
||||
echo "Old .exe and .zip assets deleted."
|
||||
echo "Old Qt6 .exe and .zip assets deleted (legacy Qt5 assets left untouched)."
|
||||
|
||||
- name: Update nightly release
|
||||
uses: softprops/action-gh-release@v3
|
||||
@@ -763,9 +421,7 @@ 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).
|
||||
|
||||
> 🧪 **Try Qt6 — soon the only track.** Files tagged `-qt6-` are built against Qt6. The Qt5
|
||||
> builds above will be removed from Windows CI soon; please switch and test Qt6 now — report
|
||||
> any issue clearly labelled "Qt6".
|
||||
> 🗄️ 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: |
|
||||
@@ -774,4 +430,4 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# GitHub Pages is generated and deployed by windows-msi.yml
|
||||
# after the MSI upload, so that all URLs (exe/zip/msi, Qt5+Qt6) are known.
|
||||
# after the MSI upload, so that all URLs (exe/zip/msi) are known.
|
||||
|
||||
@@ -25,18 +25,17 @@ jobs:
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# Single-entry matrix kept on purpose (rather than flattening the job):
|
||||
# matrix.flavor is used as part of the MSI ProductCode seed, so changing
|
||||
# it would generate a new ProductCode and break upgrade detection for
|
||||
# existing installs. Keeping "qt6" here preserves continuity.
|
||||
matrix:
|
||||
include:
|
||||
- flavor: qt5
|
||||
portable_artifact: qelectrotech-windows-portable
|
||||
version_source: qetversion # parsed from sources/qetversion.cpp
|
||||
label_suffix: ""
|
||||
experimental: false
|
||||
- flavor: qt6
|
||||
portable_artifact: qelectrotech-windows-portable-qt6
|
||||
version_source: hardcoded # see note in "Extract version" step
|
||||
label_suffix: "-qt6"
|
||||
experimental: true
|
||||
experimental: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -44,7 +43,6 @@ jobs:
|
||||
id-token: write # Required by SignPath
|
||||
|
||||
outputs:
|
||||
qt5_msi: ${{ steps.export.outputs.msi_name_qt5 }}
|
||||
qt6_msi: ${{ steps.export.outputs.msi_name_qt6 }}
|
||||
|
||||
steps:
|
||||
@@ -72,14 +70,12 @@ jobs:
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# 3. Extract version
|
||||
# Qt5: parsed from sources/qetversion.cpp (single source of truth
|
||||
# for the software's own reported version).
|
||||
# Qt6: hardcoded "0.200.1" here — deliberately NOT parsed from the
|
||||
# binary/source, since Qt-version self-detection inside the
|
||||
# compiled code proved unreliable (see commit e1aa65f). The CI
|
||||
# matrix entry already knows for certain which flavor it is
|
||||
# packaging, so the experimental-track label is set explicitly
|
||||
# at the packaging level instead.
|
||||
# packaging, so the version label is set explicitly at the
|
||||
# packaging level instead.
|
||||
# ----------------------------------------------------------------
|
||||
- name: Extract version
|
||||
id: version
|
||||
@@ -125,10 +121,9 @@ jobs:
|
||||
Write-Host "Version MSI : $verMsi"
|
||||
Write-Host "Version display : $verDisplay"
|
||||
|
||||
# Qt platform argument: only Qt5 needs the QTBUG-83161 font-rendering
|
||||
# workaround (GDI backend). Qt6 uses DirectWrite by default and does
|
||||
# not need it. Computed once here so both the bundled "Lancer QET.bat"
|
||||
# and the MSI shortcuts (wix build -d QtPlatformArgs=...) stay in sync.
|
||||
# Qt platform argument: kept from the Qt5 era (QTBUG-83161
|
||||
# font-rendering workaround, GDI backend). Qt6 uses DirectWrite by
|
||||
# default and does not need it, so this always resolves empty now.
|
||||
if ("${{ matrix.flavor }}" -eq "qt5") {
|
||||
$qtArgs = "-platform windows:fontengine=freetype"
|
||||
} else {
|
||||
@@ -228,10 +223,10 @@ jobs:
|
||||
Write-Host "Lancer QET.bat replaced for MSI installation (qtArgs: '$qtArgs')."
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# 9. Build the MSI (unsigned)
|
||||
# Qt6 (experimental) uses a distinct ProductCode seed so it never
|
||||
# collides with / upgrades over the Qt5 MSI — they must be able to
|
||||
# coexist as clearly separate installs.
|
||||
# 9. Build the MSI (unsigned at this stage — signing happens below)
|
||||
# Qt6 keeps its own ProductCode seed (distinct from the retired Qt5
|
||||
# MSI), so a machine that still has the old Qt5 MSI installed gets a
|
||||
# separate, coexisting install rather than an unexpected upgrade.
|
||||
# ----------------------------------------------------------------
|
||||
- name: Build MSI
|
||||
shell: pwsh
|
||||
@@ -289,12 +284,13 @@ jobs:
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
||||
# Qt6 stays on the unsigned/experimental track (no signing request for
|
||||
# this flavor), and forks never have the SignPath secrets, so guard on
|
||||
# both: only qt5, and only in the upstream repo.
|
||||
# Qt6 is now signed too (previously excluded while Qt5 was the stable
|
||||
# track and Qt6 was experimental-only). The remaining guard is the fork
|
||||
# check: forks never have the SignPath secrets, and a fork-originated
|
||||
# PR/run must never attempt a signing request.
|
||||
# (cf. DieterMayerOSS:fix/msi-signing-fork-guard, d3f60c88)
|
||||
- name: Sign MSI via SignPath
|
||||
if: matrix.flavor == 'qt5' && github.repository == 'qelectrotech/qelectrotech-source-mirror'
|
||||
if: github.repository == 'qelectrotech/qelectrotech-source-mirror'
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
@@ -314,15 +310,14 @@ jobs:
|
||||
retention-days: 40
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Delete old nightly .msi asset for this flavor
|
||||
- name: Delete old nightly .msi asset
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
$pattern = if ("${{ matrix.flavor }}" -eq "qt6") { "-qt6.*\\.msi$" } else { "\\.msi$" }
|
||||
$pattern = "-qt6.*\\.msi$"
|
||||
$names = gh release view nightly --repo $env:REPO --json assets --jq ".assets[] | select(.name | test(`"$pattern`")) | .name"
|
||||
foreach ($name in ($names -split "`n" | Where-Object { $_ })) {
|
||||
if ("${{ matrix.flavor }}" -eq "qt5" -and $name -match "-qt6") { continue }
|
||||
Write-Host "Deleting old asset: $name"
|
||||
gh release delete-asset nightly $name --repo $env:REPO --yes
|
||||
}
|
||||
@@ -342,11 +337,7 @@ jobs:
|
||||
shell: pwsh
|
||||
run: |
|
||||
$name = "$env:MSI_NAME"
|
||||
if ("${{ matrix.flavor }}" -eq "qt6") {
|
||||
echo "msi_name_qt6=$name" >> $env:GITHUB_OUTPUT
|
||||
} else {
|
||||
echo "msi_name_qt5=$name" >> $env:GITHUB_OUTPUT
|
||||
}
|
||||
echo "msi_name_qt6=$name" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
@@ -354,11 +345,11 @@ jobs:
|
||||
run: |
|
||||
Write-Host "=== MSI build summary (${{ matrix.flavor }}) ==="
|
||||
Write-Host "Version : ${{ steps.version.outputs.VERSION_DISPLAY }}"
|
||||
Write-Host "Experimental : ${{ matrix.experimental }}"
|
||||
Write-Host "Signed : true"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 2 : Génère et déploie la page GitHub Pages une fois les DEUX MSI
|
||||
# (Qt5 + Qt6) publiés, pour que toutes les URLs soient connues.
|
||||
# Job 2 : Génère et déploie la page GitHub Pages une fois le MSI publié,
|
||||
# pour que toutes les URLs soient connues.
|
||||
# ---------------------------------------------------------------------------
|
||||
deploy-pages:
|
||||
needs: build-msi
|
||||
@@ -388,13 +379,17 @@ jobs:
|
||||
|
||||
ASSETS=$(gh release view nightly --repo "$REPO" --json assets --jq '.assets[].name')
|
||||
|
||||
EXE_NAME=$(echo "$ASSETS" | grep -v 'qt6' | grep '\.exe$' | head -1)
|
||||
ZIP_NAME=$(echo "$ASSETS" | grep -v 'qt6' | grep '\.zip$' | head -1)
|
||||
MSI_NAME=$(echo "$ASSETS" | grep -v 'qt6' | grep '\.msi$' | head -1 || echo "")
|
||||
EXE_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.exe$' | head -1)
|
||||
ZIP_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.zip$' | head -1)
|
||||
MSI_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.msi$' | head -1 || echo "")
|
||||
|
||||
EXE_QT6_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.exe$' | head -1 || echo "")
|
||||
ZIP_QT6_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.zip$' | head -1 || echo "")
|
||||
MSI_QT6_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 "")
|
||||
|
||||
BASE="https://github.com/$REPO/releases/download/nightly"
|
||||
INSTALLER_URL="$BASE/$EXE_NAME"
|
||||
@@ -402,12 +397,12 @@ jobs:
|
||||
MSI_URL=""
|
||||
[ -n "$MSI_NAME" ] && MSI_URL="$BASE/$MSI_NAME"
|
||||
|
||||
INSTALLER_QT6_URL=""
|
||||
PORTABLE_QT6_URL=""
|
||||
MSI_QT6_URL=""
|
||||
[ -n "$EXE_QT6_NAME" ] && INSTALLER_QT6_URL="$BASE/$EXE_QT6_NAME"
|
||||
[ -n "$ZIP_QT6_NAME" ] && PORTABLE_QT6_URL="$BASE/$ZIP_QT6_NAME"
|
||||
[ -n "$MSI_QT6_NAME" ] && MSI_QT6_URL="$BASE/$MSI_QT6_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}"
|
||||
@@ -417,7 +412,7 @@ jobs:
|
||||
|
||||
export DATE SHORT REPO SHA RUN_URL RUN_NUMBER
|
||||
export INSTALLER_URL PORTABLE_URL MSI_URL
|
||||
export INSTALLER_QT6_URL PORTABLE_QT6_URL MSI_QT6_URL
|
||||
export LEGACY_INSTALLER_URL LEGACY_PORTABLE_URL LEGACY_MSI_URL
|
||||
|
||||
python3 source/build-aux/generate-page.py
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ All notable changes to QElectroTech are documented here.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Fix #798: clamp element-editor and diagram-view zoom to prevent view-transform overflow crash on scroll-wheel zoom ([3ca5d4a](../../commit/3ca5d4ab2))
|
||||
- Fix(windows-msi): inject rev into MSI Version Build field ([e19f523](../../commit/e19f5232277efb37435cb65a83563d73333d62ec))
|
||||
- Fix #391: use wide-char path for pugixml on Windows to handle Unicode paths ([31edf30](../../commit/31edf30c619213368e9b592b51be6ca8190db831))
|
||||
- Fix(#283): restore center alignment when loading table config ([f55ba56](../../commit/f55ba568f68293e06436899bcc831431e9d27295))
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"runtime": "org.kde.Platform",
|
||||
"runtime-version": "6.11",
|
||||
"sdk": "org.kde.Sdk",
|
||||
"base": "io.qt.qtwebengine.BaseApp",
|
||||
"base-version": "6.11",
|
||||
"command": "qelectrotech",
|
||||
"rename-desktop-file": "org.qelectrotech.qelectrotech.desktop",
|
||||
"rename-appdata-file": "qelectrotech.appdata.xml",
|
||||
@@ -25,13 +27,14 @@
|
||||
"*.la",
|
||||
"*.a"
|
||||
],
|
||||
"cleanup-commands": [
|
||||
"/app/cleanup-BaseApp.sh"
|
||||
],
|
||||
"modules": [
|
||||
"tkinter.json",
|
||||
"pypi-dependencies.json",
|
||||
{
|
||||
"name": "qelectrotech",
|
||||
"//qt6-private-headers-note": "qt6-base-private-dev has no Flatpak build-depends equivalent — private Qt6 headers ship inside org.kde.Sdk itself. If the cmake build fails looking for QtCore/private/*.h, the SDK/runtime branch is mismatched with the source tree, not a missing package.",
|
||||
"//sqlite-driver-note": "libqt6sql6-sqlite / libsqlite3-dev have no Flatpak build-depends equivalent either. QET_EXPORT_PROJECT_DB=ON assumes the KDE runtime ships the Qt6 SQLite plugin (libqsqlite.so) already built into QtSql — verified at Debian packaging time this needed an explicit runtime dep there (see debian/control). Test the in-app database export after building; if it silently fails, the runtime's QtSql plugin set needs checking, not a package to add here.",
|
||||
"buildsystem": "cmake",
|
||||
"config-opts": [
|
||||
"-DCMAKE_INSTALL_PREFIX=/app",
|
||||
|
||||
@@ -2,11 +2,19 @@
|
||||
"""
|
||||
generate-page.py — Generates gh-pages/index.html for QElectroTech nightly builds.
|
||||
Called from windows-msi.yml deploy-pages job.
|
||||
|
||||
Environment variables required:
|
||||
DATE, SHORT, REPO, SHA, RUN_URL, RUN_NUMBER,
|
||||
INSTALLER_URL, PORTABLE_URL, MSI_URL (optional)
|
||||
Optional (Qt6 track — omitted entirely if empty):
|
||||
INSTALLER_QT6_URL, PORTABLE_QT6_URL, MSI_QT6_URL
|
||||
|
||||
Optional (frozen Qt5 legacy build — omitted entirely if empty):
|
||||
LEGACY_INSTALLER_URL, LEGACY_PORTABLE_URL, LEGACY_MSI_URL
|
||||
|
||||
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).
|
||||
"""
|
||||
import os
|
||||
|
||||
@@ -20,9 +28,9 @@ installer_url = os.environ.get("INSTALLER_URL", "")
|
||||
portable_url = os.environ.get("PORTABLE_URL", "")
|
||||
msi_url = os.environ.get("MSI_URL", "")
|
||||
|
||||
installer_qt6_url = os.environ.get("INSTALLER_QT6_URL", "")
|
||||
portable_qt6_url = os.environ.get("PORTABLE_QT6_URL", "")
|
||||
msi_qt6_url = os.environ.get("MSI_QT6_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", "")
|
||||
|
||||
msi_block = ""
|
||||
if msi_url:
|
||||
@@ -32,43 +40,44 @@ if msi_url:
|
||||
<span class="btn-text">Windows Installer .msi<small>.msi — for enterprise / GPO deployment</small></span>
|
||||
</a>"""
|
||||
|
||||
# Qt6 section — only rendered if at least one Qt6 asset exists.
|
||||
qt6_block = ""
|
||||
if installer_qt6_url or portable_qt6_url or msi_qt6_url:
|
||||
qt6_msi_btn = ""
|
||||
if msi_qt6_url:
|
||||
qt6_msi_btn = f"""
|
||||
<a class="btn btn-msi" href="{msi_qt6_url}">
|
||||
# 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 .msi (Qt6)<small>.msi — for enterprise / GPO deployment</small></span>
|
||||
<span class="btn-text">Windows Installer (Qt5, legacy)<small>.exe — frozen, no longer updated</small></span>
|
||||
</a>"""
|
||||
qt6_installer_btn = ""
|
||||
if installer_qt6_url:
|
||||
qt6_installer_btn = f"""
|
||||
<a class="btn btn-primary" href="{installer_qt6_url}">
|
||||
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 (Qt6)<small>.exe — includes all dependencies</small></span>
|
||||
<span class="btn-text">Windows Installer .msi (Qt5, legacy)<small>.msi — frozen, no longer updated</small></span>
|
||||
</a>"""
|
||||
qt6_portable_btn = ""
|
||||
if portable_qt6_url:
|
||||
qt6_portable_btn = f"""
|
||||
<a class="btn btn-secondary" href="{portable_qt6_url}">
|
||||
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 (Qt6)<small>.zip — no installation required</small></span>
|
||||
<span class="btn-text">Windows Portable (Qt5, legacy)<small>.zip — frozen, no longer updated</small></span>
|
||||
</a>"""
|
||||
|
||||
qt6_block = f"""
|
||||
legacy_block = f"""
|
||||
<div class="card">
|
||||
<h2>🧪 Windows — x86_64 — Qt6 track</h2>
|
||||
<h2>🗃 Windows — x86_64 — Qt5 (legacy, unmaintained)</h2>
|
||||
<div class="warning">
|
||||
🧪 <strong>Try Qt6 — soon the only track.</strong> The Qt5 builds above
|
||||
will be removed from Windows CI soon; Qt6 is becoming the sole supported track.
|
||||
Please switch and test it now — report any issue and mention "Qt6" explicitly.
|
||||
🗃 <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">
|
||||
{qt6_installer_btn}
|
||||
{qt6_msi_btn}
|
||||
{qt6_portable_btn}
|
||||
{legacy_installer_btn}
|
||||
{legacy_msi_btn}
|
||||
{legacy_portable_btn}
|
||||
</div>
|
||||
</div>"""
|
||||
|
||||
@@ -126,7 +135,7 @@ For production use, download a <a href="https://github.com/{repo}/releases">stab
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>🏹 Windows — x86_64 — Qt5 track</h2>
|
||||
<h2>🏹 Windows — x86_64</h2>
|
||||
<div class="downloads">
|
||||
<a class="btn btn-primary" href="{installer_url}">
|
||||
<span class="btn-icon">⬇</span>
|
||||
@@ -143,7 +152,7 @@ For production use, download a <a href="https://github.com/{repo}/releases">stab
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{qt6_block}
|
||||
{legacy_block}
|
||||
</main>
|
||||
<footer>
|
||||
Auto-generated by GitHub Actions ·
|
||||
|
||||
@@ -318,6 +318,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddimage.h
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddshape.cpp
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddshape.h
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddpath.cpp
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddpath.h
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddtext.cpp
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddtext.h
|
||||
${QET_DIR}/sources/diagramevent/diagrameventinterface.cpp
|
||||
@@ -518,6 +520,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/qetgraphicsitem/qetgraphicsitem.h
|
||||
${QET_DIR}/sources/qetgraphicsitem/qetshapeitem.cpp
|
||||
${QET_DIR}/sources/qetgraphicsitem/qetshapeitem.h
|
||||
${QET_DIR}/sources/qetgraphicsitem/shapetransform.cpp
|
||||
${QET_DIR}/sources/qetgraphicsitem/shapetransform.h
|
||||
${QET_DIR}/sources/qetgraphicsitem/qgraphicsitemutility.cpp
|
||||
${QET_DIR}/sources/qetgraphicsitem/qgraphicsitemutility.h
|
||||
${QET_DIR}/sources/qetgraphicsitem/reportelement.cpp
|
||||
@@ -718,8 +722,12 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/ui/elementpropertieswidget.h
|
||||
${QET_DIR}/sources/ui/formulaassistantdialog.cpp
|
||||
${QET_DIR}/sources/ui/formulaassistantdialog.h
|
||||
${QET_DIR}/sources/ui/imagecropdialog.cpp
|
||||
${QET_DIR}/sources/ui/imagecropdialog.h
|
||||
${QET_DIR}/sources/ui/imagepropertieswidget.cpp
|
||||
${QET_DIR}/sources/ui/imagepropertieswidget.h
|
||||
${QET_DIR}/sources/ui/imagetransparentcolordialog.cpp
|
||||
${QET_DIR}/sources/ui/imagetransparentcolordialog.h
|
||||
${QET_DIR}/sources/ui/importelementdialog.cpp
|
||||
${QET_DIR}/sources/ui/importelementdialog.h
|
||||
${QET_DIR}/sources/ui/importelementtextpatterndialog.cpp
|
||||
@@ -790,6 +798,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/undocommand/setautonumcontextcommand.h
|
||||
${QET_DIR}/sources/undocommand/rotateselectioncommand.cpp
|
||||
${QET_DIR}/sources/undocommand/rotateselectioncommand.h
|
||||
${QET_DIR}/sources/undocommand/promoteshapecommand.cpp
|
||||
${QET_DIR}/sources/undocommand/promoteshapecommand.h
|
||||
${QET_DIR}/sources/undocommand/rotatetextscommand.cpp
|
||||
${QET_DIR}/sources/undocommand/rotatetextscommand.h
|
||||
${QET_DIR}/sources/undocommand/movegraphicsitemcommand.cpp
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="m18.5 4c-.65424 0-1.202197.418077-1.408203 1h-13.0918v1c3.601 0 6.5 2.899 6.5 6.5 0 1.905514-.822115 3.601777-2.121094 4.787109-.245727-.180239-.548776-.287109-.878906-.287109-.831 0-1.5.669-1.5 1.5 0 .831.669 1.5 1.5 1.5.65424 0 1.202197-.418077 1.408203-1h4.091797v1h3v-3h-3v1h-3.912109c1.479147-1.367931 2.412109-3.317097 2.412109-5.5 0-2.788667-1.510668-5.207469-3.757813-6.5h9.349609c.206006.581923.753963 1 1.408203 1 .831 0 1.5-.669 1.5-1.5 0-.831-.669-1.5-1.5-1.5m0 1c.277 0 .5.223.5.5 0 .277-.223.5-.5.5-.06925 0-.135453-.013828-.195312-.039063-.179579-.075703-.304688-.253188-.304688-.460938 0-.06925.013828-.135453.039062-.195313.075704-.179578.253188-.304688.460938-.304688m-11 13c.277 0 .5.223.5.5 0 .277-.223.5-.5.5-.277 0-.5-.223-.5-.5 0-.277.223-.5.5-.5m6.5 0h1v1h-1v-1z" transform="translate(-.99999-.99999)" class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
<g transform="translate(1,1)">
|
||||
<g class="ColorScheme-Text" fill="currentColor">
|
||||
<path d="m6 14c0 2.77 2.23 5 5 5s5-2.23 5-5z" fill-rule="evenodd" opacity=".66"/>
|
||||
<path d="m7.482 10c-.34.586-.65 1.148-.886 1.676a4.958 4.958 0 0 0 -.596 2.324h10c0-.845-.228-1.627-.596-2.324a18.518 18.518 0 0 0 -.886-1.676z" fill-rule="evenodd" opacity=".33"/>
|
||||
<path d="m11 3c-.973 3.405-3.354 6.332-4.404 8.676a4.958 4.958 0 0 0 -.596 2.324c0 2.77 2.23 5 5 5s5-2.23 5-5c0-.845-.228-1.627-.596-2.324-1.05-2.344-3.431-5.271-4.404-8.676zm0 2.393c1.194 2.668 2.747 5.027 3.492 6.691l.012.03.016.029a3.94 3.94 0 0 1 .48 1.857c0 2.233-1.767 4-4 4a3.97 3.97 0 0 1 -4-4c0-.665.177-1.283.48-1.857l.016-.03.012-.029c.745-1.664 2.298-4.023 3.492-6.691z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 983 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor;fill-opacity:1;stroke:none"
|
||||
d="M 4 2 L 4 3 L 5 3 L 5 2 L 4 2 z M 11 2 L 11 3 L 12 3 L 12 2 L 11 2 z M 2 4 L 2 12 L 7 12 L 7 10 L 9 10 L 9 12 L 14 12 L 14 4 L 9 4 L 9 6 L 7 6 L 7 4 L 2 4 z M 3 5 L 6 5 L 6 11 L 3 11 L 3 5 z M 10 5 L 13 5 L 13 11 L 10 11 L 10 5 z M 11 6 L 11 10 L 12 10 L 12 6 L 11 6 z M 7 7 L 9 7 L 9 9 L 7 9 L 7 7 z M 4 13 L 4 14 L 5 14 L 5 13 L 4 13 z M 11 13 L 11 14 L 12 14 L 12 13 L 11 13 z "
|
||||
class="ColorScheme-Text"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 716 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor;fill-opacity:1;stroke:none"
|
||||
d="M 4 2 L 4 7 L 6 7 L 6 9 L 4 9 L 4 14 L 12 14 L 12 9 L 10 9 L 10 7 L 12 7 L 12 2 L 4 2 z M 5 3 L 11 3 L 11 6 L 5 6 L 5 3 z M 2 4 L 2 5 L 3 5 L 3 4 L 2 4 z M 13 4 L 13 5 L 14 5 L 14 4 L 13 4 z M 7 7 L 9 7 L 9 9 L 7 9 L 7 7 z M 5 10 L 11 10 L 11 13 L 5 13 L 5 10 z M 2 11 L 2 12 L 3 12 L 3 11 L 2 11 z M 6 11 L 6 12 L 10 12 L 10 11 L 6 11 z M 13 11 L 13 12 L 14 12 L 14 11 L 13 11 z "
|
||||
class="ColorScheme-Text"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 716 B |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 3 3 L 3 5 L 3 6 L 6 6 L 6 5 L 6 3 L 4 3 L 3 3 z M 16 3 L 16 4 L 16 5 L 16 6 L 17 6 L 18 6 L 19 6 L 19 5 L 19 4 L 19 3 L 16 3 z M 4 4 L 5 4 L 5 5 L 4 5 L 4 4 z M 17 4 L 18 4 L 18 5 L 17 5 L 17 4 z M 7 5 L 7 6 L 9 6 L 9 5 L 7 5 z M 10 5 L 10 6 L 12 6 L 12 5 L 10 5 z M 13 5 L 13 6 L 15 6 L 15 5 L 13 5 z M 5 7 L 5 9 L 6 9 L 6 7 L 5 7 z M 16 7 L 16 9 L 17 9 L 17 7 L 16 7 z M 5 10 L 5 12 L 6 12 L 6 10 L 5 10 z M 16 10 L 16 12 L 17 12 L 17 10 L 16 10 z M 5 13 L 5 15 L 6 15 L 6 13 L 5 13 z M 16 13 L 16 15 L 17 15 L 17 13 L 16 13 z M 3 16 L 3 17 L 3 19 L 4 19 L 5 19 L 6 19 L 6 16 L 3 16 z M 7 16 L 7 17 L 9 17 L 9 16 L 7 16 z M 10 16 L 10 17 L 12 17 L 12 16 L 10 16 z M 13 16 L 13 17 L 15 17 L 15 16 L 13 16 z M 16 16 L 16 17 L 16 18 L 16 19 L 17 19 L 18 19 L 19 19 L 19 18 L 19 17 L 19 16 L 17 16 L 16 16 z M 4 17 L 5 17 L 5 18 L 4 18 L 4 17 z M 17 17 L 18 17 L 18 18 L 17 18 L 17 17 z " class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
@@ -548,6 +548,14 @@
|
||||
<file>ico/breeze-icons/scalable/mimetypes/small/48x48/application-x-qet-element.svgz</file>
|
||||
<file>ico/breeze-icons/scalable/mimetypes/small/48x48/application-x-qet-project.svgz</file>
|
||||
<file>ico/breeze-icons/scalable/mimetypes/small/48x48/application-x-qet-titleblock.svgz</file>
|
||||
<file>ico/breeze-icons/scalable/apps/hidef/draw-bezier-curves.svg</file>
|
||||
<file>ico/breeze-icons/scalable/apps/hidef/transform-crop.svg</file>
|
||||
<file>ico/breeze-icons/scalable/apps/hidef/image-flip-horizontal-symbolic.svg</file>
|
||||
<file>ico/breeze-icons/scalable/apps/hidef/image-flip-vertical-symbolic.svg</file>
|
||||
<file>ico/breeze-icons/scalable/apps/hidef/edit-opacity.svg</file>
|
||||
<file>ico/generated/rect-to-polyline.svg</file>
|
||||
<file>ico/generated/rect-to-bezier.svg</file>
|
||||
<file>ico/generated/ellipse-to-bezier.svg</file>
|
||||
<file>ico/16x16/object-group.png</file>
|
||||
<file>ico/mac_icon/elmt.icns</file>
|
||||
<file>ico/mac_icon/qelectrotech.icns</file>
|
||||
|
||||
@@ -72,6 +72,7 @@ QPropertyUndoCommand::QPropertyUndoCommand(const QPropertyUndoCommand *other)
|
||||
m_new_value = other->m_new_value;
|
||||
m_animate = other->m_animate;
|
||||
m_first_time = other->m_first_time;
|
||||
m_undo_first_time = other->m_undo_first_time;
|
||||
setText(other->text());
|
||||
}
|
||||
|
||||
@@ -99,11 +100,20 @@ void QPropertyUndoCommand::enableAnimation (bool animate) {
|
||||
@param first_time = if true,
|
||||
the first animation is done at the first call of redo if false,
|
||||
the first animation is done at the second call of redo.
|
||||
The same rule applies to undo, tracked independently: redo() always
|
||||
runs before the first undo() (QUndoStack::push() calls redo()
|
||||
immediately), so by the time undo() can run at all, redo()'s own
|
||||
m_first_time has already flipped true. Sharing that flag would leave
|
||||
undo() with no instant path ever reachable in practice -- reusing it
|
||||
is not actually symmetric, it just looks like it is. m_undo_first_time
|
||||
gives undo() the same one-time grace period redo() has, on its own
|
||||
first call instead of redo's.
|
||||
*/
|
||||
void QPropertyUndoCommand::setAnimated(bool animate, bool first_time)
|
||||
{
|
||||
m_animate = animate;
|
||||
m_first_time = first_time;
|
||||
m_undo_first_time = first_time;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,6 +128,18 @@ bool QPropertyUndoCommand::mergeWith(const QUndoCommand *other)
|
||||
QPropertyUndoCommand const *undo = static_cast<const QPropertyUndoCommand *>(other);
|
||||
if (m_object != undo->m_object
|
||||
|| m_property_name != undo->m_property_name) return false;
|
||||
// Same object and property name alone isn't enough: two entirely
|
||||
// separate, deliberate actions (say, cropping an image and then
|
||||
// mirroring it) both go through the same "pixmap" property and
|
||||
// would otherwise silently coalesce into one undo entry, carrying
|
||||
// only the first action's label -- the second vanishes from the
|
||||
// undo list with no way to undo just it. Legitimate merging (a
|
||||
// slider or spinbox pushing one command per tick while being
|
||||
// dragged, e.g. ArcEditor's angle editors) always reuses the exact
|
||||
// same text() across the whole sequence, so requiring a match here
|
||||
// keeps that working unchanged while refusing to merge anything
|
||||
// that isn't actually a continuation of the same action.
|
||||
if (text() != other->text()) return false;
|
||||
m_new_value = undo->m_new_value;
|
||||
return true;
|
||||
}
|
||||
@@ -155,7 +177,7 @@ void QPropertyUndoCommand::undo()
|
||||
{
|
||||
if (m_object->property(m_property_name) != m_old_value)
|
||||
{
|
||||
if (m_animate)
|
||||
if (m_animate && m_undo_first_time)
|
||||
{
|
||||
QPropertyAnimation *animation = new QPropertyAnimation(m_object, m_property_name);
|
||||
animation->setStartValue(m_new_value);
|
||||
@@ -163,7 +185,10 @@ void QPropertyUndoCommand::undo()
|
||||
animation->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_object->setProperty(m_property_name, m_old_value);
|
||||
m_undo_first_time = true;
|
||||
}
|
||||
}
|
||||
|
||||
QUndoCommand::undo();
|
||||
|
||||
@@ -58,7 +58,8 @@ class QPropertyUndoCommand : public QUndoCommand
|
||||
const char *m_property_name;
|
||||
QVariant m_old_value, m_new_value;
|
||||
bool m_animate = false,
|
||||
m_first_time = true;
|
||||
m_first_time = true,
|
||||
m_undo_first_time = true;
|
||||
};
|
||||
|
||||
#endif // QPROPERTYUNDOCOMMAND_H
|
||||
|
||||
@@ -109,7 +109,16 @@ QString diagramStem(Diagram *diagram, int index)
|
||||
}
|
||||
|
||||
/// Render @p diagram into @p painter, fitting @p target to the page rect.
|
||||
void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target)
|
||||
/// @p showTerminals: paint terminal markers (red stroke + blue docking
|
||||
/// dot) and terminal names, as the interactive editor does. Off by
|
||||
/// default — Terminal::paint() draws them whenever the diagram's
|
||||
/// drawTerminals()/drawTerminalNames() flags are set (default true, so
|
||||
/// headless export used to ship them as editor UI); the GUI export
|
||||
/// dialog already defaults to clearing them via
|
||||
/// Diagram::applyProperties(). Pass true (--show-terminals) to keep
|
||||
/// them, e.g. to visually debug an unconnected pin.
|
||||
void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target,
|
||||
bool showTerminals = false)
|
||||
{
|
||||
const QRect source = diagramRect(diagram);
|
||||
// Export without the editor grid: drawBackground() only paints it when
|
||||
@@ -117,14 +126,21 @@ void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target)
|
||||
// and restore it afterwards.
|
||||
const bool was_drawing_grid = diagram->displayGrid();
|
||||
const bool was_drawing_guides = diagram->displayGuides();
|
||||
const bool was_drawing_terminals = diagram->drawTerminals();
|
||||
const bool was_drawing_terminal_names = diagram->drawTerminalNames();
|
||||
diagram->setDisplayGrid(false);
|
||||
diagram->setDisplayGuides(false);
|
||||
diagram->setDrawTerminals(showTerminals);
|
||||
diagram->setDrawTerminalNames(showTerminals);
|
||||
diagram->render(&painter, target, source, Qt::KeepAspectRatio);
|
||||
diagram->setDisplayGrid(was_drawing_grid);
|
||||
diagram->setDisplayGuides(was_drawing_guides);
|
||||
diagram->setDrawTerminals(was_drawing_terminals);
|
||||
diagram->setDrawTerminalNames(was_drawing_terminal_names);
|
||||
}
|
||||
|
||||
int exportPdf(QETProject &project, const QString &output)
|
||||
int exportPdf(QETProject &project, const QString &output,
|
||||
bool showTerminals = false)
|
||||
{
|
||||
const QList<Diagram *> diagrams = project.diagrams();
|
||||
if (diagrams.isEmpty()) {
|
||||
@@ -164,7 +180,7 @@ int exportPdf(QETProject &project, const QString &output)
|
||||
}
|
||||
const QRectF target(0, 0,
|
||||
writer.width(), writer.height());
|
||||
renderDiagram(diagram, painter, target);
|
||||
renderDiagram(diagram, painter, target, showTerminals);
|
||||
|
||||
// Inject clickable cross-reference / folio-report hyperlinks for this
|
||||
// page. The geometry is rebuilt from the QPdfWriter (not a QPrinter):
|
||||
@@ -214,7 +230,7 @@ int exportPdf(QETProject &project, const QString &output)
|
||||
}
|
||||
|
||||
int exportImages(QETProject &project, const QString &format,
|
||||
const QString &out_dir)
|
||||
const QString &out_dir, bool showTerminals = false)
|
||||
{
|
||||
const QList<Diagram *> diagrams = project.diagrams();
|
||||
if (diagrams.isEmpty()) {
|
||||
@@ -237,14 +253,16 @@ int exportImages(QETProject &project, const QString &format,
|
||||
gen.setViewBox(QRect(0, 0, r.width(), r.height()));
|
||||
gen.setTitle(diagram->title());
|
||||
QPainter painter(&gen);
|
||||
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()));
|
||||
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()),
|
||||
showTerminals);
|
||||
painter.end();
|
||||
} else { // png
|
||||
QImage image(r.size(), QImage::Format_ARGB32);
|
||||
image.fill(Qt::white);
|
||||
QPainter painter(&image);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()));
|
||||
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()),
|
||||
showTerminals);
|
||||
painter.end();
|
||||
if (!image.save(path)) {
|
||||
err << "Failed to write '" << path << "'.\n";
|
||||
@@ -768,13 +786,19 @@ bool isExportRequest(const QStringList &args)
|
||||
|
||||
int run(const QStringList &args)
|
||||
{
|
||||
// --show-terminals is a standalone switch (not tied to a position),
|
||||
// so pull it out before the positional project/output arguments are
|
||||
// collected below.
|
||||
QStringList filtered = args;
|
||||
const bool showTerminals = filtered.removeAll("--show-terminals") > 0;
|
||||
|
||||
QString flag;
|
||||
QStringList rest;
|
||||
for (int i = 0; i < args.size(); ++i) {
|
||||
if (exportFlags().contains(args.at(i))) {
|
||||
flag = args.at(i);
|
||||
for (int j = i + 1; j < args.size(); ++j)
|
||||
rest << args.at(j);
|
||||
for (int i = 0; i < filtered.size(); ++i) {
|
||||
if (exportFlags().contains(filtered.at(i))) {
|
||||
flag = filtered.at(i);
|
||||
for (int j = i + 1; j < filtered.size(); ++j)
|
||||
rest << filtered.at(j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -818,7 +842,7 @@ int run(const QStringList &args)
|
||||
return 2;
|
||||
}
|
||||
if (format == "pdf")
|
||||
return exportPdf(project, output);
|
||||
return exportPdf(project, output, showTerminals);
|
||||
if (format == "cables" || format == "wires")
|
||||
return exportCsv(project, format, output);
|
||||
if (format == "bom")
|
||||
@@ -831,7 +855,7 @@ int run(const QStringList &args)
|
||||
return resaveProject(project, output);
|
||||
if (format == "settb")
|
||||
return setTitleBlock(project, output, rest.mid(2));
|
||||
return exportImages(project, format, output);
|
||||
return exportImages(project, format, output, showTerminals);
|
||||
}
|
||||
|
||||
} // namespace CLIExport
|
||||
|
||||
@@ -42,9 +42,9 @@ namespace CLIExport {
|
||||
@return process exit code (0 on success).
|
||||
|
||||
Usage:
|
||||
qelectrotech --export-pdf <project.qet> <output.pdf>
|
||||
qelectrotech --export-png <project.qet> <output_dir>
|
||||
qelectrotech --export-svg <project.qet> <output_dir>
|
||||
qelectrotech --export-pdf <project.qet> <output.pdf> [--show-terminals]
|
||||
qelectrotech --export-png <project.qet> <output_dir> [--show-terminals]
|
||||
qelectrotech --export-svg <project.qet> <output_dir> [--show-terminals]
|
||||
qelectrotech --export-cables <project.qet> <output.csv>
|
||||
qelectrotech --export-wires <project.qet> <output.csv>
|
||||
qelectrotech --export-bom <project.qet> <output.csv>
|
||||
@@ -57,6 +57,10 @@ namespace CLIExport {
|
||||
|
||||
PDF: one multi-page document (one diagram per page).
|
||||
PNG/SVG: one file per diagram, named <output_dir>/<NN>_<title>.<ext>.
|
||||
--show-terminals: also paint terminal markers (red stroke + blue
|
||||
docking dot) and terminal names, as the interactive editor
|
||||
does; off by default, matching the GUI export dialog's
|
||||
default. Has no effect on the non-image export modes.
|
||||
cables: wiring list (one row per conductor) as CSV.
|
||||
wires: list of distinct wire numbers as CSV.
|
||||
bom: bill of materials (one row per element) as CSV.
|
||||
|
||||
@@ -225,12 +225,57 @@ void projectDataBase::addDiagram(Diagram *diagram)
|
||||
|
||||
void projectDataBase::removeDiagram(Diagram *diagram)
|
||||
{
|
||||
m_remove_diagram_query.bindValue(":uuid", diagram->uuid().toString());
|
||||
const QString uuid_str = diagram->uuid().toString();
|
||||
|
||||
//Order matters: element_info and terminal are scoped through a
|
||||
//subquery on element, so they must run before element itself is
|
||||
//deleted below. The whole cascade runs in one transaction and is
|
||||
//rolled back on the first error, so a mid-cascade failure (e.g. a
|
||||
//locked DB) can't leave the diagram row deleted while its
|
||||
//element/terminal/element_info/conductor rows survive.
|
||||
m_data_base.transaction();
|
||||
|
||||
m_cascade_remove_element_info_query.bindValue(":uuid", uuid_str);
|
||||
if (!m_cascade_remove_element_info_query.exec()) {
|
||||
qDebug() << "projectDataBase::removeDiagram element_info cascade error : "
|
||||
<< m_cascade_remove_element_info_query.lastError();
|
||||
m_data_base.rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
m_cascade_remove_terminal_query.bindValue(":uuid", uuid_str);
|
||||
if (!m_cascade_remove_terminal_query.exec()) {
|
||||
qDebug() << "projectDataBase::removeDiagram terminal cascade error : "
|
||||
<< m_cascade_remove_terminal_query.lastError();
|
||||
m_data_base.rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
m_cascade_remove_conductor_query.bindValue(":uuid", uuid_str);
|
||||
if (!m_cascade_remove_conductor_query.exec()) {
|
||||
qDebug() << "projectDataBase::removeDiagram conductor cascade error : "
|
||||
<< m_cascade_remove_conductor_query.lastError();
|
||||
m_data_base.rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
m_cascade_remove_element_query.bindValue(":uuid", uuid_str);
|
||||
if (!m_cascade_remove_element_query.exec()) {
|
||||
qDebug() << "projectDataBase::removeDiagram element cascade error : "
|
||||
<< m_cascade_remove_element_query.lastError();
|
||||
m_data_base.rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
m_remove_diagram_query.bindValue(":uuid", uuid_str);
|
||||
if (!m_remove_diagram_query.exec()) {
|
||||
qDebug() << "projectDataBase::removeDiagram delete error : " << m_remove_diagram_query.lastError();
|
||||
} else {
|
||||
emit dataBaseUpdated();
|
||||
m_data_base.rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
m_data_base.commit();
|
||||
emit dataBaseUpdated();
|
||||
}
|
||||
|
||||
void projectDataBase::diagramInfoChanged(Diagram *diagram)
|
||||
@@ -772,7 +817,27 @@ void projectDataBase::prepareQuery()
|
||||
m_insert_diagram_query = QSqlQuery(m_data_base);
|
||||
m_insert_diagram_query.prepare("INSERT INTO diagram (uuid, pos) VALUES (:uuid, :pos)");
|
||||
|
||||
//REMOVE DIAGRAM
|
||||
//REMOVE DIAGRAM (cascade first: element_info and terminal have no
|
||||
//diagram_uuid column of their own, so both are scoped through
|
||||
//element while the element rows for this diagram still exist).
|
||||
m_cascade_remove_element_info_query = QSqlQuery(m_data_base);
|
||||
m_cascade_remove_element_info_query.prepare(
|
||||
"DELETE FROM element_info WHERE element_uuid IN "
|
||||
"(SELECT uuid FROM element WHERE diagram_uuid = :uuid)");
|
||||
|
||||
m_cascade_remove_terminal_query = QSqlQuery(m_data_base);
|
||||
m_cascade_remove_terminal_query.prepare(
|
||||
"DELETE FROM terminal WHERE element_uuid IN "
|
||||
"(SELECT uuid FROM element WHERE diagram_uuid = :uuid)");
|
||||
|
||||
m_cascade_remove_conductor_query = QSqlQuery(m_data_base);
|
||||
m_cascade_remove_conductor_query.prepare(
|
||||
"DELETE FROM conductor WHERE diagram_uuid = :uuid");
|
||||
|
||||
m_cascade_remove_element_query = QSqlQuery(m_data_base);
|
||||
m_cascade_remove_element_query.prepare(
|
||||
"DELETE FROM element WHERE diagram_uuid = :uuid");
|
||||
|
||||
m_remove_diagram_query = QSqlQuery(m_data_base);
|
||||
m_remove_diagram_query.prepare("DELETE FROM diagram WHERE uuid=:uuid");
|
||||
|
||||
|
||||
@@ -106,7 +106,11 @@ class projectDataBase : public QObject
|
||||
m_insert_terminal_query,
|
||||
m_insert_conductor_query,
|
||||
m_update_conductor_query,
|
||||
m_remove_conductor_query;
|
||||
m_remove_conductor_query,
|
||||
m_cascade_remove_element_info_query,
|
||||
m_cascade_remove_terminal_query,
|
||||
m_cascade_remove_conductor_query,
|
||||
m_cascade_remove_element_query;
|
||||
|
||||
#ifdef QET_EXPORT_PROJECT_DB
|
||||
public:
|
||||
|
||||
@@ -40,9 +40,53 @@
|
||||
#include "undocommand/addelementtextcommand.h"
|
||||
#include "qetinformation.h"
|
||||
#include "qetproject.h"
|
||||
#include "diagramsortkeys.h"
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <math.h>
|
||||
|
||||
namespace {
|
||||
using DiagramSortKeys::positionKey;
|
||||
|
||||
/// Sort key for Diagram::toXml()'s <elements> block: the element's own
|
||||
/// diagram-local position, exactly what it's already saved as (x/y),
|
||||
/// never invented or regenerated. uuid() is deliberately NOT used here:
|
||||
/// for an element with no persisted uuid attribute, fromXml() invents a
|
||||
/// fresh random one on every load, so sorting by uuid would still be
|
||||
/// non-deterministic across process runs for any legacy file.
|
||||
QString elementSortKey(Element *elmt)
|
||||
{
|
||||
return positionKey(elmt->pos());
|
||||
}
|
||||
|
||||
/// Sort key for a terminal: its parent element's position, then the
|
||||
/// terminal's own position local to that element (from the .elmt
|
||||
/// definition, fixed regardless of where the element is placed).
|
||||
QString terminalSortKey(Terminal *terminal)
|
||||
{
|
||||
if (!terminal)
|
||||
return QString();
|
||||
Element *parent = terminal->parentElement();
|
||||
return (parent ? positionKey(parent->pos()) : QStringLiteral("?"))
|
||||
+ QLatin1Char(':') + positionKey(terminal->pos());
|
||||
}
|
||||
|
||||
/// Sort key for Diagram::toXml()'s <conductors> block. Built from both
|
||||
/// endpoints' terminalSortKey(), not Conductor::uuid(): in every example
|
||||
/// project checked, conductors have no persisted uuid attribute at all,
|
||||
/// so uuid() is a freshly-minted random value on every load -- exactly
|
||||
/// as unusable for cross-run determinism as the element case above, just
|
||||
/// with no persisted fallback to reach for instead. Canonicalised
|
||||
/// (smaller key first) since a conductor's two ends are unordered for
|
||||
/// this purpose.
|
||||
QString conductorSortKey(Conductor *cond)
|
||||
{
|
||||
QString a = terminalSortKey(cond->terminal1);
|
||||
QString b = terminalSortKey(cond->terminal2);
|
||||
return (a <= b) ? (a + QLatin1Char('>') + b) : (b + QLatin1Char('>') + a);
|
||||
}
|
||||
}
|
||||
|
||||
int Diagram::xGrid = 10;
|
||||
int Diagram::yGrid = 10;
|
||||
int Diagram::xKeyGrid = 10;
|
||||
@@ -1014,6 +1058,20 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) {
|
||||
}
|
||||
}
|
||||
|
||||
// items() returns items in stacking order, which is not guaranteed
|
||||
// reproducible across processes (ties between same-Z items follow
|
||||
// the scene's internal index, not any content-derived order) -- so
|
||||
// without this, saving an unmodified project produces a different
|
||||
// byte stream on every run. Elements and conductors are the two
|
||||
// blocks observed to actually churn across the example corpus;
|
||||
// sort them into a deterministic, content-derived order before
|
||||
// serializing. This also fixes the legacy terminal-id churn below,
|
||||
// since those ids are assigned sequentially in element order.
|
||||
std::stable_sort(list_elements.begin(), list_elements.end(),
|
||||
[](Element *a, Element *b) { return elementSortKey(a) < elementSortKey(b); });
|
||||
std::stable_sort(list_conductors.begin(), list_conductors.end(),
|
||||
[](Conductor *a, Conductor *b) { return conductorSortKey(a) < conductorSortKey(b); });
|
||||
|
||||
// correspondence table between the addresses of the terminals and their ids
|
||||
// table de correspondance entre les adresses des bornes et leurs ids
|
||||
QHash<Terminal *, int> table_adr_id;
|
||||
|
||||
@@ -19,10 +19,14 @@
|
||||
#include "diagrameventaddimage.h"
|
||||
|
||||
#include "../qetapp.h"
|
||||
#include "../qetdiagrameditor.h"
|
||||
#include "../diagram.h"
|
||||
#include "../undocommand/addgraphicsobjectcommand.h"
|
||||
#include "../qetgraphicsitem/diagramimageitem.h"
|
||||
|
||||
#include <QStatusBar>
|
||||
#include <QTimer>
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddImage::DiagramEventAddImage
|
||||
Default constructor
|
||||
@@ -34,6 +38,16 @@ DiagramEventAddImage::DiagramEventAddImage(Diagram *diagram) :
|
||||
m_is_added (false)
|
||||
{
|
||||
openDialog();
|
||||
if (m_running)
|
||||
{
|
||||
// Deferred for the same reason as the shape tools' own
|
||||
// constructor-time hint: Diagram::setEventInterface() destroys
|
||||
// whatever tool was previously active *after* this constructor
|
||||
// returns, and that tool's own destructor clears the status bar
|
||||
// -- an immediate show here would just get wiped out moments
|
||||
// later by that cleanup.
|
||||
QTimer::singleShot(0, this, [this]() { showHint(); });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,33 +61,61 @@ DiagramEventAddImage::~DiagramEventAddImage()
|
||||
delete m_image;
|
||||
}
|
||||
|
||||
if (!m_diagram->views().isEmpty())
|
||||
{
|
||||
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
|
||||
editor->statusBar()->clearMessage();
|
||||
}
|
||||
|
||||
foreach (QGraphicsView *view, m_diagram->views())
|
||||
view->setContextMenuPolicy((Qt::DefaultContextMenu));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddImage::showHint
|
||||
Re-asserted on every move (see mouseMoveEvent), not just once at
|
||||
activation: Qt's own built-in "show an action's statusTip on hover"
|
||||
has its own internal "restore whatever was there before" logic for
|
||||
when the hover ends. Since this message is first shown *during* that
|
||||
same hover session (the user is still over the toolbar icon when the
|
||||
deferred constructor-time call above fires), Qt's hover-tracking has
|
||||
no idea this code changed the status bar in the meantime -- the
|
||||
moment the mouse leaves the icon for the canvas, it silently
|
||||
restores whatever it remembers being there before its own tip
|
||||
started, overwriting this one. Re-showing it on every move within
|
||||
the canvas simply outlasts that one-time restore -- the exact same
|
||||
issue already found and fixed for the shape tools.
|
||||
*/
|
||||
void DiagramEventAddImage::showHint() const
|
||||
{
|
||||
if (m_diagram->views().isEmpty())
|
||||
return;
|
||||
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
|
||||
editor->statusBar()->showMessage(tr("Clic : positionner à la taille d'origine. "
|
||||
"Cliquer-glisser : positionner et redimensionner. "
|
||||
"Clic droit : pivoter de 90°. Ctrl+molette : ajuster la taille."));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddImage::mousePressEvent
|
||||
Action when mouse is pressed
|
||||
Left button: starts a potential drag-to-resize, anchored here -- but
|
||||
doesn't commit to anything yet. A quick click-release (see
|
||||
mouseMoveEvent's threshold check) still places the image at its
|
||||
original size, matching the previous behavior exactly; only an
|
||||
actual drag switches to resizing. Right button still rotates in 90
|
||||
degree steps, unchanged, and only while not already left-dragging.
|
||||
@param event : event of mouse pressed
|
||||
*/
|
||||
void DiagramEventAddImage::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (m_image && event -> button() == Qt::LeftButton)
|
||||
if (m_image && event->button() == Qt::LeftButton)
|
||||
{
|
||||
QPointF pos = event->scenePos();
|
||||
pos.rx() -= m_image->boundingRect().width()/2;
|
||||
pos.ry() -= m_image->boundingRect().height()/2;
|
||||
m_diagram -> undoStack().push (new AddGraphicsObjectCommand(m_image, m_diagram, pos));
|
||||
|
||||
for (QGraphicsView *view : m_diagram->views()) {
|
||||
view->setContextMenuPolicy((Qt::DefaultContextMenu));
|
||||
}
|
||||
|
||||
m_running = false;
|
||||
emit finish();
|
||||
m_pressed = true;
|
||||
m_resize_engaged = false;
|
||||
m_press_pos = event->scenePos();
|
||||
event->setAccepted(true);
|
||||
}
|
||||
else if (m_image && event -> button() == Qt::RightButton)
|
||||
else if (m_image && !m_pressed && event->button() == Qt::RightButton)
|
||||
{
|
||||
m_image->setRotation(m_image->rotation() + 90);
|
||||
event->setAccepted(true);
|
||||
@@ -87,26 +129,95 @@ void DiagramEventAddImage::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
*/
|
||||
void DiagramEventAddImage::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (!m_image || event->buttons() != Qt::NoButton) {
|
||||
if (!m_image) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
showHint();
|
||||
|
||||
QPointF pos = event->scenePos();
|
||||
|
||||
|
||||
if (!m_is_added)
|
||||
{
|
||||
for (QGraphicsView *view : m_diagram->views()) {
|
||||
view->setContextMenuPolicy((Qt::NoContextMenu));
|
||||
}
|
||||
|
||||
|
||||
m_diagram->addItem(m_image);
|
||||
m_is_added = true;
|
||||
}
|
||||
|
||||
m_image->setPos(pos - m_image->boundingRect().center());
|
||||
|
||||
if (m_pressed)
|
||||
{
|
||||
// Anchored on m_press_pos, not the item's own current position:
|
||||
// dragging in any direction has to visibly grow the image from
|
||||
// where the click started, not from wherever the "no button
|
||||
// held" preview phase happened to leave it centered.
|
||||
const QPointF delta = pos - m_press_pos;
|
||||
if (!m_resize_engaged && QLineF(m_press_pos, pos).length() >= 4.0)
|
||||
m_resize_engaged = true; // latched: crossing back within the threshold afterward must not un-engage it
|
||||
|
||||
if (!m_resize_engaged)
|
||||
{
|
||||
// Still just a (so far) plain click -- keep behaving like
|
||||
// the pre-drag preview: original size, centered here, so
|
||||
// releasing right now reproduces the old click-to-place
|
||||
// behavior exactly.
|
||||
m_image->setPos(m_press_pos - m_image->boundingRect().center());
|
||||
}
|
||||
else
|
||||
{
|
||||
const QSizeF naturalSize = m_image->boundingRect().size();
|
||||
if (naturalSize.width() > 0 && naturalSize.height() > 0)
|
||||
{
|
||||
const qreal scaleX = qAbs(delta.x()) / naturalSize.width();
|
||||
const qreal scaleY = qAbs(delta.y()) / naturalSize.height();
|
||||
// The larger of the two, not a per-axis stretch: images
|
||||
// only support a single uniform scale today (see
|
||||
// boundingRect()/paint(), which never touch aspect
|
||||
// ratio), so this is a diagonal-drag size, not a
|
||||
// free-form one -- breaking aspect ratio on purpose is
|
||||
// its own, separate, larger piece of work.
|
||||
const qreal newScale = qBound(0.01, qMax(scaleX, scaleY), 50.0);
|
||||
m_image->setScale(newScale);
|
||||
}
|
||||
m_image->setPos(qMin(m_press_pos.x(), pos.x()), qMin(m_press_pos.y(), pos.y()));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_image->setPos(pos - m_image->boundingRect().center());
|
||||
}
|
||||
|
||||
event->setAccepted(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddImage::mouseReleaseEvent
|
||||
Left button release commits whatever mouseMoveEvent last set --
|
||||
original size and centered if the press never turned into a real
|
||||
drag, or the dragged-out size and position otherwise. Either way,
|
||||
this is the only place placement is actually finalized now; a plain
|
||||
click no longer finishes inside mousePressEvent itself, since it has
|
||||
to wait and see whether a drag follows.
|
||||
@param event : event of mouse release
|
||||
*/
|
||||
void DiagramEventAddImage::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (m_image && m_pressed && event->button() == Qt::LeftButton)
|
||||
{
|
||||
m_diagram->undoStack().push(new AddGraphicsObjectCommand(m_image, m_diagram, m_image->pos()));
|
||||
|
||||
for (QGraphicsView *view : m_diagram->views()) {
|
||||
view->setContextMenuPolicy((Qt::DefaultContextMenu));
|
||||
}
|
||||
|
||||
m_running = false;
|
||||
emit finish();
|
||||
event->setAccepted(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddImage::mouseDoubleClickEvent
|
||||
This method is used only to overwrite double click.
|
||||
@@ -124,7 +235,14 @@ void DiagramEventAddImage::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event
|
||||
*/
|
||||
void DiagramEventAddImage::wheelEvent(QGraphicsSceneWheelEvent *event)
|
||||
{
|
||||
if (!m_is_added || !m_image || event -> modifiers() != Qt::CTRL) {
|
||||
// !m_pressed added alongside the modifier fix: without it, wheel
|
||||
// scaling could fight with an active drag-resize, both trying to
|
||||
// set scale() from different sources in the same gesture.
|
||||
// event->modifiers() & Qt::ControlModifier, not != Qt::CTRL: the
|
||||
// same exact-equality bug already found and fixed several times
|
||||
// this session elsewhere -- Ctrl held together with any other
|
||||
// modifier would silently fail to register as Ctrl at all.
|
||||
if (!m_is_added || !m_image || m_pressed || !(event->modifiers() & Qt::ControlModifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
#include "diagrameventinterface.h"
|
||||
|
||||
#include <QPointF>
|
||||
|
||||
class Diagram;
|
||||
class DiagramImageItem;
|
||||
|
||||
@@ -37,15 +39,20 @@ class DiagramEventAddImage : public DiagramEventInterface
|
||||
|
||||
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseDoubleClickEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void wheelEvent (QGraphicsSceneWheelEvent *event) override;
|
||||
|
||||
bool isNull () const;
|
||||
private:
|
||||
void openDialog();
|
||||
void showHint() const;
|
||||
|
||||
DiagramImageItem *m_image;
|
||||
bool m_is_added;
|
||||
bool m_pressed = false; // left button held: dragging out a size, not just positioning
|
||||
bool m_resize_engaged = false; // latched once the drag threshold is crossed, matching the pen tool's own curve-drag threshold convention -- so dragging out and back near the start point doesn't "snap back" to original size before release
|
||||
QPointF m_press_pos; // scene position of the left-button press, the resize anchor
|
||||
};
|
||||
|
||||
#endif // DIAGRAMEVENTADDIMAGE_H
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
/*
|
||||
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 "diagrameventaddpath.h"
|
||||
|
||||
#include "../diagram.h"
|
||||
#include "../lastusedstyle.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qetdiagrameditor.h"
|
||||
#include "../undocommand/addgraphicsobjectcommand.h"
|
||||
|
||||
#include <QGraphicsLineItem>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QLineF>
|
||||
#include <QStatusBar>
|
||||
#include <QTimer>
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::DiagramEventAddPath
|
||||
@param diagram : the diagram where this event must operate
|
||||
*/
|
||||
DiagramEventAddPath::DiagramEventAddPath(Diagram *diagram) :
|
||||
DiagramEventInterface(diagram),
|
||||
m_shape_item (nullptr),
|
||||
m_help_horiz (nullptr),
|
||||
m_help_verti (nullptr)
|
||||
{
|
||||
m_running = true;
|
||||
init();
|
||||
// Deferred for the same reason as DiagramEventAddShape's own
|
||||
// constructor-time hint: Diagram::setEventInterface() destroys
|
||||
// whatever tool was previously active *after* this constructor
|
||||
// returns, and that tool's own destructor clears the status bar --
|
||||
// an immediate show here would just get wiped out moments later.
|
||||
QTimer::singleShot(0, this, [this]() { showHint(); });
|
||||
}
|
||||
|
||||
DiagramEventAddPath::~DiagramEventAddPath()
|
||||
{
|
||||
if ((m_running || m_abort) && m_shape_item)
|
||||
{
|
||||
m_diagram->removeItem(m_shape_item);
|
||||
delete m_shape_item;
|
||||
}
|
||||
delete m_help_horiz;
|
||||
delete m_help_verti;
|
||||
|
||||
if (m_diagram && !m_diagram->views().isEmpty())
|
||||
{
|
||||
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
|
||||
editor->statusBar()->clearMessage();
|
||||
}
|
||||
|
||||
foreach (QGraphicsView *v, m_diagram->views())
|
||||
v->setContextMenuPolicy(Qt::DefaultContextMenu);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::showHint
|
||||
Re-asserted on every move within the canvas (see mouseMoveEvent), not
|
||||
just once at activation: Qt's own built-in "show an action's
|
||||
statusTip on hover" has its own internal "restore whatever was there
|
||||
before" logic for when the hover ends. Since this message is first
|
||||
shown *during* that same hover session (the user is still over the
|
||||
toolbar icon when the deferred constructor-time call above fires),
|
||||
Qt's hover-tracking has no idea this code changed the status bar in
|
||||
the meantime -- the moment the mouse leaves the icon for the canvas,
|
||||
it silently restores whatever it remembers being there before its
|
||||
own tip started, overwriting this one. Re-showing it on every move
|
||||
within the canvas simply outlasts that one-time restore.
|
||||
*/
|
||||
void DiagramEventAddPath::showHint() const
|
||||
{
|
||||
if (!m_diagram || m_diagram->views().isEmpty())
|
||||
return;
|
||||
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
|
||||
editor->statusBar()->showMessage(tr("Clic: point anguleux. Cliquer-glisser: point courbe. "
|
||||
"Clic sur le premier point: fermer. Échap/Entrée: terminer. "
|
||||
"Clic droit: annuler le dernier point."));
|
||||
}
|
||||
|
||||
void DiagramEventAddPath::init()
|
||||
{
|
||||
foreach (QGraphicsView *v, m_diagram->views())
|
||||
v->setContextMenuPolicy(Qt::NoContextMenu);
|
||||
}
|
||||
|
||||
QPointF DiagramEventAddPath::snapped(const QPointF &scenePos, Qt::KeyboardModifiers mods) const
|
||||
{
|
||||
return mods == Qt::ControlModifier ? scenePos : Diagram::snapToGrid(scenePos);
|
||||
}
|
||||
|
||||
int DiagramEventAddPath::confirmedNodeCount() const
|
||||
{
|
||||
// The trailing element is always the live preview while m_shape_item
|
||||
// exists; with no shape yet there are no nodes of any kind.
|
||||
return m_shape_item ? qMax(0, m_nodes.size() - 1) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::mousePressEvent
|
||||
Left click: on the very first click, creates the shape with a real
|
||||
node *and* an immediate preview node at the same spot, so a segment
|
||||
exists (even if zero-length) from the start rather than requiring a
|
||||
second click before anything is visible. On later clicks: either
|
||||
confirms the live preview into a real point and appends a fresh one
|
||||
for the next segment, or -- if close enough to the first node --
|
||||
closes the path.
|
||||
*/
|
||||
void DiagramEventAddPath::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (Q_UNLIKELY(m_diagram->isReadOnly()))
|
||||
return;
|
||||
|
||||
if (event->button() != Qt::LeftButton)
|
||||
{
|
||||
// Accept every button while this tool is running, not just the
|
||||
// one it actually acts on -- Diagram::mousePressEvent falls
|
||||
// through to Qt's own default scene handling for anything left
|
||||
// unaccepted, which is exactly the kind of competing control
|
||||
// this tool can't afford while it's supposed to have exclusive
|
||||
// ownership of input.
|
||||
event->setAccepted(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const QPointF pos = snapped(event->scenePos(), event->modifiers());
|
||||
|
||||
if (!m_shape_item)
|
||||
{
|
||||
m_shape_item = new QetShapeItem(pos, pos, QetShapeItem::Path);
|
||||
if (LastUsedStyle::hasShapePen())
|
||||
m_shape_item->setPen(LastUsedStyle::shapePen());
|
||||
if (LastUsedStyle::hasShapeBrush())
|
||||
m_shape_item->setBrush(LastUsedStyle::shapeBrush());
|
||||
m_diagram->addItem(m_shape_item);
|
||||
// Handles only ever get built for a selected item.
|
||||
m_shape_item->setSelected(true);
|
||||
|
||||
QetShapeItem::PathNode node;
|
||||
node.anchor = pos;
|
||||
m_nodes << node;
|
||||
m_nodes << node; // live preview, tracks the mouse from here on
|
||||
m_shape_item->setPathNodes(m_nodes);
|
||||
m_shape_item->enableNodeEditMode();
|
||||
|
||||
m_dragging_node = 0;
|
||||
event->setAccepted(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirmedNodeCount() >= 2 && nearFirstNode(pos))
|
||||
{
|
||||
finishPath(true);
|
||||
event->setAccepted(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm the preview node as a real point, then append a fresh
|
||||
// preview (a plain Corner, not a copy of the just-confirmed node's
|
||||
// kind/handles) for the segment after it.
|
||||
m_dragging_node = m_nodes.size() - 1;
|
||||
m_nodes[m_dragging_node].anchor = pos;
|
||||
|
||||
QetShapeItem::PathNode preview;
|
||||
preview.anchor = pos;
|
||||
m_nodes << preview;
|
||||
|
||||
m_shape_item->setPathNodes(m_nodes);
|
||||
m_shape_item->enableNodeEditMode();
|
||||
event->setAccepted(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::mouseMoveEvent
|
||||
Two mutually exclusive behaviours, matching whether a button is held:
|
||||
with the left button down on a just-placed node, dragging shapes that
|
||||
node's handles (same convention as editing an existing node -- see
|
||||
QetShapeItem::dragPathControlHandle()). With no button held, the
|
||||
trailing preview node instead tracks the mouse, giving the live
|
||||
rubber-band segment.
|
||||
*/
|
||||
void DiagramEventAddPath::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
updateHelpCross(event->scenePos());
|
||||
showHint();
|
||||
|
||||
if (m_shape_item)
|
||||
{
|
||||
const QPointF pos = snapped(event->scenePos(), event->modifiers());
|
||||
|
||||
if (m_dragging_node >= 0 && (event->buttons() & Qt::LeftButton))
|
||||
{
|
||||
QetShapeItem::PathNode &node = m_nodes[m_dragging_node];
|
||||
const QPointF delta = pos - node.anchor;
|
||||
|
||||
// A small threshold so an accidental few-pixel wobble on
|
||||
// what was meant to be a plain click doesn't silently add
|
||||
// curve handles the user never intended.
|
||||
if (QLineF(QPointF(), delta).length() > 3.0)
|
||||
{
|
||||
node.kind = QetShapeItem::NodeKind::Smooth;
|
||||
node.outHandle = delta;
|
||||
node.inHandle = -delta;
|
||||
}
|
||||
else
|
||||
{
|
||||
node.kind = QetShapeItem::NodeKind::Corner;
|
||||
node.outHandle.reset();
|
||||
node.inHandle.reset();
|
||||
}
|
||||
m_shape_item->setPathNodes(m_nodes);
|
||||
}
|
||||
else if (!(event->buttons() & Qt::LeftButton) && !m_nodes.isEmpty())
|
||||
{
|
||||
m_nodes.last().anchor = pos;
|
||||
m_shape_item->setPathNodes(m_nodes);
|
||||
}
|
||||
}
|
||||
|
||||
// Ours unconditionally while running: a stray, unaccepted move event
|
||||
// falling through to Qt's default handling risks it dragging our
|
||||
// selected, movable in-progress shape out from under the tool.
|
||||
event->setAccepted(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::mouseReleaseEvent
|
||||
Left release just ends the current node's drag phase (the trailing
|
||||
preview resumes tracking the mouse on the next move). Right release
|
||||
steps back one *confirmed* point (the preview is left alone), or
|
||||
cancels outright once only one remains, or exits the tool entirely if
|
||||
nothing is in progress at all.
|
||||
*/
|
||||
void DiagramEventAddPath::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton)
|
||||
{
|
||||
m_dragging_node = -1;
|
||||
}
|
||||
else if (event->button() == Qt::RightButton)
|
||||
{
|
||||
if (m_shape_item)
|
||||
{
|
||||
if (confirmedNodeCount() > 1)
|
||||
{
|
||||
m_nodes.remove(m_nodes.size() - 2); // the last *confirmed* node; keep the trailing preview
|
||||
m_shape_item->setPathNodes(m_nodes);
|
||||
}
|
||||
else
|
||||
{
|
||||
cancelPath();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_running = false;
|
||||
emit finish();
|
||||
}
|
||||
}
|
||||
event->setAccepted(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::mouseDoubleClickEvent
|
||||
A double-click is a press, release, press, release, doubleclick
|
||||
sequence -- the second press already confirmed the preview into a
|
||||
duplicate point (mousePressEvent can't distinguish a double-click
|
||||
from two single clicks in the same place) and appended a fresh
|
||||
preview after it. Dropping the last node here removes that fresh
|
||||
preview; finishPath()'s own trailing-preview removal then removes the
|
||||
duplicate underneath it, leaving only the genuinely-placed points.
|
||||
*/
|
||||
void DiagramEventAddPath::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (m_shape_item && event->button() == Qt::LeftButton && !m_nodes.isEmpty())
|
||||
{
|
||||
m_nodes.removeLast();
|
||||
finishPath(false);
|
||||
}
|
||||
event->setAccepted(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::keyPressEvent
|
||||
Escape or Enter finish the path open once at least two real points
|
||||
exist; Escape with fewer (or none placed at all) cancels/exits
|
||||
instead, since there's nothing meaningful to keep.
|
||||
*/
|
||||
void DiagramEventAddPath::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
if (event->key() == Qt::Key_Escape)
|
||||
{
|
||||
if (m_shape_item && confirmedNodeCount() >= 2)
|
||||
finishPath(false);
|
||||
else if (m_shape_item)
|
||||
cancelPath();
|
||||
else
|
||||
{
|
||||
m_running = false;
|
||||
emit finish();
|
||||
}
|
||||
event->accept();
|
||||
}
|
||||
else if ((event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
|
||||
&& m_shape_item && confirmedNodeCount() >= 2)
|
||||
{
|
||||
finishPath(false);
|
||||
event->accept();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::finishPath
|
||||
Strips the trailing live-preview node, commits the in-progress path
|
||||
onto the undo stack, and resets so the tool is ready to draw another
|
||||
one immediately -- matching every other shape tool's own behaviour
|
||||
after finishing a shape.
|
||||
*/
|
||||
void DiagramEventAddPath::finishPath(bool closed)
|
||||
{
|
||||
if (!m_shape_item)
|
||||
return;
|
||||
|
||||
if (!m_nodes.isEmpty())
|
||||
m_nodes.removeLast();
|
||||
|
||||
if (m_nodes.size() < 2)
|
||||
{
|
||||
cancelPath();
|
||||
return;
|
||||
}
|
||||
|
||||
if (closed)
|
||||
m_shape_item->setClosed(true);
|
||||
|
||||
m_shape_item->setPathNodes(m_nodes);
|
||||
m_diagram->undoStack().push(new AddGraphicsObjectCommand(m_shape_item, m_diagram));
|
||||
m_shape_item = nullptr;
|
||||
m_nodes.clear();
|
||||
m_dragging_node = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::cancelPath
|
||||
Discards the in-progress path entirely -- nothing worth keeping (an
|
||||
empty or single-point path isn't a usable shape).
|
||||
*/
|
||||
void DiagramEventAddPath::cancelPath()
|
||||
{
|
||||
if (m_shape_item)
|
||||
{
|
||||
m_diagram->removeItem(m_shape_item);
|
||||
delete m_shape_item;
|
||||
m_shape_item = nullptr;
|
||||
}
|
||||
m_nodes.clear();
|
||||
m_dragging_node = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::nearFirstNode
|
||||
m_shape_item's pos()/transform() stay at their identity defaults for
|
||||
its entire construction here -- nothing during drawing ever touches
|
||||
them -- so the first node's anchor, stored in local coordinates, is
|
||||
directly comparable to a scene position without any mapping.
|
||||
*/
|
||||
bool DiagramEventAddPath::nearFirstNode(const QPointF &scenePos) const
|
||||
{
|
||||
if (m_nodes.isEmpty())
|
||||
return false;
|
||||
return QLineF(m_nodes.first().anchor, scenePos).length() <= CLOSE_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::updateHelpCross
|
||||
Same crosshair guide as every other shape tool (see
|
||||
DiagramEventAddShape::updateHelpCross) -- duplicated rather than
|
||||
shared, since the two classes don't otherwise share a common base
|
||||
beyond DiagramEventInterface.
|
||||
*/
|
||||
void DiagramEventAddPath::updateHelpCross(const QPointF &p)
|
||||
{
|
||||
if (!m_help_horiz || !m_help_verti)
|
||||
{
|
||||
QPen pen;
|
||||
pen.setWidthF(0.4);
|
||||
pen.setCosmetic(true);
|
||||
pen.setColor(Diagram::background_color == Qt::darkGray ? Qt::lightGray : Qt::darkGray);
|
||||
|
||||
QRectF rect = m_diagram->border_and_titleblock.insideBorderRect();
|
||||
|
||||
if (!m_help_horiz)
|
||||
{
|
||||
m_help_horiz = new QGraphicsLineItem(rect.topLeft().x(), 0, rect.topRight().x(), 0);
|
||||
m_help_horiz->setPen(pen);
|
||||
m_diagram->addItem(m_help_horiz);
|
||||
}
|
||||
|
||||
if (!m_help_verti)
|
||||
{
|
||||
m_help_verti = new QGraphicsLineItem(0, rect.topLeft().y(), 0, rect.bottomLeft().y());
|
||||
m_help_verti->setPen(pen);
|
||||
m_diagram->addItem(m_help_verti);
|
||||
}
|
||||
}
|
||||
|
||||
QPointF point = Diagram::snapToGrid(p);
|
||||
|
||||
m_help_horiz->setY(point.y());
|
||||
m_help_verti->setX(point.x());
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
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 DIAGRAMEVENTADDPATH_H
|
||||
#define DIAGRAMEVENTADDPATH_H
|
||||
|
||||
#include "../qetgraphicsitem/qetshapeitem.h"
|
||||
#include "diagrameventinterface.h"
|
||||
|
||||
class QGraphicsLineItem;
|
||||
|
||||
/**
|
||||
@brief The DiagramEventAddPath class
|
||||
Pen tool: interactively draw a new Path (Bezier) shape, following the
|
||||
same vocabulary every vector editor's pen tool uses:
|
||||
- click places a Corner node;
|
||||
- press-drag-release places a Smooth node, with the drag defining a
|
||||
pair of mirrored handles (same convention as
|
||||
QetShapeItem::dragPathControlHandle's "Smooth" mirroring);
|
||||
- clicking back on the first node closes the path;
|
||||
- double-click, Enter, or Escape (once 2+ nodes exist) finishes it
|
||||
open;
|
||||
- right-click steps back one node;
|
||||
- right-click or Escape with nothing placed yet cancels the tool.
|
||||
While running, m_nodes always carries one extra "preview" node at the
|
||||
end, tracking the mouse so a live rubber-band segment is always
|
||||
visible -- confirmedNodeCount() excludes it; every public gesture
|
||||
handler is responsible for stripping it before treating the list as
|
||||
"the path so far" (see finishPath(), which does this once for every
|
||||
finishing gesture).
|
||||
*/
|
||||
class DiagramEventAddPath : public DiagramEventInterface
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DiagramEventAddPath(Diagram *diagram);
|
||||
~DiagramEventAddPath() override;
|
||||
|
||||
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseDoubleClickEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void keyPressEvent (QKeyEvent *event) override;
|
||||
void init() override;
|
||||
|
||||
private:
|
||||
void updateHelpCross (const QPointF &p);
|
||||
void showHint () const;
|
||||
void finishPath (bool closed);
|
||||
void cancelPath ();
|
||||
bool nearFirstNode (const QPointF &scenePos) const;
|
||||
int confirmedNodeCount () const; // m_nodes always carries one trailing "preview" node while running; this excludes it
|
||||
QPointF snapped (const QPointF &scenePos, Qt::KeyboardModifiers mods) const;
|
||||
|
||||
QetShapeItem *m_shape_item;
|
||||
QVector<QetShapeItem::PathNode> m_nodes;
|
||||
int m_dragging_node = -1;
|
||||
QGraphicsLineItem *m_help_horiz, *m_help_verti;
|
||||
|
||||
// Scene units within which a click on an existing path is
|
||||
// treated as "on the first node" and closes the shape, rather
|
||||
// than adding yet another node right next to it.
|
||||
static constexpr qreal CLOSE_THRESHOLD = 12.0;
|
||||
};
|
||||
|
||||
#endif // DIAGRAMEVENTADDPATH_H
|
||||
@@ -19,8 +19,16 @@
|
||||
|
||||
#include "../diagram.h"
|
||||
#include "../lastusedstyle.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qetdiagrameditor.h"
|
||||
#include "../undocommand/addgraphicsobjectcommand.h"
|
||||
|
||||
#include <QGraphicsEllipseItem>
|
||||
#include <QGuiApplication>
|
||||
#include <QKeyEvent>
|
||||
#include <QStatusBar>
|
||||
#include <QTimer>
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddShape::DiagramEventAddShape
|
||||
Default constructor
|
||||
@@ -36,6 +44,18 @@ DiagramEventAddShape::DiagramEventAddShape(Diagram *diagram, QetShapeItem::Shape
|
||||
{
|
||||
m_running = true;
|
||||
init();
|
||||
// Deferred to the next event-loop iteration, not shown immediately:
|
||||
// Diagram::setEventInterface() destroys whatever tool was
|
||||
// previously active *after* this constructor returns, and that
|
||||
// previous tool's own destructor clears the status bar (see
|
||||
// ~DiagramEventAddShape() below) -- an immediate show here would
|
||||
// just get wiped out moments later by that cleanup. Letting the old
|
||||
// tool's teardown finish first, then showing this one's message, is
|
||||
// the same fix already used for the tooltip-flicker issue in
|
||||
// QetShapeItem::refreshInteractionHints(), applied to the same
|
||||
// class of "something later in the same call chain undoes what I
|
||||
// just did" ordering problem.
|
||||
QTimer::singleShot(0, this, [this]() { updateCreationHint(); });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,11 +70,123 @@ DiagramEventAddShape::~DiagramEventAddShape()
|
||||
}
|
||||
delete m_help_horiz;
|
||||
delete m_help_verti;
|
||||
delete m_center_marker;
|
||||
|
||||
if (m_diagram && !m_diagram->views().isEmpty())
|
||||
{
|
||||
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
|
||||
editor->statusBar()->clearMessage();
|
||||
}
|
||||
|
||||
foreach (QGraphicsView *v, m_diagram->views())
|
||||
v->setContextMenuPolicy(Qt::DefaultContextMenu);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddShape::applyPosition
|
||||
Applies a drag/click position to the in-progress shape, honouring two
|
||||
modifiers that mirror how the very same shape can already be edited
|
||||
afterward, once placed:
|
||||
- Ctrl, for Rectangle/Ellipse only: the first click becomes the
|
||||
shape's *center* rather than a corner, growing symmetrically as
|
||||
the cursor moves away from it -- the same meaning Ctrl already
|
||||
has on a Resize handle (anchor at center). Deliberately not
|
||||
offered for Line: unlike the Rectangle/Ellipse case, there's no
|
||||
established convention for "a line grows symmetrically from its
|
||||
middle" to justify it by, so Ctrl for Line means only free
|
||||
positioning (see the plain grid-snap check above), nothing more.
|
||||
- Shift, for Rectangle/Ellipse only: forces the bounding box square
|
||||
(so Ellipse becomes a true circle), using whichever of the two
|
||||
dragged dimensions is currently larger and mirroring that onto
|
||||
the other, preserving the direction the user is actually
|
||||
dragging in.
|
||||
Both can combine (Ctrl+Shift: a centered square/circle). Whether or
|
||||
not Ctrl is currently held, the non-anchored branch always rebuilds
|
||||
from m_anchor_point rather than nudging the existing rect/line --
|
||||
otherwise, if Ctrl had been held earlier in the same drag (moving the
|
||||
shape's own first point to a mirrored position), releasing it would
|
||||
leave that point stuck there instead of actually restoring it.
|
||||
*/
|
||||
void DiagramEventAddShape::applyPosition(const QPointF &pos, Qt::KeyboardModifiers mods)
|
||||
{
|
||||
if (!m_shape_item)
|
||||
return;
|
||||
|
||||
if (m_shape_type == QetShapeItem::Polygon)
|
||||
{
|
||||
// setP2() has its own dedicated Polygon handling: it moves the
|
||||
// *last* vertex in place rather than setting a second point,
|
||||
// which is exactly the live "next segment follows the mouse"
|
||||
// preview -- the same idea DiagramEventAddPath's trailing
|
||||
// preview node gives the pen tool. No Ctrl/Shift modifiers apply
|
||||
// here (those are Rectangle/Ellipse/Line-specific below), so
|
||||
// this is a direct, unconditional call.
|
||||
m_shape_item->setP2(pos);
|
||||
return;
|
||||
}
|
||||
|
||||
// m_center_anchored is decided once, in mousePressEvent, not
|
||||
// re-checked here on every call -- re-checking it live meant
|
||||
// releasing Ctrl mid-drag (something you'd naturally do the moment
|
||||
// your hand gets tired holding it, long before you're done resizing)
|
||||
// silently snapped the shape back to corner-anchored, discarding
|
||||
// what felt like an already-made decision. Deciding it once at the
|
||||
// first click matches "I held Ctrl when I clicked, so this shape is
|
||||
// centered" -- a single, predictable rule instead of a live toggle.
|
||||
QPointF target = pos;
|
||||
|
||||
if ((mods & Qt::ShiftModifier)
|
||||
&& (m_shape_type == QetShapeItem::Rectangle || m_shape_type == QetShapeItem::Ellipse))
|
||||
{
|
||||
const QPointF ref = m_anchor_point;
|
||||
const qreal dx = target.x() - ref.x();
|
||||
const qreal dy = target.y() - ref.y();
|
||||
const qreal size = qMax(qAbs(dx), qAbs(dy));
|
||||
target.setX(ref.x() + (dx < 0 ? -size : size));
|
||||
target.setY(ref.y() + (dy < 0 ? -size : size));
|
||||
}
|
||||
|
||||
if (m_center_anchored)
|
||||
{
|
||||
const QPointF mirrored = 2 * m_anchor_point - target;
|
||||
m_shape_item->setRect(QRectF(mirrored, target));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_shape_type == QetShapeItem::Line)
|
||||
m_shape_item->setLine(QLineF(m_anchor_point, target));
|
||||
else
|
||||
m_shape_item->setRect(QRectF(m_anchor_point, target));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddShape::showCenterMarker
|
||||
Small, filled marker at the anchor point, shown only while Ctrl-
|
||||
anchoring is actually in effect right now (see applyPosition()) --
|
||||
doubling as live confirmation that it is, rather than leaving the
|
||||
user to infer it purely from how the shape happens to be growing.
|
||||
*/
|
||||
void DiagramEventAddShape::showCenterMarker(const QPointF &scenePos)
|
||||
{
|
||||
if (!m_center_marker)
|
||||
{
|
||||
m_center_marker = new QGraphicsEllipseItem(-4, -4, 8, 8);
|
||||
QPen pen(Qt::red);
|
||||
pen.setCosmetic(true);
|
||||
m_center_marker->setPen(pen);
|
||||
m_center_marker->setBrush(Qt::red);
|
||||
m_diagram->addItem(m_center_marker);
|
||||
}
|
||||
m_center_marker->setPos(scenePos);
|
||||
}
|
||||
|
||||
void DiagramEventAddShape::hideCenterMarker()
|
||||
{
|
||||
delete m_center_marker;
|
||||
m_center_marker = nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddShape::mousePressEvent
|
||||
Action when mouse is pressed
|
||||
@@ -67,7 +199,14 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
}
|
||||
|
||||
QPointF pos = event->scenePos();
|
||||
if (event->modifiers() != Qt::ControlModifier) {
|
||||
// A bitwise flag check, not exact equality: modifiers() == Ctrl
|
||||
// alone fails the moment any other key (Shift for the square/circle
|
||||
// lock, or an incidental platform flag) is also held, silently
|
||||
// falling through to snapToGrid() even though Ctrl is held --
|
||||
// exactly what made Ctrl+Shift together feel "frozen" (both grid-
|
||||
// snapped *and* square-locked, quantizing to whichever is coarser)
|
||||
// and made "Ctrl = free positioning" not actually hold up.
|
||||
if (!(event->modifiers() & Qt::ControlModifier)) {
|
||||
pos = Diagram::snapToGrid(pos);
|
||||
}
|
||||
|
||||
@@ -78,6 +217,15 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
if (!m_shape_item)
|
||||
{
|
||||
m_shape_item = new QetShapeItem(pos, pos, m_shape_type);
|
||||
m_anchor_point = pos;
|
||||
// Decided once, here, rather than re-checked on every mouse
|
||||
// move for the rest of the drag -- see applyPosition()'s doc
|
||||
// comment for why continuous re-checking made releasing Ctrl
|
||||
// mid-drag feel like a bug rather than a deliberate choice.
|
||||
m_center_anchored = (event->modifiers() & Qt::ControlModifier)
|
||||
&& (m_shape_type == QetShapeItem::Rectangle || m_shape_type == QetShapeItem::Ellipse);
|
||||
if (m_center_anchored)
|
||||
showCenterMarker(m_anchor_point);
|
||||
//Start from whatever pen/brush was last applied this
|
||||
//session, rather than always the hardcoded default.
|
||||
if (LastUsedStyle::hasShapePen()) {
|
||||
@@ -87,6 +235,7 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
m_shape_item->setBrush(LastUsedStyle::shapeBrush());
|
||||
}
|
||||
m_diagram->addItem (m_shape_item);
|
||||
updateCreationHint();
|
||||
event->setAccepted(true);
|
||||
return;
|
||||
}
|
||||
@@ -94,12 +243,14 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
//If current item isn't a polyline, add it with an undo command
|
||||
if (m_shape_type != QetShapeItem::Polygon)
|
||||
{
|
||||
m_shape_item->setP2 (pos);
|
||||
applyPosition(pos, event->modifiers());
|
||||
if (m_shape_item->shapeType() == QetShapeItem::Rectangle || m_shape_item->shapeType() == QetShapeItem::Ellipse) {
|
||||
m_shape_item->setRect(m_shape_item->rect().normalized());
|
||||
}
|
||||
m_diagram->undoStack().push (new AddGraphicsObjectCommand(m_shape_item, m_diagram));
|
||||
m_shape_item = nullptr; //< set to nullptr for create new shape at next left clic
|
||||
hideCenterMarker();
|
||||
updateCreationHint();
|
||||
}
|
||||
//Else add a new point to polyline
|
||||
else
|
||||
@@ -125,18 +276,76 @@ void DiagramEventAddShape::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
updateHelpCross(event->scenePos());
|
||||
|
||||
// Re-asserted on every move, not just once at activation: Qt's own
|
||||
// built-in "show an action's statusTip on hover" has its own
|
||||
// internal "restore whatever was there before" logic for when the
|
||||
// hover ends. Since our message is shown *during* that same hover
|
||||
// session (the user is still over the toolbar icon when the
|
||||
// deferred constructor-time call fires), Qt's hover-tracking has no
|
||||
// idea we changed the status bar in the meantime -- the moment the
|
||||
// mouse leaves the icon for the canvas, it "restores" to whatever it
|
||||
// remembers being there before its own tip started, which is stale
|
||||
// and empty, silently overwriting ours. Re-showing it here, on every
|
||||
// move within the canvas, simply outlasts that one-time restore.
|
||||
updateCreationHint();
|
||||
|
||||
if (m_shape_item && event->buttons() == Qt::NoButton)
|
||||
{
|
||||
m_last_mouse_scene_pos = event->scenePos(); // raw, before snapping -- see reapplyLastPosition()
|
||||
|
||||
QPointF pos = event->scenePos();
|
||||
if (event->modifiers() != Qt::ControlModifier) {
|
||||
if (!(event->modifiers() & Qt::ControlModifier)) {
|
||||
pos = Diagram::snapToGrid(pos);
|
||||
}
|
||||
|
||||
m_shape_item->setP2 (pos);
|
||||
applyPosition(pos, event->modifiers());
|
||||
event->setAccepted(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddShape::keyPressEvent / keyReleaseEvent
|
||||
Pressing or releasing Shift (the square/circle lock) does nothing
|
||||
visible on its own -- applyPosition() only ever runs from
|
||||
mouseMoveEvent, so without this, a keyboard-only change just sits
|
||||
there until the next, often incidental, pixel of mouse movement
|
||||
brings the shape in line with it. That's exactly what looked like a
|
||||
freeze: holding Shift while the mouse is genuinely still produces no
|
||||
visible change (correctly -- nothing has moved), and it only
|
||||
"unsticks" once the mouse moves again, which released keys tend to
|
||||
coincide with purely by hand tremor, not because releasing itself
|
||||
did anything. Re-running the last known mouse position through
|
||||
applyPosition() here makes the key press or release itself the
|
||||
trigger, giving immediate feedback instead of waiting on chance.
|
||||
*/
|
||||
void DiagramEventAddShape::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
reapplyLastPosition(event);
|
||||
}
|
||||
|
||||
void DiagramEventAddShape::keyReleaseEvent(QKeyEvent *event)
|
||||
{
|
||||
reapplyLastPosition(event);
|
||||
}
|
||||
|
||||
void DiagramEventAddShape::reapplyLastPosition(QKeyEvent *event)
|
||||
{
|
||||
if (!m_shape_item || (event->key() != Qt::Key_Shift && event->key() != Qt::Key_Control))
|
||||
return;
|
||||
|
||||
// A fresh, global query, not event->modifiers(): for a press/release
|
||||
// of a modifier key itself, whether that key is already reflected in
|
||||
// the key event's own modifiers() is ambiguous and platform-
|
||||
// dependent -- the same reason Diagram::snapToGrid() queries this
|
||||
// directly rather than trusting a passed-in modifiers() value.
|
||||
const Qt::KeyboardModifiers mods = QGuiApplication::keyboardModifiers();
|
||||
QPointF pos = m_last_mouse_scene_pos;
|
||||
if (!(mods & Qt::ControlModifier))
|
||||
pos = Diagram::snapToGrid(pos);
|
||||
|
||||
applyPosition(pos, mods);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddShape::mouseReleaseEvent
|
||||
Action when mouse button is released
|
||||
@@ -155,7 +364,7 @@ void DiagramEventAddShape::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||
m_shape_item->removePoints();
|
||||
|
||||
QPointF pos = event->scenePos();
|
||||
if (event->modifiers() != Qt::ControlModifier)
|
||||
if (!(event->modifiers() & Qt::ControlModifier))
|
||||
pos = Diagram::snapToGrid(pos);
|
||||
|
||||
m_shape_item->setP2(pos); //Set the new last point under the cursor
|
||||
@@ -167,6 +376,8 @@ void DiagramEventAddShape::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||
m_diagram->removeItem(m_shape_item);
|
||||
delete m_shape_item;
|
||||
m_shape_item = nullptr;
|
||||
hideCenterMarker();
|
||||
updateCreationHint();
|
||||
event->setAccepted(true);
|
||||
return;
|
||||
}
|
||||
@@ -203,6 +414,8 @@ void DiagramEventAddShape::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event
|
||||
}
|
||||
m_diagram->undoStack().push (new AddGraphicsObjectCommand(m_shape_item, m_diagram));
|
||||
m_shape_item = nullptr; //< set to nullptr for create new shape at next left clic
|
||||
hideCenterMarker();
|
||||
updateCreationHint();
|
||||
event->setAccepted(true);
|
||||
}
|
||||
}
|
||||
@@ -213,6 +426,59 @@ void DiagramEventAddShape::init()
|
||||
v->setContextMenuPolicy(Qt::NoContextMenu);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddShape::updateCreationHint
|
||||
Shows whichever of beforeClickHint()/afterClickHint() matches the
|
||||
current phase -- there was previously either no message at all
|
||||
(Line/Rectangle/Ellipse) or a single static one that never changed
|
||||
regardless of progress (Polygon, set externally in
|
||||
QETDiagramEditor::addItemGroupTriggered()); this replaces both with
|
||||
one phase-aware message per shape type, managed by the tool itself.
|
||||
*/
|
||||
void DiagramEventAddShape::updateCreationHint() const
|
||||
{
|
||||
if (!m_diagram || m_diagram->views().isEmpty())
|
||||
return;
|
||||
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
|
||||
editor->statusBar()->showMessage(m_shape_item ? afterClickHint() : beforeClickHint());
|
||||
}
|
||||
|
||||
QString DiagramEventAddShape::beforeClickHint() const
|
||||
{
|
||||
switch (m_shape_type)
|
||||
{
|
||||
case QetShapeItem::Line:
|
||||
return tr("Clic gauche : positionner le point de départ (Ctrl = position libre)");
|
||||
case QetShapeItem::Rectangle:
|
||||
case QetShapeItem::Ellipse:
|
||||
return tr("Clic gauche : positionner le premier coin (Ctrl = point central, position libre)");
|
||||
case QetShapeItem::Polygon:
|
||||
return tr("Clic gauche : positionner le premier point (Ctrl = position libre)");
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
QString DiagramEventAddShape::afterClickHint() const
|
||||
{
|
||||
switch (m_shape_type)
|
||||
{
|
||||
case QetShapeItem::Line:
|
||||
return tr("Clic gauche : positionner le point final (Ctrl = position libre) ; clic droit : annuler");
|
||||
case QetShapeItem::Rectangle:
|
||||
return tr("Clic gauche : positionner le coin opposé (Maj = carré, "
|
||||
"Ctrl = depuis le centre + position libre, Ctrl+Maj = carré centré) ; clic droit : annuler");
|
||||
case QetShapeItem::Ellipse:
|
||||
return tr("Clic gauche : positionner le coin opposé (Maj = cercle, "
|
||||
"Ctrl = depuis le centre + position libre, Ctrl+Maj = cercle centré) ; clic droit : annuler");
|
||||
case QetShapeItem::Polygon:
|
||||
return tr("Clic gauche : point suivant ; double-clic ou Entrée : terminer ; "
|
||||
"clic droit : annuler le dernier point");
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddShape::updateHelpCross
|
||||
Create and update the position of the cross to help user for draw new shape
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#include "../qetgraphicsitem/qetshapeitem.h"
|
||||
#include "diagrameventinterface.h"
|
||||
|
||||
class QGraphicsEllipseItem;
|
||||
|
||||
/**
|
||||
@brief The DiagramEventAddShape class
|
||||
This event manage the creation of a shape.
|
||||
@@ -37,15 +39,28 @@ class DiagramEventAddShape : public DiagramEventInterface
|
||||
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseDoubleClickEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void keyPressEvent (QKeyEvent *event) override;
|
||||
void keyReleaseEvent (QKeyEvent *event) override;
|
||||
void init() override;
|
||||
|
||||
private:
|
||||
void updateHelpCross (const QPointF &p);
|
||||
void applyPosition (const QPointF &pos, Qt::KeyboardModifiers mods);
|
||||
void updateCreationHint () const;
|
||||
QString beforeClickHint () const;
|
||||
QString afterClickHint () const;
|
||||
void showCenterMarker (const QPointF &scenePos);
|
||||
void hideCenterMarker ();
|
||||
void reapplyLastPosition (QKeyEvent *event);
|
||||
|
||||
protected:
|
||||
QetShapeItem::ShapeType m_shape_type;
|
||||
QetShapeItem *m_shape_item;
|
||||
QGraphicsLineItem *m_help_horiz, *m_help_verti;
|
||||
QPointF m_anchor_point; // the shape's first-click point -- meaningful once m_shape_item exists
|
||||
QGraphicsEllipseItem *m_center_marker = nullptr; // shown only while Ctrl-anchoring is actually in effect, so it doubles as confirmation that it is
|
||||
bool m_center_anchored = false; // decided once, at the first click -- see applyPosition()'s doc comment for why
|
||||
QPointF m_last_mouse_scene_pos; // raw, unsnapped -- lets a modifier-only change re-snap correctly when reapplied
|
||||
};
|
||||
|
||||
#endif // DIAGRAMEVENTADDSHAPE_H
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
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 DIAGRAMSORTKEYS_H
|
||||
#define DIAGRAMSORTKEYS_H
|
||||
|
||||
#include <QPointF>
|
||||
#include <QString>
|
||||
#include <QtGlobal>
|
||||
|
||||
/// Sort-key helpers used by Diagram::toXml() to serialize elements and
|
||||
/// conductors in a deterministic order. Pulled into their own header
|
||||
/// (instead of an anonymous namespace in diagram.cpp) so they can be
|
||||
/// unit-tested directly -- see tests/qttest/tst_diagramsortkeys.cpp.
|
||||
namespace DiagramSortKeys {
|
||||
|
||||
/// Format a coordinate as a string that sorts the same way the number
|
||||
/// does. Plain fixed-precision formatting ("%.4f") does NOT do this --
|
||||
/// e.g. "15.0000" sorts before "5.0000" as text even though 15 > 5 --
|
||||
/// so shift into a non-negative range and zero-pad to a fixed width
|
||||
/// before comparing.
|
||||
inline QString coordinateKey(double v)
|
||||
{
|
||||
//Diagram coordinates are nowhere near this range; the offset and
|
||||
//width just need to be big enough that shifted values are always
|
||||
//non-negative and always the same digit count.
|
||||
constexpr double offset = 1e9;
|
||||
qint64 scaled = qint64(qRound64((v + offset) * 10000.0));
|
||||
return QStringLiteral("%1").arg(scaled, 20, 10, QLatin1Char('0'));
|
||||
}
|
||||
|
||||
inline QString positionKey(const QPointF &pos)
|
||||
{
|
||||
return coordinateKey(pos.x()) + QLatin1Char('|') + coordinateKey(pos.y());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -334,6 +334,13 @@ void DiagramView::setSelectionMode()
|
||||
*/
|
||||
void DiagramView::zoom(const qreal zoom_factor)
|
||||
{
|
||||
// clamp the resulting scale so a repeated wheel-zoom cannot drive the view
|
||||
// transform to floating-point overflow and crash the editor (issue #798)
|
||||
const qreal target = transform().m11() * zoom_factor;
|
||||
if (target < m_min_zoom || target > m_max_zoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (zoom_factor >= 1){
|
||||
scale(zoom_factor, zoom_factor);
|
||||
}
|
||||
|
||||
@@ -103,6 +103,12 @@ class DiagramView : public QGraphicsView
|
||||
bool mustIntegrateTitleBlockTemplate(const TitleBlockTemplateLocation &) const;
|
||||
bool gestures() const;
|
||||
|
||||
/// Lowest and highest allowed value of the view transform scale (m11).
|
||||
/// Prevents wheel-zoom from driving the transform to overflow, which
|
||||
/// crashes the editor (see GitHub issue #798, same class of bug).
|
||||
static constexpr qreal m_min_zoom = 0.01;
|
||||
static constexpr qreal m_max_zoom = 200.0;
|
||||
|
||||
signals:
|
||||
/// Signal emitted after the selection mode changed
|
||||
void modeChanged();
|
||||
|
||||
@@ -200,18 +200,35 @@ void DxfPaintEngine::drawTextItem(const QPointF &p, const QTextItem &textItem)
|
||||
|
||||
/**
|
||||
@brief DxfPaintEngine::drawPixmap
|
||||
Not implemented in v1 - out of scope per the design note in the
|
||||
header (CrossRefItem, the only item exported through this engine so
|
||||
far, never draws a pixmap). qWarning() rather than a hard failure, so
|
||||
an item that does call this in the future degrades to "one entity
|
||||
missing" instead of crashing the whole export.
|
||||
No DXF dialect this old (AC1006, AutoCAD R10 from 1988) has any
|
||||
raster image representation at all -- IMAGE/IMAGEDEF wasn't
|
||||
introduced until R2000, over a decade later, and even there the
|
||||
pixels are never embedded, only referenced by external file path.
|
||||
Actually supporting images means upgrading the DXF version target
|
||||
and managing a second file alongside the DXF; until then, this
|
||||
draws a placeholder rectangle outline (the item's own destination
|
||||
rect, mapped through m_world_transform exactly like drawRects()
|
||||
does) instead of silently dropping the item -- so its position,
|
||||
size, rotation, and skew all survive the export even though the
|
||||
picture itself can't yet. A fixed, plain pen, not m_pen: an image
|
||||
item has no meaningful pen of its own for this engine to have
|
||||
picked up from a prior updateState().
|
||||
*/
|
||||
void DxfPaintEngine::drawPixmap(const QRectF &r, const QPixmap &pm, const QRectF &sr)
|
||||
{
|
||||
Q_UNUSED(r)
|
||||
Q_UNUSED(pm)
|
||||
Q_UNUSED(sr)
|
||||
qWarning() << "DxfPaintEngine::drawPixmap: not supported, entity skipped";
|
||||
|
||||
QPolygonF corners;
|
||||
corners << r.topLeft() << r.topRight() << r.bottomRight() << r.bottomLeft();
|
||||
|
||||
QPolygonF mapped;
|
||||
mapped.reserve(corners.size() + 1);
|
||||
for (const QPointF &p : corners)
|
||||
mapped << toDxf(p);
|
||||
mapped << mapped.first();
|
||||
|
||||
Createdxf::drawPolyline(m_filepath, mapped, Createdxf::dxfColor(QPen(Qt::black)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,8 +278,21 @@ int DxfPaintDevice::metric(PaintDeviceMetric metric) const
|
||||
case PdmDepth:
|
||||
return 24;
|
||||
case PdmDevicePixelRatio:
|
||||
case PdmDevicePixelRatioScaled:
|
||||
return 1;
|
||||
case PdmDevicePixelRatioScaled:
|
||||
// Not just 1: Qt's own convention (see QPaintDevice's docs)
|
||||
// is that this metric equals PdmDevicePixelRatio scaled by
|
||||
// devicePixelRatioFScale() -- a large constant (65536)
|
||||
// used internally for sub-integer precision. Returning the
|
||||
// plain, unscaled ratio here (as this used to) meant
|
||||
// QPainter's own transform bookkeeping divided by it
|
||||
// expecting the scaled value, silently shrinking every
|
||||
// drawPixmap() destination rect by a factor of 65536 --
|
||||
// this is what was actually behind the placeholder
|
||||
// rectangle rendering at a barely-visible fraction of its
|
||||
// real size instead of the size the item's own
|
||||
// sceneTransform() correctly specified.
|
||||
return int(1 * QPaintDevice::devicePixelRatioFScale());
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -56,11 +56,18 @@
|
||||
- fillPath -> same outline-only handling as drawRects; no HATCH
|
||||
support in v1 (see design note in the PR)
|
||||
|
||||
Anything outside this list (images, gradients, etc.) is intentionally
|
||||
unimplemented and asserts in debug builds rather than silently
|
||||
producing an incomplete drawing - callers should know immediately if
|
||||
an item they're exporting uses something this engine doesn't cover
|
||||
yet, rather than getting a DXF file quietly missing content.
|
||||
- drawPixmap -> no raster image entity exists in this DXF
|
||||
dialect at all (see drawPixmap()'s own
|
||||
comment) -- draws a placeholder rectangle
|
||||
outline instead, preserving the item's
|
||||
position/size/rotation/skew even though the
|
||||
picture itself can't be included yet
|
||||
|
||||
Anything else outside this list (gradients, etc.) is intentionally
|
||||
unimplemented and logs a warning rather than silently producing an
|
||||
incomplete drawing - callers should know immediately if an item
|
||||
they're exporting uses something this engine doesn't cover yet,
|
||||
rather than getting a DXF file quietly missing content.
|
||||
*/
|
||||
class DxfPaintEngine : public QPaintEngine
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "editorcommands.h"
|
||||
#include "../diagram.h"
|
||||
|
||||
/**
|
||||
@brief ElementEditionCommand::ElementEditionCommand
|
||||
@@ -666,10 +667,32 @@ void RotateFineElementsCommand::redo()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@brief selectionCenter
|
||||
@param items
|
||||
@return the center of the united scene bounding rect of items,
|
||||
snapped to the nearest half of the diagram grid. Mirroring across a
|
||||
half-grid line keeps points that were on the grid on the grid.
|
||||
*/
|
||||
static QPointF selectionCenter(const QList<QGraphicsItem *> &items)
|
||||
{
|
||||
QRectF bounding;
|
||||
for (auto *item : items) {
|
||||
bounding = bounding.united(item->sceneBoundingRect());
|
||||
}
|
||||
QPointF center = bounding.center();
|
||||
const qreal half_x = Diagram::xGrid / 2.0;
|
||||
const qreal half_y = Diagram::yGrid / 2.0;
|
||||
center.setX(qRound(center.x() / half_x) * half_x);
|
||||
center.setY(qRound(center.y() / half_y) * half_y);
|
||||
return center;
|
||||
}
|
||||
|
||||
MirrorElementsCommand::MirrorElementsCommand(ElementScene *scene, QUndoCommand *parent) :
|
||||
ElementEditionCommand(QObject::tr("Miroir de sélection", "undo caption"), scene, nullptr, parent)
|
||||
{
|
||||
m_items = scene->selectedItems();
|
||||
m_axis_x = selectionCenter(m_items).x();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -680,28 +703,28 @@ void MirrorElementsCommand::redo()
|
||||
foreach (auto *item, m_items) {
|
||||
if (item->type() == PartText::Type) {
|
||||
PartText* staticText = qgraphicsitem_cast<PartText*>(item);
|
||||
staticText->mirror();
|
||||
staticText->mirror(m_axis_x);
|
||||
} else if (item->type() == PartDynamicTextField::Type) {
|
||||
PartDynamicTextField* dyntext = qgraphicsitem_cast<PartDynamicTextField*>(item);
|
||||
dyntext->mirror();
|
||||
dyntext->mirror(m_axis_x);
|
||||
} else if (item->type() == PartArc::Type) {
|
||||
PartArc* arc = qgraphicsitem_cast<PartArc*>(item);
|
||||
arc->mirror();
|
||||
arc->mirror(m_axis_x);
|
||||
} else if (item->type() == PartEllipse::Type) {
|
||||
PartEllipse* ellipse = qgraphicsitem_cast<PartEllipse*>(item);
|
||||
ellipse->mirror();
|
||||
ellipse->mirror(m_axis_x);
|
||||
} else if (item->type() == PartLine::Type) {
|
||||
PartLine* line = qgraphicsitem_cast<PartLine*>(item);
|
||||
line->mirror();
|
||||
line->mirror(m_axis_x);
|
||||
} else if (item->type() == PartPolygon::Type) {
|
||||
PartPolygon* poly = qgraphicsitem_cast<PartPolygon*>(item);
|
||||
poly->mirror();
|
||||
poly->mirror(m_axis_x);
|
||||
} else if (item->type() == PartRectangle::Type) {
|
||||
PartRectangle* rect = qgraphicsitem_cast<PartRectangle*>(item);
|
||||
rect->mirror();
|
||||
rect->mirror(m_axis_x);
|
||||
} else if (item->type() == PartTerminal::Type) {
|
||||
PartTerminal* term = qgraphicsitem_cast<PartTerminal*>(item);
|
||||
term->mirror();
|
||||
term->mirror(m_axis_x);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -718,6 +741,7 @@ FlipElementsCommand::FlipElementsCommand(ElementScene *scene, QUndoCommand *pare
|
||||
ElementEditionCommand(QObject::tr("Retourner la sélection", "undo caption"), scene, nullptr, parent)
|
||||
{
|
||||
m_items = scene->selectedItems();
|
||||
m_axis_y = selectionCenter(m_items).y();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -728,28 +752,28 @@ void FlipElementsCommand::redo()
|
||||
foreach (auto *item, m_items) {
|
||||
if (item->type() == PartText::Type) {
|
||||
PartText* staticText = qgraphicsitem_cast<PartText*>(item);
|
||||
staticText->flip();
|
||||
staticText->flip(m_axis_y);
|
||||
} else if (item->type() == PartDynamicTextField::Type) {
|
||||
PartDynamicTextField* dyntext = qgraphicsitem_cast<PartDynamicTextField*>(item);
|
||||
dyntext->flip();
|
||||
dyntext->flip(m_axis_y);
|
||||
} else if (item->type() == PartArc::Type) {
|
||||
PartArc* arc = qgraphicsitem_cast<PartArc*>(item);
|
||||
arc->flip();
|
||||
arc->flip(m_axis_y);
|
||||
} else if (item->type() == PartEllipse::Type) {
|
||||
PartEllipse* ellipse = qgraphicsitem_cast<PartEllipse*>(item);
|
||||
ellipse->flip();
|
||||
ellipse->flip(m_axis_y);
|
||||
} else if (item->type() == PartLine::Type) {
|
||||
PartLine* line = qgraphicsitem_cast<PartLine*>(item);
|
||||
line->flip();
|
||||
line->flip(m_axis_y);
|
||||
} else if (item->type() == PartPolygon::Type) {
|
||||
PartPolygon* poly = qgraphicsitem_cast<PartPolygon*>(item);
|
||||
poly->flip();
|
||||
poly->flip(m_axis_y);
|
||||
} else if (item->type() == PartRectangle::Type) {
|
||||
PartRectangle* rect = qgraphicsitem_cast<PartRectangle*>(item);
|
||||
rect->flip();
|
||||
rect->flip(m_axis_y);
|
||||
} else if (item->type() == PartTerminal::Type) {
|
||||
PartTerminal* term = qgraphicsitem_cast<PartTerminal*>(item);
|
||||
term->flip();
|
||||
term->flip(m_axis_y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,6 +279,12 @@ private:
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
@brief The MirrorElementsCommand class
|
||||
Mirror the selected parts horizontally (left <-> right) across the
|
||||
vertical line through the center of the selection, so the selection
|
||||
keeps its place in the scene.
|
||||
*/
|
||||
class MirrorElementsCommand : public ElementEditionCommand
|
||||
{
|
||||
public:
|
||||
@@ -288,8 +294,15 @@ public:
|
||||
private:
|
||||
ElementScene *m_scene =nullptr;
|
||||
QList<QGraphicsItem*> m_items;
|
||||
qreal m_axis_x = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
@brief The FlipElementsCommand class
|
||||
Flip the selected parts vertically (top <-> bottom) across the
|
||||
horizontal line through the center of the selection, so the selection
|
||||
keeps its place in the scene.
|
||||
*/
|
||||
class FlipElementsCommand : public ElementEditionCommand
|
||||
{
|
||||
public:
|
||||
@@ -299,6 +312,7 @@ public:
|
||||
private:
|
||||
ElementScene *m_scene =nullptr;
|
||||
QList<QGraphicsItem*> m_items;
|
||||
qreal m_axis_y = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -100,13 +100,30 @@ void ElementView::setSelectionMode()
|
||||
emit(modeChanged());
|
||||
}
|
||||
|
||||
/**
|
||||
Applique un facteur d'echelle a la vue en bornant l'echelle resultante
|
||||
entre m_min_zoom et m_max_zoom. Sans cette borne, un zoom repete (molette)
|
||||
finit par faire deborder la transformation de la vue et fait planter
|
||||
l'editeur (issue #798).
|
||||
@param factor facteur d'echelle a appliquer
|
||||
*/
|
||||
void ElementView::scaleClamped(qreal factor)
|
||||
{
|
||||
const qreal current = transform().m11();
|
||||
const qreal target = current * factor;
|
||||
if (target < m_min_zoom || target > m_max_zoom) {
|
||||
return;
|
||||
}
|
||||
scale(factor, factor);
|
||||
}
|
||||
|
||||
/**
|
||||
Agrandit le schema (+33% = inverse des -25 % de zoomMoins())
|
||||
*/
|
||||
void ElementView::zoomIn()
|
||||
{
|
||||
adjustSceneRect();
|
||||
scale(4.0/3.0, 4.0/3.0);
|
||||
scaleClamped(4.0/3.0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,7 +132,7 @@ void ElementView::zoomIn()
|
||||
void ElementView::zoomOut()
|
||||
{
|
||||
adjustSceneRect();
|
||||
scale(0.75, 0.75);
|
||||
scaleClamped(0.75);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,7 +140,7 @@ void ElementView::zoomOut()
|
||||
*/
|
||||
void ElementView::zoomInSlowly()
|
||||
{
|
||||
scale(1.02, 1.02);
|
||||
scaleClamped(1.02);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,7 +148,7 @@ void ElementView::zoomInSlowly()
|
||||
*/
|
||||
void ElementView::zoomOutSlowly()
|
||||
{
|
||||
scale(0.98, 0.98);
|
||||
scaleClamped(0.98);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -53,6 +53,13 @@ class ElementView : public QGraphicsView {
|
||||
|
||||
private:
|
||||
QRectF applyMovement(const QRectF &, const QPointF &);
|
||||
void scaleClamped(qreal factor);
|
||||
|
||||
/// Lowest and highest allowed value of the view transform scale (m11).
|
||||
/// Prevents the wheel-zoom from driving the transform to overflow, which
|
||||
/// crashes the editor (bugtracker / GitHub issue #798).
|
||||
static constexpr qreal m_min_zoom = 0.1;
|
||||
static constexpr qreal m_max_zoom = 200.0;
|
||||
|
||||
public slots:
|
||||
void setVisualisationMode();
|
||||
|
||||
@@ -197,13 +197,13 @@ qreal PartArc::rotation() const {
|
||||
return qRound(m_rot * 100.0) / 100.0;
|
||||
}
|
||||
|
||||
void PartArc::flip() {
|
||||
void PartArc::flip(qreal axis_y) {
|
||||
m_start_angle = (-1) * m_start_angle;
|
||||
m_span_angle = (-1) * m_span_angle;
|
||||
while (m_start_angle < 0) { m_start_angle += (360*16); }
|
||||
while (m_start_angle >= (360*16)) { m_start_angle -= (360*16); }
|
||||
auto p1 = mapToScene(m_rect.x(),m_rect.y());
|
||||
p1.setY(((-1.0) * p1.y()) - m_rect.height());
|
||||
p1.setY(2 * axis_y - p1.y() - m_rect.height());
|
||||
p1 = mapFromScene(p1.x(),p1.y());
|
||||
m_rect = QRectF(m_rect.x(), p1.y(), m_rect.width(), m_rect.height());
|
||||
prepareGeometryChange();
|
||||
@@ -211,13 +211,13 @@ void PartArc::flip() {
|
||||
emit rectChanged();
|
||||
}
|
||||
|
||||
void PartArc::mirror() {
|
||||
void PartArc::mirror(qreal axis_x) {
|
||||
m_start_angle = (180.0 * 16) - m_start_angle;
|
||||
m_span_angle = (-1) * m_span_angle;
|
||||
while (m_start_angle < 0) { m_start_angle += (360*16); }
|
||||
while (m_start_angle >= (360*16)) { m_start_angle -= (360*16); }
|
||||
auto p1 = mapToScene(m_rect.x(),m_rect.y());
|
||||
p1.setX(((-1.0) * p1.x()) - m_rect.width());
|
||||
p1.setX(2 * axis_x - p1.x() - m_rect.width());
|
||||
p1 = mapFromScene(p1.x(), p1.y());
|
||||
m_rect = QRectF(p1.x(), m_rect.y(), m_rect.width(), m_rect.height());
|
||||
prepareGeometryChange();
|
||||
|
||||
@@ -62,8 +62,8 @@ class PartArc : public AbstractPartEllipse
|
||||
QRectF sceneGeometricRect() const override;
|
||||
void setRotation(qreal angle);
|
||||
qreal rotation() const;
|
||||
void flip();
|
||||
void mirror();
|
||||
void flip(qreal axis_y = 0);
|
||||
void mirror(qreal axis_x = 0);
|
||||
|
||||
void addHandler() override;
|
||||
void removeHandler() override;
|
||||
|
||||
@@ -74,19 +74,19 @@ void PartDynamicTextField::setRotation(qreal angle) {
|
||||
setPos(QTransform().rotate(diffAngle).map(pos()));
|
||||
}
|
||||
|
||||
void PartDynamicTextField::mirror() {
|
||||
void PartDynamicTextField::mirror(qreal axis_x) {
|
||||
// at first: rotate the text:
|
||||
QGraphicsObject::setRotation(QET::correctAngle(360-rotation(), true));
|
||||
// then see, where we need to re-position depending on the angle!
|
||||
qreal rot = qRound(QET::correctAngle(rotation(), true));
|
||||
qreal c = qCos(qDegreesToRadians(rot));
|
||||
qreal s = qSin(qDegreesToRadians(rot));
|
||||
qreal x = (-1) * pos().x() - c * boundingRect().width();
|
||||
qreal x = 2 * axis_x - pos().x() - c * boundingRect().width();
|
||||
qreal y = pos().y() - s * boundingRect().width();
|
||||
setPos(x, y);
|
||||
}
|
||||
|
||||
void PartDynamicTextField::flip() {
|
||||
void PartDynamicTextField::flip(qreal axis_y) {
|
||||
// at first: rotate the text:
|
||||
QGraphicsObject::setRotation(QET::correctAngle(360-rotation(), true));
|
||||
// then see, where we need to re-position depending on the angle!
|
||||
@@ -94,7 +94,7 @@ void PartDynamicTextField::flip() {
|
||||
qreal c = qCos(qDegreesToRadians(rot));
|
||||
qreal s = qSin(qDegreesToRadians(rot));
|
||||
qreal x = pos().x() + s * boundingRect().height();
|
||||
qreal y = (-1) * pos().y() - c * boundingRect().height();
|
||||
qreal y = 2 * axis_y - pos().y() - c * boundingRect().height();
|
||||
setPos(x, y);
|
||||
}
|
||||
|
||||
|
||||
@@ -106,8 +106,8 @@ class PartDynamicTextField : public QGraphicsTextItem, public CustomElementPart
|
||||
bool rotationPointCenter() const;
|
||||
|
||||
void setRotation(qreal angle);
|
||||
void mirror();
|
||||
void flip();
|
||||
void mirror(qreal axis_x = 0);
|
||||
void flip(qreal axis_y = 0);
|
||||
|
||||
|
||||
protected:
|
||||
|
||||
@@ -256,9 +256,9 @@ qreal PartEllipse::rotation() const {
|
||||
return qRound(m_rot * 100.0) / 100.0;
|
||||
}
|
||||
|
||||
void PartEllipse::flip() {
|
||||
void PartEllipse::flip(qreal axis_y) {
|
||||
auto p1 = mapToScene(m_rect.x(), m_rect.y());
|
||||
p1.setY(((-1.0) * p1.y()) - m_rect.height());
|
||||
p1.setY(2 * axis_y - p1.y() - m_rect.height());
|
||||
p1 = mapFromScene(p1.x(), p1.y());
|
||||
m_rect = QRectF(p1.x(), p1.y(), m_rect.width(), m_rect.height());
|
||||
prepareGeometryChange();
|
||||
@@ -266,9 +266,9 @@ void PartEllipse::flip() {
|
||||
emit rectChanged();
|
||||
}
|
||||
|
||||
void PartEllipse::mirror() {
|
||||
void PartEllipse::mirror(qreal axis_x) {
|
||||
auto p1 = mapToScene(m_rect.x(), m_rect.y());
|
||||
p1.setX(((-1.0) * p1.x()) - m_rect.width());
|
||||
p1.setX(2 * axis_x - p1.x() - m_rect.width());
|
||||
p1 = mapFromScene(p1.x(), p1.y());
|
||||
m_rect = QRectF(p1.x(), p1.y(), m_rect.width(), m_rect.height());
|
||||
prepareGeometryChange();
|
||||
|
||||
@@ -62,8 +62,8 @@ class PartEllipse : public AbstractPartEllipse
|
||||
void setRect(const QRectF &rect) override {AbstractPartEllipse::setRect(rect); adjustHandlerPos();}
|
||||
void setRotation(qreal angle);
|
||||
qreal rotation() const;
|
||||
void flip();
|
||||
void mirror();
|
||||
void flip(qreal axis_y = 0);
|
||||
void mirror(qreal axis_x = 0);
|
||||
|
||||
void addHandler() override;
|
||||
void removeHandler() override;
|
||||
|
||||
@@ -590,11 +590,11 @@ qreal PartLine::rotation() const {
|
||||
return qRound(m_rot * 100.0) / 100.0;
|
||||
}
|
||||
|
||||
void PartLine::flip() {
|
||||
void PartLine::flip(qreal axis_y) {
|
||||
auto p1 = mapToScene(m_line.p1());
|
||||
auto p2 = mapToScene(m_line.p2());
|
||||
p1 = QPointF(p1.x(), (-1) * p1.y());
|
||||
p2 = QPointF(p2.x(), (-1) * p2.y());
|
||||
p1 = QPointF(p1.x(), 2 * axis_y - p1.y());
|
||||
p2 = QPointF(p2.x(), 2 * axis_y - p2.y());
|
||||
m_line.setP1(mapFromScene(p1));
|
||||
m_line.setP2(mapFromScene(p2));
|
||||
setLine(m_line);
|
||||
@@ -603,11 +603,11 @@ void PartLine::flip() {
|
||||
emit lineChanged();
|
||||
}
|
||||
|
||||
void PartLine::mirror() {
|
||||
void PartLine::mirror(qreal axis_x) {
|
||||
auto p1 = mapToScene(m_line.p1());
|
||||
auto p2 = mapToScene(m_line.p2());
|
||||
p1 = QPointF((-1) * p1.x(), p1.y());
|
||||
p2 = QPointF((-1) * p2.x(), p2.y());
|
||||
p1 = QPointF(2 * axis_x - p1.x(), p1.y());
|
||||
p2 = QPointF(2 * axis_x - p2.x(), p2.y());
|
||||
m_line.setP1(mapFromScene(p1));
|
||||
m_line.setP2(mapFromScene(p2));
|
||||
setLine(m_line);
|
||||
|
||||
@@ -96,8 +96,8 @@ class PartLine : public CustomElementGraphicPart
|
||||
void setSecondEndLength(const qreal &l);
|
||||
void setRotation(qreal angle);
|
||||
qreal rotation() const;
|
||||
void flip();
|
||||
void mirror();
|
||||
void flip(qreal axis_y = 0);
|
||||
void mirror(qreal axis_x = 0);
|
||||
|
||||
void addHandler() override;
|
||||
void removeHandler() override;
|
||||
|
||||
@@ -307,10 +307,10 @@ qreal PartPolygon::rotation() const {
|
||||
return qRound(m_rot * 100.0) / 100.0;
|
||||
}
|
||||
|
||||
void PartPolygon::flip() {
|
||||
void PartPolygon::flip(qreal axis_y) {
|
||||
for (auto &pt : m_polygon) {
|
||||
pt = mapToScene(pt.x(), pt.y());
|
||||
pt = QPointF(pt.x(), (-1) * pt.y());
|
||||
pt = QPointF(pt.x(), 2 * axis_y - pt.y());
|
||||
pt = mapFromScene(pt.x(), pt.y());
|
||||
}
|
||||
setPolygon(m_polygon);
|
||||
@@ -319,10 +319,10 @@ void PartPolygon::flip() {
|
||||
emit polygonChanged();
|
||||
}
|
||||
|
||||
void PartPolygon::mirror() {
|
||||
void PartPolygon::mirror(qreal axis_x) {
|
||||
for (auto &pt : m_polygon) {
|
||||
pt = mapToScene(pt.x(), pt.y());
|
||||
pt = QPointF((-1) * pt.x(), pt.y());
|
||||
pt = QPointF(2 * axis_x - pt.x(), pt.y());
|
||||
pt = mapFromScene(pt.x(), pt.y());
|
||||
}
|
||||
setPolygon(m_polygon);
|
||||
|
||||
@@ -89,8 +89,8 @@ class PartPolygon : public CustomElementGraphicPart
|
||||
|
||||
void setRotation (qreal angle);
|
||||
qreal rotation () const;
|
||||
void flip();
|
||||
void mirror();
|
||||
void flip(qreal axis_y = 0);
|
||||
void mirror(qreal axis_x = 0);
|
||||
|
||||
void addHandler() override;
|
||||
void removeHandler() override;
|
||||
|
||||
@@ -187,11 +187,11 @@ qreal PartRectangle::rotation() const {
|
||||
return qRound(m_rot * 100.0) / 100.0;
|
||||
}
|
||||
|
||||
void PartRectangle::flip() {
|
||||
void PartRectangle::flip(qreal axis_y) {
|
||||
auto height = m_rect.height();
|
||||
auto p1 = mapToScene(m_rect.x(),m_rect.y());
|
||||
qreal x = p1.x();
|
||||
qreal y = ((-1.0) * p1.y()) - height;
|
||||
qreal y = 2 * axis_y - p1.y() - height;
|
||||
p1 = mapFromScene(x, y);
|
||||
m_rect.setX(p1.x());
|
||||
m_rect.setY(p1.y());
|
||||
@@ -201,10 +201,10 @@ void PartRectangle::flip() {
|
||||
emit rectChanged();
|
||||
}
|
||||
|
||||
void PartRectangle::mirror() {
|
||||
void PartRectangle::mirror(qreal axis_x) {
|
||||
auto width = m_rect.width();
|
||||
auto p1 = mapToScene(m_rect.x(),m_rect.y());
|
||||
qreal x = ((-1.0) * p1.x()) - width;
|
||||
qreal x = 2 * axis_x - p1.x() - width;
|
||||
qreal y = p1.y();
|
||||
p1 = mapFromScene(x, y);
|
||||
m_rect.setX(p1.x());
|
||||
|
||||
@@ -73,8 +73,8 @@ class PartRectangle : public CustomElementGraphicPart
|
||||
void setYRadius(qreal Y);
|
||||
void setRotation(qreal angle);
|
||||
qreal rotation() const;
|
||||
void flip();
|
||||
void mirror();
|
||||
void flip(qreal axis_y = 0);
|
||||
void mirror(qreal axis_x = 0);
|
||||
|
||||
QRectF sceneGeometricRect() const override;
|
||||
virtual QPointF sceneTopLeft() const;
|
||||
|
||||
@@ -293,9 +293,9 @@ qreal PartTerminal::rotation() const {
|
||||
@brief PartTerminal::flip
|
||||
turn part upside down
|
||||
*/
|
||||
void PartTerminal::flip() {
|
||||
void PartTerminal::flip(qreal axis_y) {
|
||||
d->m_pos.setX( pos().x());
|
||||
d->m_pos.setY((-1.0) * pos().y());
|
||||
d->m_pos.setY(2 * axis_y - pos().y());
|
||||
switch (d->m_orientation) {
|
||||
case Qet::North : setOrientation(Qet::South);
|
||||
break;
|
||||
@@ -314,8 +314,8 @@ void PartTerminal::flip() {
|
||||
@brief PartTerminal::mirror
|
||||
turn part from left to right
|
||||
*/
|
||||
void PartTerminal::mirror() {
|
||||
d->m_pos.setX((-1.0) * pos().x());
|
||||
void PartTerminal::mirror(qreal axis_x) {
|
||||
d->m_pos.setX(2 * axis_x - pos().x());
|
||||
d->m_pos.setY( pos().y());
|
||||
switch (d->m_orientation) {
|
||||
case Qet::North : break;
|
||||
|
||||
@@ -96,8 +96,8 @@ class PartTerminal : public CustomElementGraphicPart
|
||||
|
||||
void setRotation(qreal angle);
|
||||
qreal rotation() const;
|
||||
void flip();
|
||||
void mirror();
|
||||
void flip(qreal axis_y = 0);
|
||||
void mirror(qreal axis_x = 0);
|
||||
void nextOrientation();
|
||||
void previousOrientation();
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ void PartText::setRotation(qreal angle) {
|
||||
setPos(QTransform().rotate(diffAngle).map(pos()));
|
||||
}
|
||||
|
||||
void PartText::mirror() {
|
||||
void PartText::mirror(qreal axis_x) {
|
||||
// at first: rotate the text:
|
||||
QGraphicsObject::setRotation(QET::correctAngle((360-rotation()), true));
|
||||
// then see, where we need to re-position depending on text, font ...
|
||||
@@ -86,12 +86,12 @@ void PartText::mirror() {
|
||||
qreal c = qCos(qDegreesToRadians(rot));
|
||||
qreal s = qSin(qDegreesToRadians(rot));
|
||||
// Now: Move!
|
||||
qreal x = (-1) * pos().x() - c * (textwidth);
|
||||
qreal x = 2 * axis_x - pos().x() - c * (textwidth);
|
||||
qreal y = pos().y() - s * (textwidth);
|
||||
setPos(x, y);
|
||||
}
|
||||
|
||||
void PartText::flip() {
|
||||
void PartText::flip(qreal axis_y) {
|
||||
// at first: rotate the text:
|
||||
QGraphicsObject::setRotation(QET::correctAngle((360-rotation()), true));
|
||||
// then see, where we need to re-position depending on text, font ...
|
||||
@@ -103,7 +103,7 @@ void PartText::flip() {
|
||||
qreal s = qSin(qDegreesToRadians(rot));
|
||||
// Now: Move!
|
||||
qreal x = pos().x() - s * (textheight);
|
||||
qreal y = (-1) * pos().y() + c * (textheight);
|
||||
qreal y = 2 * axis_y - pos().y() + c * (textheight);
|
||||
setPos(x, y);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,8 +64,8 @@ class PartText : public QGraphicsTextItem, public CustomElementPart {
|
||||
void fromXml(const QDomElement &) override;
|
||||
const QDomElement toXml(QDomDocument &) const override;
|
||||
void setRotation(qreal angle);
|
||||
void mirror();
|
||||
void flip();
|
||||
void mirror(qreal axis_x = 0);
|
||||
void flip(qreal axis_y = 0);
|
||||
bool isUseless() const override;
|
||||
QRectF sceneGeometricRect() const override;
|
||||
void startUserTransformation(const QRectF &) override;
|
||||
|
||||
@@ -744,6 +744,25 @@ void ExportDialog::generateDxf(
|
||||
painter.end();
|
||||
}
|
||||
|
||||
//Draw images -- collected above (list_images) but never actually
|
||||
//drawn until now, an existing gap this reuses the same paint()
|
||||
//-replay approach to fix: DiagramImageItem::paint() has no
|
||||
//viewport-dependent logic (unlike CrossRefItem, which needs its own
|
||||
//paintForExport() for that reason), so it's called directly with a
|
||||
//default QStyleOptionGraphicsItem rather than needing an export-
|
||||
//specific variant of its own. DxfPaintEngine::drawPixmap() is what
|
||||
//actually turns the drawPixmap() call inside paint() into a
|
||||
//placeholder outline, since this DXF dialect has no raster image
|
||||
//entity to draw instead.
|
||||
for (DiagramImageItem *image : std::as_const(list_images))
|
||||
{
|
||||
DxfPaintDevice dxf_device(file_path);
|
||||
QPainter painter(&dxf_device);
|
||||
painter.setWorldTransform(image->sceneTransform());
|
||||
image->paintForExport(&painter);
|
||||
painter.end();
|
||||
}
|
||||
|
||||
Createdxf::dxfEnd(file_path);
|
||||
|
||||
saveReloadDiagramParameters(diagram, false);
|
||||
@@ -818,6 +837,42 @@ void ExportDialog::slot_export()
|
||||
return;
|
||||
}
|
||||
|
||||
// Warn once, up front, rather than per-diagram: this DXF dialect
|
||||
// (AC1006, AutoCAD R10) has no raster image representation at all
|
||||
// (IMAGE/IMAGEDEF wasn't introduced until R2000, over a decade
|
||||
// later, and even there the picture is never embedded, only
|
||||
// referenced by external file path) -- so any image ends up as a
|
||||
// placeholder rectangle outline instead (see DxfPaintEngine::
|
||||
// drawPixmap()), with its position/size/rotation/skew preserved but
|
||||
// not its actual content.
|
||||
if (epw -> exportProperties().format.compare(QLatin1String("dxf"), Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
bool any_images = false;
|
||||
for (ExportDiagramLine *diagram_line : std::as_const(diagrams_to_export))
|
||||
{
|
||||
for (QGraphicsItem *item : diagram_line->diagram->items())
|
||||
{
|
||||
if (qgraphicsitem_cast<DiagramImageItem *>(item)) {
|
||||
any_images = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (any_images) break;
|
||||
}
|
||||
|
||||
if (any_images)
|
||||
{
|
||||
QET::QetMessageBox::warning(
|
||||
this,
|
||||
tr("Images non incluses dans l'export DXF", "message box title"),
|
||||
tr("Le format DXF utilisé ici (AC1006) ne permet pas d'inclure d'image. "
|
||||
"Les images seront représentées uniquement par un rectangle de contour "
|
||||
"(position, taille, rotation et inclinaison conservées), sans le contenu de l'image.",
|
||||
"message box content")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// exporte chaque schema a exporter
|
||||
foreach(ExportDiagramLine *diagram_line, diagrams_to_export) {
|
||||
exportDiagram(diagram_line);
|
||||
|
||||
@@ -730,11 +730,25 @@ void GenericPanel::projectInformationsChanged(QETProject *project) {
|
||||
/**
|
||||
@brief GenericPanel::diagramAdded
|
||||
@param project
|
||||
@param diagram (unused)
|
||||
@param diagram the newly added diagram to select in the tree
|
||||
*/
|
||||
void GenericPanel::diagramAdded(QETProject *project, Diagram *diagram) {
|
||||
Q_UNUSED(diagram)
|
||||
addProject(project, nullptr, GenericPanel::AddChildDiagrams);
|
||||
if (diagram) {
|
||||
QTreeWidgetItem *projectItem = itemForProject(project);
|
||||
if (projectItem) {
|
||||
for (int i = 0; i < projectItem->childCount(); ++i) {
|
||||
QTreeWidgetItem *child = projectItem->child(i);
|
||||
if (child && child->type() == QET::Diagram
|
||||
&& valueForItem<Diagram *>(child) == diagram) {
|
||||
clearSelection();
|
||||
setCurrentItem(child);
|
||||
child->setSelected(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
emit(panelContentChanged());
|
||||
}
|
||||
|
||||
|
||||
@@ -297,11 +297,19 @@ QString QET::ElementsAndConductorsSentence(
|
||||
|
||||
if (images_count) {
|
||||
if (!text.isEmpty()) text += ", ";
|
||||
text += QObject::tr(
|
||||
"%n image(s)",
|
||||
"part of a sentence listing the content of a diagram",
|
||||
images_count
|
||||
);
|
||||
// Qt's %n only selects a grammatical singular/plural form (the
|
||||
// "(s)" convention used by every other count here) -- it never
|
||||
// spells the number out as a word, so getting "une image"
|
||||
// instead of the literal "1 image" for the single-item case
|
||||
// means handling that count outside %n entirely, with its own
|
||||
// fixed string.
|
||||
text += images_count == 1
|
||||
? QObject::tr("une image", "part of a sentence listing the content of a diagram")
|
||||
: QObject::tr(
|
||||
"%n images",
|
||||
"part of a sentence listing the content of a diagram",
|
||||
images_count
|
||||
);
|
||||
}
|
||||
|
||||
if (shapes_count) {
|
||||
|
||||
@@ -267,7 +267,7 @@ QString QETApp::langFromSetting()
|
||||
{
|
||||
QSettings settings;
|
||||
system_language = settings.value("lang", "system").toString();
|
||||
if(system_language == "system") {
|
||||
if ((system_language == "system") || (system_language == QString())) {
|
||||
// Keep the full locale (e.g. "pt_BR"), not just the base language
|
||||
// ("pt"): QET ships regional translations (pt_BR, nl_BE, nl_NL) and
|
||||
// truncating here loaded the wrong one. setLanguage() falls back to
|
||||
@@ -1423,7 +1423,7 @@ QFont QETApp::diagramTextsItemFont(qreal size)
|
||||
@param size
|
||||
@return dynamic text font with PointSizeF(size)
|
||||
*/
|
||||
QFont QETApp::dynamicTextsItemFont(qreal size)
|
||||
QFont QETApp::dynamicTextsItemFont(qreal size)
|
||||
{
|
||||
QSettings settings;
|
||||
//Font to use
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "diagramevent/diagrameventaddpdf.h"
|
||||
#endif
|
||||
#include "diagramevent/diagrameventaddshape.h"
|
||||
#include "diagramevent/diagrameventaddpath.h"
|
||||
#include "diagramevent/diagrameventaddtext.h"
|
||||
#include "diagramview.h"
|
||||
#include "elementspanelwidget.h"
|
||||
@@ -60,6 +61,7 @@
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QTimer>
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
# include "ui/nokde/kautosavefile.h"
|
||||
#else
|
||||
@@ -718,6 +720,19 @@ void QETDiagramEditor::setUpActions()
|
||||
connect(&m_zoom_actions_group, &QActionGroup::triggered, this, &QETDiagramEditor::zoomGroupTriggered);
|
||||
|
||||
//Adding action (add text, image, shape...)
|
||||
// Exclusive (the default) prevents the active action from ever being
|
||||
// unchecked by clicking it again -- Qt only lets you switch to a
|
||||
// different one. That's exactly why clicking an already-active
|
||||
// tool's own icon never actually deactivated it: confirmed by
|
||||
// logging every addItemGroupTriggered() call and finding
|
||||
// isChecked()==true on *every* click, including the one meant to
|
||||
// turn the tool off -- the click-handling code's own "was this an
|
||||
// uncheck?" check was correct, its precondition just could never
|
||||
// occur. ExclusiveOptional allows exactly one more transition:
|
||||
// clicking the currently-checked action unchecks it, leaving none
|
||||
// checked, which is required for "click the active tool to turn it
|
||||
// off" to mean anything at the QAction level at all.
|
||||
m_add_item_actions_group.setExclusionPolicy(QActionGroup::ExclusionPolicy::ExclusiveOptional);
|
||||
QAction *add_text = m_add_item_actions_group.addAction(QET::Icons::PartTextField, tr("Ajouter un champ de texte"));
|
||||
QAction *add_image = m_add_item_actions_group.addAction(QET::Icons::adding_image, tr("Ajouter une image"));
|
||||
#ifdef QET_HAS_QTPDF
|
||||
@@ -727,6 +742,7 @@ void QETDiagramEditor::setUpActions()
|
||||
QAction *add_rectangle = m_add_item_actions_group.addAction(QET::Icons::PartRectangle, tr("Ajouter un rectangle"));
|
||||
QAction *add_ellipse = m_add_item_actions_group.addAction(QET::Icons::PartEllipse, tr("Ajouter une ellipse"));
|
||||
QAction *add_polyline = m_add_item_actions_group.addAction(QET::Icons::PartPolygon, tr("Ajouter une polyligne"));
|
||||
QAction *add_path = m_add_item_actions_group.addAction(QET::Icons::PartBezier, tr("Ajouter une courbe"));
|
||||
QAction *add_terminal_strip = m_add_item_actions_group.addAction(QET::Icons::TerminalStrip, tr("Ajouter un plan de bornes"));
|
||||
|
||||
add_text ->setStatusTip(tr("Ajoute un champ de texte sur le folio actuel"));
|
||||
@@ -738,6 +754,7 @@ void QETDiagramEditor::setUpActions()
|
||||
add_rectangle->setStatusTip(tr("Ajoute un rectangle sur le folio actuel"));
|
||||
add_ellipse ->setStatusTip(tr("Ajoute une ellipse sur le folio actuel"));
|
||||
add_polyline ->setStatusTip(tr("Ajoute une polyligne sur le folio actuel"));
|
||||
add_path ->setStatusTip(tr("Ajoute une courbe de Bézier sur le folio actuel"));
|
||||
add_terminal_strip->setStatusTip(tr("Ajoute un plan de bornier sur le folio actuel"));
|
||||
|
||||
add_text ->setData(QStringLiteral("text"));
|
||||
@@ -749,6 +766,7 @@ void QETDiagramEditor::setUpActions()
|
||||
add_rectangle->setData(QStringLiteral("rectangle"));
|
||||
add_ellipse ->setData(QStringLiteral("ellipse"));
|
||||
add_polyline ->setData(QStringLiteral("polyline"));
|
||||
add_path ->setData(QStringLiteral("path"));
|
||||
add_terminal_strip->setData(QStringLiteral("terminal_strip"));
|
||||
|
||||
add_text->setCheckable(true);
|
||||
@@ -756,6 +774,7 @@ void QETDiagramEditor::setUpActions()
|
||||
add_rectangle->setCheckable(true);
|
||||
add_ellipse->setCheckable(true);
|
||||
add_polyline->setCheckable(true);
|
||||
add_path->setCheckable(true);
|
||||
|
||||
connect(&m_add_item_actions_group, &QActionGroup::triggered, this, &QETDiagramEditor::addItemGroupTriggered);
|
||||
|
||||
@@ -1555,6 +1574,27 @@ void QETDiagramEditor::addItemGroupTriggered(QAction *action)
|
||||
if (Q_UNLIKELY (!currentDiagramView() || !currentDiagramView()->diagram() || value.isEmpty())) return;
|
||||
|
||||
Diagram *d = currentDiagramView()->diagram();
|
||||
|
||||
// This action group allows deselecting the currently-active tool by
|
||||
// clicking its own icon again, not just switching between tools --
|
||||
// that click still fires this slot (QActionGroup::triggered fires on
|
||||
// every click in the group, checked-state-changing or not), so
|
||||
// without this check it would unconditionally construct *another*
|
||||
// instance of the same tool and reactivate it, leaving the tool
|
||||
// fully active in the canvas while its own toolbar button shows
|
||||
// unchecked -- exactly backwards from what the click was for.
|
||||
// Scoped to checkable actions specifically: image/pdf/terminal_strip
|
||||
// are one-shot actions (pick a file, open a dialog) with no
|
||||
// persistent "active tool" state at all, and aren't even checkable
|
||||
// -- isChecked() on them is always false, so without this guard
|
||||
// they'd hit the branch above on every single click and never
|
||||
// reach their own handling below.
|
||||
if (action->isCheckable() && !action->isChecked())
|
||||
{
|
||||
d->clearEventInterface();
|
||||
return;
|
||||
}
|
||||
|
||||
DiagramEventInterface *diagram_event = nullptr;
|
||||
|
||||
if (value == "line")
|
||||
@@ -1564,12 +1604,10 @@ void QETDiagramEditor::addItemGroupTriggered(QAction *action)
|
||||
else if (value == "ellipse")
|
||||
diagram_event = new DiagramEventAddShape (d, QetShapeItem::Ellipse);
|
||||
else if (value == "polyline")
|
||||
{
|
||||
diagram_event = new DiagramEventAddShape (d, QetShapeItem::Polygon);
|
||||
statusBar()-> showMessage(tr("Double-click pour terminer la forme, Click droit pour annuler le dernier point"));
|
||||
connect(diagram_event, &DiagramEventInterface::destroyed, [this]() {
|
||||
statusBar()->clearMessage();
|
||||
});
|
||||
else if (value == "path")
|
||||
{
|
||||
diagram_event = new DiagramEventAddPath (d);
|
||||
}
|
||||
else if (value == "image")
|
||||
{
|
||||
@@ -1612,6 +1650,22 @@ void QETDiagramEditor::addItemGroupTriggered(QAction *action)
|
||||
{
|
||||
d->setEventInterface(diagram_event);
|
||||
connect(diagram_event, &DiagramEventInterface::destroyed, [action]() {action->setChecked(false);});
|
||||
// Defensive: on this style/theme, the toolbar button bound to an
|
||||
// exclusive-group action doesn't reliably repaint its checked
|
||||
// appearance after the group briefly had *no* action checked at
|
||||
// all (the gap between unchecking one tool and checking one
|
||||
// again, even the same one) -- confirmed by tracing a real
|
||||
// recording frame by frame: the status bar correctly showed this
|
||||
// tool's "before first click" hint (which can only appear from
|
||||
// inside a freshly-constructed tool's own constructor, so the
|
||||
// action's checked state and the tool's activation were both
|
||||
// genuinely correct) while the button itself stayed visually
|
||||
// unchecked for a sustained period. Forcing an explicit repaint
|
||||
// here makes the button's appearance match its actual state
|
||||
// regardless of whether Qt's own change notification fired
|
||||
// correctly.
|
||||
if (QWidget *button = m_add_item_tool_bar->widgetForAction(action))
|
||||
button->update();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1011,10 +1011,14 @@ bool Conductor::fromXml(QDomElement &dom_element)
|
||||
//that field was introduced (see terminal1/terminal2 handling in
|
||||
//toXml() below).
|
||||
m_uuid = QUuid(dom_element.attribute(QStringLiteral("uuid")));
|
||||
m_persist_uuid = !m_uuid.isNull();
|
||||
if (m_uuid.isNull()) {
|
||||
//Absent, empty or malformed: mint one. A null uuid is not a usable
|
||||
//identity -- every conductor carrying one would collide with every
|
||||
//other on the conductor table's primary key.
|
||||
//other on the conductor table's primary key. It's runtime-only,
|
||||
//though: toXml() must not write it back out, or a legacy file
|
||||
//with no conductor uuids gets a freshly different one on every
|
||||
//single load-and-resave (see #754).
|
||||
m_uuid = QUuid::createUuid();
|
||||
}
|
||||
|
||||
@@ -1056,7 +1060,8 @@ QDomElement Conductor::toXml(QDomDocument &dom_document,
|
||||
{
|
||||
QDomElement dom_element = dom_document.createElement("conductor");
|
||||
|
||||
dom_element.setAttribute("uuid", m_uuid.toString());
|
||||
if (m_persist_uuid)
|
||||
dom_element.setAttribute("uuid", m_uuid.toString());
|
||||
dom_element.setAttribute("x", QString::number(pos().x()));
|
||||
dom_element.setAttribute("y", QString::number(pos().y()));
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ class Conductor : public QGraphicsObject
|
||||
Diagram *diagram() const;
|
||||
ConductorTextItem *textItem() const;
|
||||
QUuid uuid() const {return m_uuid;}
|
||||
void newUuid() {m_uuid = QUuid::createUuid();} //create new uuid for this conductor
|
||||
void newUuid() {m_uuid = QUuid::createUuid(); m_persist_uuid = true;} //create new uuid for this conductor
|
||||
void updatePath(const QRectF & = QRectF());
|
||||
|
||||
//This method do nothing, it's only made to be used with Q_PROPERTY
|
||||
@@ -209,6 +209,10 @@ class Conductor : public QGraphicsObject
|
||||
bool m_valid;
|
||||
bool m_freeze_label = false;
|
||||
QUuid m_uuid;
|
||||
/// false when m_uuid was synthesized by fromXml() because the
|
||||
/// file had none -- toXml() must not persist that value, or
|
||||
/// every reload mints and saves a new random one (see #754).
|
||||
bool m_persist_uuid = true;
|
||||
|
||||
/// QPen et QBrush objects used to draw conductors
|
||||
static QPen conductor_pen;
|
||||
|
||||
@@ -19,9 +19,18 @@
|
||||
#define DIAGRAM_IMAGE_ITEM_H
|
||||
|
||||
#include "qetgraphicsitem.h"
|
||||
#include "shapetransform.h"
|
||||
#include "../ui/imagetransparentcolordialog.h"
|
||||
|
||||
#include <QColor>
|
||||
#include <QList>
|
||||
#include <QStyleOptionGraphicsItem>
|
||||
#include <QVector>
|
||||
|
||||
class QDomElement;
|
||||
class QDomDocument;
|
||||
class QGraphicsSceneContextMenuEvent;
|
||||
class QetGraphicsHandlerItem;
|
||||
|
||||
/**
|
||||
This class represents a selectable, movable and editable image on a
|
||||
@@ -30,6 +39,23 @@ class QDomDocument;
|
||||
*/
|
||||
class DiagramImageItem : public QetGraphicsItem {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QPixmap pixmap READ pixmap WRITE setPixmap NOTIFY pixmapChanged)
|
||||
Q_PROPERTY(qreal scaleFactorX READ scaleFactorX WRITE setScaleFactorX NOTIFY transformChanged)
|
||||
Q_PROPERTY(qreal scaleFactorY READ scaleFactorY WRITE setScaleFactorY NOTIFY transformChanged)
|
||||
Q_PROPERTY(qreal rotationAngle READ rotationAngle WRITE setRotationAngle NOTIFY transformChanged)
|
||||
Q_PROPERTY(qreal skewX READ skewX WRITE setSkewX NOTIFY transformChanged)
|
||||
Q_PROPERTY(qreal skewY READ skewY WRITE setSkewY NOTIFY transformChanged)
|
||||
Q_PROPERTY(QPointF pivot READ pivot WRITE setPivot NOTIFY transformChanged)
|
||||
// A second, deliberately non-compensating property on the SAME
|
||||
// underlying value -- setPivot() (above) intentionally adjusts
|
||||
// pos() to keep the image visually in place, which is exactly
|
||||
// wrong for crop()'s own undo chain: crop() already computes the
|
||||
// correct final pos() itself (accounting for the crop, not just
|
||||
// the pivot move) and pushes it as its own separate "pos" command,
|
||||
// so a chained "pivot" step going through the compensating setter
|
||||
// would silently overwrite that already-correct pos() a second
|
||||
// time. rawPivot exists solely for that one caller.
|
||||
Q_PROPERTY(QPointF rawPivot READ pivot WRITE setPivotRaw NOTIFY transformChanged)
|
||||
|
||||
// constructors, destructor
|
||||
public:
|
||||
@@ -40,6 +66,19 @@ class DiagramImageItem : public QetGraphicsItem {
|
||||
// attributes
|
||||
public:
|
||||
enum { Type = UserType + 1007 };
|
||||
|
||||
// A deliberately smaller, image-specific set than QetShapeItem's own
|
||||
// HandleMode/HandleRole: images have no path/polygon/arc concepts at
|
||||
// all, so reusing those enums directly would pull in a great deal of
|
||||
// irrelevant baggage for no benefit. RotateSkew's math (rotate,
|
||||
// skew, and their shared scaleAndShearOffset()/scaleOnlyOffset()
|
||||
// helpers) mirrors QetShapeItem's own identical formulas -- adapted
|
||||
// to this class's own, simpler 4-corner/4-edge/1-pivot handle set,
|
||||
// not shared code, since the two classes' broader handle roles
|
||||
// differ too much (no path/polygon/arc concepts here) for sharing
|
||||
// the dispatcher itself to be worth an indirect abstraction.
|
||||
enum class HandleMode { Size, RotateSkew };
|
||||
enum class HandleRole { Resize, Rotate, SkewEdge, Pivot };
|
||||
|
||||
// methods
|
||||
public:
|
||||
@@ -54,13 +93,125 @@ class DiagramImageItem : public QetGraphicsItem {
|
||||
virtual QDomElement toXml(QDomDocument &) const;
|
||||
void editProperty() override;
|
||||
void setPixmap(const QPixmap &pixmap);
|
||||
QPixmap pixmap() const { return pixmap_; }
|
||||
QRectF boundingRect() const override;
|
||||
QString name() const override;
|
||||
|
||||
|
||||
/// DXF export: replay this item's paint() on an arbitrary QPainter
|
||||
/// (e.g. one targeting DxfPaintDevice). paint() itself stays
|
||||
/// protected, as it should for the normal
|
||||
/// QGraphicsScene/QGraphicsView paint contract - this is a
|
||||
/// deliberate, narrow escape hatch for exporters, not a general
|
||||
/// relaxation of that contract. Matches CrossRefItem::paintForExport()
|
||||
/// exactly, for the identical reason.
|
||||
void paintForExport(QPainter *painter)
|
||||
{
|
||||
QStyleOptionGraphicsItem option;
|
||||
paint(painter, &option, nullptr);
|
||||
}
|
||||
|
||||
qreal scaleFactorX() const { return m_transform.scaleX; }
|
||||
qreal scaleFactorY() const { return m_transform.scaleY; }
|
||||
void setScaleFactorX(qreal factor);
|
||||
void setScaleFactorY(qreal factor);
|
||||
qreal rotationAngle() const { return m_transform.rotation; }
|
||||
void setRotationAngle(qreal angle);
|
||||
qreal skewX() const { return m_transform.skewX; }
|
||||
void setSkewX(qreal skew);
|
||||
qreal skewY() const { return m_transform.skewY; }
|
||||
void setSkewY(qreal skew);
|
||||
QPointF pivot() const { return m_transform.pivot; }
|
||||
void setPivot(const QPointF &pivot);
|
||||
void setPivotRaw(const QPointF &pivot);
|
||||
|
||||
signals:
|
||||
void pixmapChanged();
|
||||
void transformChanged();
|
||||
|
||||
protected:
|
||||
void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override;
|
||||
|
||||
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
|
||||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
|
||||
void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;
|
||||
bool sceneEventFilter(QGraphicsItem *watched, QEvent *event) override;
|
||||
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
|
||||
|
||||
private:
|
||||
void replaceImage();
|
||||
void mirror(bool horizontal);
|
||||
void setTransparentColor();
|
||||
void crop();
|
||||
void restoreAspectRatio();
|
||||
void saveImageAs();
|
||||
void saveOriginalImageAs();
|
||||
void saveImagePixmapAs(const QPixmap &pixmap, const QString &dialogTitle, bool hasTransparency);
|
||||
static bool writeRasterAsSvg(const QPixmap &pixmap, const QString &path);
|
||||
static QPixmap computeDisplayPixmap(const QPixmap &base, const QRect &cropRect, const QList<ImageTransparentColorDialog::PickedColor> &colors);
|
||||
|
||||
void toggleHandleMode();
|
||||
HandleMode nextHandleMode() const;
|
||||
static QString handleModeLabel(HandleMode mode);
|
||||
void updateModeHint();
|
||||
void refreshInteractionHints();
|
||||
QString currentModeStatusHint() const;
|
||||
void rebuildHandles();
|
||||
void repositionHandles();
|
||||
void clearHandles();
|
||||
void resetPivotToBoundingRectCenter();
|
||||
void handlerMousePressEvent(int index, Qt::KeyboardModifiers mods);
|
||||
void handlerMouseMoveEvent(int index, QGraphicsSceneMouseEvent *event);
|
||||
void handlerMouseReleaseEvent(int index);
|
||||
void dragResize(int index, const QPointF &localPos, Qt::KeyboardModifiers mods);
|
||||
void dragRotateHandle(int cornerIndex, const QPointF &scenePos, Qt::KeyboardModifiers mods);
|
||||
void dragSkewHandle(int edgeIndex, const QPointF &scenePos, Qt::KeyboardModifiers mods);
|
||||
void dragPivot(const QPointF &localPos);
|
||||
QPointF scaleOnlyOffset(const QPointF &localPoint) const;
|
||||
QPointF scaleAndShearOffset(const QPointF &localPoint) const;
|
||||
QPointF handlePosition(int index) const;
|
||||
static QPointF cornerPosition(int cornerIndex, qreal w, qreal h);
|
||||
static QPointF edgeMidpointPosition(int edgeIndex, qreal w, qreal h);
|
||||
static QColor colorForHandleRole(HandleRole role);
|
||||
static QString hintForHandleRole(HandleRole role);
|
||||
void showStatusHint(const QString &text) const;
|
||||
void clearStatusHint() const;
|
||||
|
||||
protected:
|
||||
QPixmap pixmap_;
|
||||
// The true, pristine original -- never itself cropped or colour-
|
||||
// keyed. pixmap_ (the displayed result) is always re-derived from
|
||||
// this plus m_crop_rect and m_transparent_colors (each colour
|
||||
// carrying its own tolerance), via computeDisplayPixmap(). Without
|
||||
// keeping this separate, re-opening either the crop or
|
||||
// transparency dialog after using the other would show an
|
||||
// already-modified image as if it were the source -- areas already
|
||||
// cropped away or coloured out would be gone for good, with no way
|
||||
// to recover or adjust them, only start over. Updated by whatever
|
||||
// genuinely replaces or reorients the image's actual content
|
||||
// (construction, replaceImage(), and mirror(), which also mirrors
|
||||
// m_crop_rect to keep referring to the same region of the
|
||||
// now-flipped base) -- never by crop() or setTransparentColor()
|
||||
// themselves, which only ever change which subset of this base is
|
||||
// shown.
|
||||
QPixmap m_base_pixmap;
|
||||
QRect m_crop_rect; // relative to m_base_pixmap; equals m_base_pixmap.rect() when nothing has been cropped
|
||||
QList<ImageTransparentColorDialog::PickedColor> m_transparent_colors;
|
||||
|
||||
// Independent scaleX/scaleY here is the actual point of this whole
|
||||
// member: QGraphicsItem::scale() is a single, uniform float, which
|
||||
// is exactly why an image could never break its own aspect ratio
|
||||
// before this. skewX/skewY exist in the struct but are never
|
||||
// written by anything below -- deliberately deferred, not an
|
||||
// oversight (see the .cpp for why).
|
||||
ShapeTransform m_transform;
|
||||
bool m_pivotIsCustom = false;
|
||||
HandleMode m_handleMode = HandleMode::Size;
|
||||
QVector<QetGraphicsHandlerItem *> m_handler_vector;
|
||||
QVector<HandleRole> m_handleRoles;
|
||||
int m_vector_index = -1;
|
||||
QPointF m_original_pos; // scene position at the start of a resize/rotate/pivot drag, for Escape-to-cancel
|
||||
ShapeTransform m_original_transform;
|
||||
bool m_deferHandleReposition = false; // see setPivot()'s comment
|
||||
bool m_resizeCenterAnchored = false; // decided once, at press time -- see handlerMousePressEvent()'s comment for why, mirroring the identical fix already made for shape creation
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -806,6 +806,7 @@ bool Element::fromXml(QDomElement &e,
|
||||
setZValue(e.attribute(QStringLiteral("z"), QString::number(this->zValue())).toDouble());
|
||||
setFlags(QGraphicsItem::ItemIsMovable
|
||||
| QGraphicsItem::ItemIsSelectable);
|
||||
is_movable_ = e.attribute(QStringLiteral("is_movable"), QStringLiteral("1")).toInt();
|
||||
|
||||
// orientation
|
||||
bool conv_ok;
|
||||
@@ -937,6 +938,7 @@ QDomElement Element::toXml(
|
||||
element.setAttribute(QStringLiteral("y"), QString::number(pos().y()));
|
||||
element.setAttribute(QStringLiteral("z"), QString::number(this->zValue()));
|
||||
element.setAttribute(QStringLiteral("orientation"), QString::number(orientation()));
|
||||
element.setAttribute(QStringLiteral("is_movable"), bool(is_movable_));
|
||||
|
||||
/* get the first id to use for the bounds of this element
|
||||
* recupere le premier id a utiliser pour les bornes de cet element */
|
||||
|
||||