mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-23 18:14:13 +02:00
Compare commits
141 Commits
ef1fbd79b4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d4c0326d4 | |||
| d889a384aa | |||
| ed0f800ab5 | |||
| 6d8459d647 | |||
| 28acbadaf0 | |||
| 4456d08b91 | |||
| aae7a8bde3 | |||
| 4bcf5834e4 | |||
| f6889af99c | |||
| 9e5af6440b | |||
| b97bccecd3 | |||
| 14cf3e403f | |||
| 1f0ed43006 | |||
| fc2a200ca2 | |||
| ccf96e0537 | |||
| fa213d90d9 | |||
| 84add7b3ef | |||
| 36fb048a80 | |||
| a7ae17a1b4 | |||
| a1675d7dea | |||
| 13a37e1f59 | |||
| 6fc7e4a090 | |||
| 08b83f23e3 | |||
| 3fa5e0a475 | |||
| 8334a9a27f | |||
| 4a636b14e3 | |||
| f777be05b4 | |||
| 8ee3f90047 | |||
| 41e1e8172c | |||
| 1212f48c6d | |||
| e01f46c5b0 | |||
| 3cec02b3f3 | |||
| a2e384761c | |||
| 4283ddad10 | |||
| 1046b97080 | |||
| a766f9a975 | |||
| 68503e2a7d | |||
| 3b05f5cf13 | |||
| 2906a48aab | |||
| a542b911e9 | |||
| 9cee71bde2 | |||
| 687837d7a3 | |||
| ea1f65107e | |||
| 6380e2a46b | |||
| 219273a8e5 | |||
| 8a2d31d835 | |||
| a8f883601d | |||
| cd27912605 | |||
| f087820757 | |||
| 2772b690e8 | |||
| 279b001a16 | |||
| 59cc758b7f | |||
| 0c958d3b66 | |||
| 528d33792d | |||
| fc41cf9f5d | |||
| 7f04bc1572 | |||
| 27bbf51024 | |||
| 65299e1178 | |||
| 8e2a29deaf | |||
| 5b739c3338 | |||
| a1e4e153ae | |||
| 56f60be60a | |||
| 852f581206 | |||
| 3b626a7d1a | |||
| 069a75d44b | |||
| e1f887a036 | |||
| c5edd0a54b | |||
| 3a13287ba9 | |||
| 24776bfcf6 | |||
| 3202c145e8 | |||
| 4888ae87f3 | |||
| 59344eb565 | |||
| 5d033bf1b2 | |||
| 8985babfe7 | |||
| 29d16c3337 | |||
| 61f5e5e502 | |||
| 77bc9ed8e4 | |||
| 9b26d5dc6a | |||
| b76d8ce8a1 | |||
| d7052e396b | |||
| e123c55754 | |||
| 268aba60eb | |||
| 313f9533a8 | |||
| 3d1cb671c1 | |||
| c0fc093b4e | |||
| 46c35d2297 | |||
| 2adc58c1c0 | |||
| 82262c5980 | |||
| 6dcb6a2a8f | |||
| c740cdf1ac | |||
| 78caeaec48 | |||
| aab0a18506 | |||
| ebab58e4b3 | |||
| 45ca53c7a8 | |||
| 2efce1752d | |||
| 27dea3ffab | |||
| c256e2dd1a | |||
| 8e3a7a6ab6 | |||
| 740271c993 | |||
| 1bec2700bd | |||
| 268e9c7bd9 | |||
| 264a0f677f | |||
| 9ccfb5e116 | |||
| 04463f0954 | |||
| 3245919c19 | |||
| 55ad2eccbf | |||
| 8b2548d601 | |||
| c3aeb0bf83 | |||
| 16d89f64fd | |||
| ac78c51b70 | |||
| 2e27f77b28 | |||
| 83fb525e27 | |||
| d1808c7a89 | |||
| 4e4f029d2d | |||
| 242f134e0f | |||
| 2dc88df29c | |||
| fd8135264f | |||
| 827cd2a91e | |||
| cdd25189b5 | |||
| f1313f5895 | |||
| 75fafd5acc | |||
| 3394c1246c | |||
| 3e34a6d55f | |||
| 50792ba1ad | |||
| 6404612014 | |||
| eaf15faaa3 | |||
| b8c9e670c4 | |||
| 85dc638a42 | |||
| dfe56cddbd | |||
| 3f397f5f78 | |||
| 7ec13cbc1a | |||
| 7fa1ff35b8 | |||
| 7e3420b1a5 | |||
| 183035d5ac | |||
| eb4109bb11 | |||
| 717bf57677 | |||
| 861e5de25e | |||
| bf7bff595e | |||
| 3c1fb2fe72 | |||
| 95d0e523fe | |||
| 81449faffd |
@@ -1 +0,0 @@
|
||||
*.qch filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
name: Auto-build doxygen docs
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '**'
|
||||
|
||||
|
||||
|
||||
jobs:
|
||||
doxygen:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
show-progress: ''
|
||||
- name: Setup and run doxygen
|
||||
run: sudo apt update && sudo apt install doxygen graphviz qhelpgenerator-qt5 -y
|
||||
- name: Set up Git LFS
|
||||
run: |
|
||||
git lfs install
|
||||
git lfs track "*.qch"
|
||||
- name: Run doxygen
|
||||
run: doxygen Doxyfile
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
token: ${{ secrets.MR_TOKEN }}
|
||||
commit-message: update QCH file
|
||||
committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
title: Update QCH Help file
|
||||
body: |
|
||||
- Updating QT Help file following commit ${{ github.sha }}.
|
||||
- Auto-generated by [create-pull-request][1]
|
||||
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
branch: update-qch
|
||||
labels: |
|
||||
qch
|
||||
cicd
|
||||
delete-branch: true
|
||||
add-paths: doc/*.qch
|
||||
- uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ${{ github.workspace }}/doc/html/
|
||||
deploy:
|
||||
# Add a dependency to the build job
|
||||
needs: doxygen
|
||||
|
||||
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
|
||||
permissions:
|
||||
pages: write # to deploy to Pages
|
||||
id-token: write # to verify the deployment originates from an appropriate source
|
||||
|
||||
# Deploy to the github-pages environment
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
# Specify runner + deployment step
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4 # or specific "vX.X.X" version tag for this action
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
ca-certificates build-essential cmake ninja-build git pkg-config \
|
||||
qt6-base-dev qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \
|
||||
libqt6svg6-dev libqt6sql6-sqlite libsqlite3-dev libcups2-dev \
|
||||
libqt6svg6-dev libqt6sql6-sqlite libcups2-dev \
|
||||
libxkbcommon-x11-0 \
|
||||
xvfb openbox xdotool x11-utils
|
||||
# extra-cmake-modules and the KF6 libraries are installed rather than
|
||||
@@ -53,7 +53,9 @@ jobs:
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
cmake -B build -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DQT_VERSION_MAJOR=6
|
||||
-DQT_VERSION_MAJOR=6 \
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
|
||||
-DQET_EXPORT_PROJECT_DB=ON
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build --parallel $(nproc)
|
||||
|
||||
@@ -49,7 +49,6 @@ jobs:
|
||||
mingw-w64-ucrt-x86_64-qt6-tools
|
||||
mingw-w64-ucrt-x86_64-qt6-translations
|
||||
mingw-w64-ucrt-x86_64-qt6-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
|
||||
@@ -122,8 +121,7 @@ jobs:
|
||||
-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"
|
||||
|
||||
@@ -196,13 +194,6 @@ jobs:
|
||||
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/"
|
||||
|
||||
@@ -59,17 +59,41 @@ jobs:
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# 2. Download the portable artifact for this flavor
|
||||
#
|
||||
# Wrapped in nick-fields/retry: actions/download-artifact@v8 has
|
||||
# shown repeated "Artifact download failed after 5 retries"
|
||||
# failures on this cross-workflow download (via run-id) — not a
|
||||
# real content/digest problem, just flaky Azure blob delivery.
|
||||
# Two separate occurrences observed within days of each other
|
||||
# (different artifact IDs/digests, same failure signature), each
|
||||
# one enough to fail build-msi outright and block the rest of the
|
||||
# pipeline. Retrying the whole download 3x is cheap insurance.
|
||||
# Switched to `gh run download` here because nick-fields/retry
|
||||
# can only retry a shell command, not re-invoke a `uses:` step.
|
||||
# ----------------------------------------------------------------
|
||||
- name: Download portable artifact
|
||||
uses: actions/download-artifact@v8
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
name: ${{ matrix.portable_artifact }}
|
||||
path: artifact\files
|
||||
# workflow_run => use the triggering run's ID
|
||||
# workflow_dispatch => use input run_id if provided, otherwise current run
|
||||
run-id: ${{ github.event.workflow_run.id || github.event.inputs.run_id || github.run_id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: ${{ github.repository }}
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
retry_wait_seconds: 30
|
||||
shell: pwsh
|
||||
command: |
|
||||
if (Test-Path "artifact\files") { Remove-Item -Recurse -Force "artifact\files" }
|
||||
New-Item -ItemType Directory -Force -Path "artifact\files" | Out-Null
|
||||
|
||||
$runId = "${{ github.event.workflow_run.id || github.event.inputs.run_id || github.run_id }}"
|
||||
gh run download $runId `
|
||||
--repo "${{ github.repository }}" `
|
||||
--name "${{ matrix.portable_artifact }}" `
|
||||
--dir "artifact\files"
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "gh run download failed (exit $LASTEXITCODE)"
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# 3. Extract version
|
||||
@@ -336,7 +360,13 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Delete old nightly .msi asset
|
||||
if: always()
|
||||
# Only run if a new MSI actually exists in dist/ — otherwise the old
|
||||
# (still working) nightly .msi would be deleted without anything to
|
||||
# replace it, leaving the nightly release with no MSI at all until
|
||||
# the next successful build (see windows-msi-pipeline notes,
|
||||
# run 35694391567: an upstream artifact-download failure meant
|
||||
# dist\*.msi never existed for that run).
|
||||
if: always() && hashFiles('dist/*.msi') != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
@@ -381,7 +411,20 @@ jobs:
|
||||
deploy-pages:
|
||||
needs: build-msi
|
||||
runs-on: ubuntu-latest
|
||||
if: always() && needs.build-msi.result == 'success'
|
||||
# Regenerate the page whenever the MSI job actually ran (success OR
|
||||
# packaging/signing failure) — not only on full success.
|
||||
# generate-page.py queries the published assets on the "nightly"
|
||||
# release itself (line 407: `gh release view nightly --json assets`),
|
||||
# so it already handles a missing MSI natively (empty MSI_NAME -> no
|
||||
# MSI button on the page). The page only needs the exe/zip already
|
||||
# published by windows-build.yml, independent of the MSI's fate.
|
||||
# Only "skipped"/"cancelled" are excluded: if build-msi never ran at
|
||||
# all (e.g. Windows Build itself failed), there is nothing new to
|
||||
# publish and regenerating the page would be pointless.
|
||||
if: >
|
||||
always() &&
|
||||
needs.build-msi.result != 'skipped' &&
|
||||
needs.build-msi.result != 'cancelled'
|
||||
permissions:
|
||||
contents: write
|
||||
pages: write
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
*.snap
|
||||
.flatpak-builder
|
||||
__pycache__/
|
||||
*.pyc
|
||||
# Qt build output
|
||||
*.user
|
||||
# doxygen Doxyfile output
|
||||
|
||||
+3
-2
@@ -113,7 +113,9 @@ if(Backtrace_FOUND)
|
||||
include_directories(${Backtrace_INCLUDE_DIRS})
|
||||
add_compile_definitions(QET_CRASH_BACKTRACE)
|
||||
else()
|
||||
message(STATUS "backtrace() not available: crash dumps will carry the log ring without a backtrace")
|
||||
message(STATUS
|
||||
"backtrace() not available: crash dumps will carry the log ring "
|
||||
"without a backtrace")
|
||||
endif()
|
||||
|
||||
find_package(SQLite3 REQUIRED)
|
||||
@@ -316,7 +318,6 @@ target_include_directories(
|
||||
${QET_DIR}/sources/NameList
|
||||
${QET_DIR}/sources/NameList/ui
|
||||
${QET_DIR}/sources/utils
|
||||
${QET_DIR}/pugixml/src
|
||||
${QET_DIR}/sources/dataBase
|
||||
${QET_DIR}/sources/dataBase/ui
|
||||
${QET_DIR}/sources/factory/ui
|
||||
|
||||
+23
-21
@@ -27,18 +27,12 @@ git submodule update --init --recursive
|
||||
| C++17 compiler | required | GCC or Clang on Unix-like platforms; MSVC or MinGW-w64 g++ on Windows — see [Choosing a compiler](#3-choosing-a-compiler-unix) / [Building on Windows](#6-building-on-windows-msvc--mingw) |
|
||||
| Qt6 base + widgets | required | |
|
||||
| Qt6 **GuiPrivate** headers | required | needed for clickable PDF hyperlinks; **hard build failure** at CMake generate time if missing, see below |
|
||||
| SQLite3 | required | used by the nomenclature/summary database |
|
||||
| Qt Linguist tools (`lrelease`) | required | compiles the tracked `.ts` files into `.qm` as part of every normal build |
|
||||
| pugixml | handled automatically | fetched and built via CMake FetchContent if not already present on the system — see [pugixml](#8-pugixml) below |
|
||||
| Qt Test module | required if building tests | `PACKAGE_TESTS` is `ON` by default; QtTest ships as part of the base Qt6 dev packages listed below on every platform, no extra package needed |
|
||||
| KDE Frameworks (KF6) | optional | see [Building without KDE Frameworks](#9-building-without-kde-frameworks) |
|
||||
| QtPdf module | optional | see [PDF page import](#7-pdf-page-import-qtpdf) |
|
||||
|
||||
A note on CMake versions: the project declares a minimum of 3.5 but is
|
||||
routinely built with much newer releases; if your CMake is older than 4.3 it
|
||||
simply won't have the newer `SQLite3::SQLite3` target name, which the build
|
||||
script compensates for automatically. There is nothing you need to do either
|
||||
way.
|
||||
|
||||
## 3. Building (out-of-source build)
|
||||
|
||||
@@ -106,7 +100,7 @@ the closest match.
|
||||
sudo apt install \
|
||||
build-essential cmake ninja-build git \
|
||||
qt6-base-dev qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \
|
||||
libsqlite3-dev \
|
||||
|
||||
libkf6coreaddons-dev libkf6widgetsaddons-dev
|
||||
```
|
||||
|
||||
@@ -138,7 +132,6 @@ sudo apt install libpugixml-dev
|
||||
sudo dnf install \
|
||||
cmake gcc-c++ git \
|
||||
qt6-qtbase-devel qt6-qtbase-private-devel qt6-qttools-devel \
|
||||
sqlite-devel \
|
||||
kf6-kcoreaddons-devel kf6-kwidgetsaddons-devel
|
||||
```
|
||||
|
||||
@@ -155,11 +148,10 @@ Optional, for a system pugixml: `sudo dnf install pugixml-devel`.
|
||||
pkg install \
|
||||
cmake git \
|
||||
qt6-base qt6-tools \
|
||||
sqlite3 \
|
||||
kf6-kcoreaddons kf6-kwidgetsaddons
|
||||
```
|
||||
|
||||
(from ports: `devel/qt6-base`, `devel/qt6-tools`, `databases/sqlite3`,
|
||||
(from ports: `devel/qt6-base`, `devel/qt6-tools`,
|
||||
`devel/kf6-kcoreaddons`, `x11-toolkits/kf6-kwidgetsaddons`.) Qt6's
|
||||
`GuiPrivate` headers ship as part of `qt6-base` on FreeBSD, no separate
|
||||
package is needed. `QtPdf` is not packaged on FreeBSD at the time of writing
|
||||
@@ -175,7 +167,7 @@ Optional, for a system pugixml: `pkg install pugixml` (`devel/pugixml`).
|
||||
Using [Homebrew](https://brew.sh):
|
||||
|
||||
```sh
|
||||
brew install cmake qt sqlite ninja
|
||||
brew install cmake qt ninja
|
||||
```
|
||||
|
||||
Homebrew's `qt` formula is Qt6 and includes the private headers, so no
|
||||
@@ -193,7 +185,7 @@ Optional, for a system pugixml: `brew install pugixml`.
|
||||
|
||||
See [Building on Windows](#6-building-on-windows-msvc--mingw) below — the
|
||||
package sources differ enough from the Unix-like platforms above (no system
|
||||
package manager, SQLite3 and Qt aren't provided the same way) that it gets
|
||||
package manager and Qt aren't provided the same way) that it gets
|
||||
its own section.
|
||||
|
||||
## 5. pugixml
|
||||
@@ -215,7 +207,7 @@ on Debian/Ubuntu, `pugixml-devel` on Fedora).
|
||||
Both toolchains QET's CMake build targets on Windows are covered here:
|
||||
**MSVC** (Visual Studio 2019/2022) and **MinGW-w64** (gcc). Unlike the
|
||||
Unix-like platforms above, there's no single system package manager, so
|
||||
Qt, SQLite3 and (optionally) KDE Frameworks each need to be sourced
|
||||
Qt and (optionally) KDE Frameworks each need to be sourced
|
||||
separately per toolchain.
|
||||
|
||||
One piece of good news either way: unlike Debian/Fedora, the official Qt
|
||||
@@ -236,11 +228,7 @@ normally use `-DBUILD_WITH_KF=OFF` (see the
|
||||
1. Install Visual Studio with the "Desktop development with C++" workload,
|
||||
and install Qt6 for MSVC (e.g. the `msvc2019_64` or `msvc2022_64` kit)
|
||||
via the [Qt Online Installer](https://www.qt.io/download-qt-installer).
|
||||
2. Get SQLite3 — the simplest route is [vcpkg](https://vcpkg.io):
|
||||
```bat
|
||||
vcpkg install sqlite3:x64-windows
|
||||
```
|
||||
3. Configure and build from an "x64 Native Tools Command Prompt for VS":
|
||||
2. Configure and build from an "x64 Native Tools Command Prompt for VS":
|
||||
```bat
|
||||
mkdir build && cd build
|
||||
cmake .. -G "Visual Studio 17 2022" -A x64 ^
|
||||
@@ -271,15 +259,29 @@ mkdir build && cd build
|
||||
cmake .. -G Ninja -DBUILD_WITH_KF=OFF -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build .
|
||||
```
|
||||
|
||||
If `libbacktrace` is installed in your MSYS2 environment, pass the following
|
||||
cached CMake variables when configuring. They are only necessary in that
|
||||
case, because `FindBacktrace` needs them to locate the library:
|
||||
|
||||
```sh
|
||||
cmake .. -G Ninja -DBUILD_WITH_KF=OFF -DCMAKE_BUILD_TYPE=Release \
|
||||
-DBacktrace_INCLUDE_DIR=/c/msys64/clang64/include \
|
||||
-DBacktrace_LIBRARY=/c/msys64/clang64/lib/libbacktrace.a
|
||||
```
|
||||
|
||||
`Backtrace_INCLUDE_DIR` must point to the directory containing `backtrace.h`,
|
||||
and `Backtrace_LIBRARY` to the `libbacktrace.a` file. The paths above are the
|
||||
usual locations for the MSYS2 `clang64` environment; adjust them if your
|
||||
installation uses a different prefix.
|
||||
|
||||
(KF6 isn't packaged in MSYS2 either, hence `-DBUILD_WITH_KF=OFF` again.)
|
||||
|
||||
Using the Qt Online Installer's bundled MinGW kit instead: point
|
||||
`CMAKE_PREFIX_PATH` at that kit (e.g. `C:\Qt\6.x.x\mingw_64`) and make sure
|
||||
its bundled `g++.exe` comes first on `PATH`, or pass
|
||||
`-DCMAKE_C_COMPILER`/`-DCMAKE_CXX_COMPILER` explicitly so CMake doesn't pick
|
||||
up a different MinGW installation. SQLite3 still has to come from elsewhere
|
||||
in this path — vcpkg with a `mingw`-flavoured triplet, or MSYS2's package as
|
||||
above.
|
||||
up a different MinGW installation.
|
||||
|
||||
## 7. Qt6 private headers (mandatory)
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ parts:
|
||||
source: .
|
||||
stage-packages:
|
||||
- git
|
||||
- sqlite3
|
||||
- xdg-user-dirs
|
||||
- libqt6qml6
|
||||
# libxcb-cursor0 workaround was needed against the Qt5/KF5 core22 content
|
||||
@@ -82,7 +81,7 @@ parts:
|
||||
- git
|
||||
- cmake
|
||||
- ninja-build
|
||||
- libsqlite3-dev
|
||||
|
||||
- qt6-tools-dev
|
||||
- qt6-base-private-dev
|
||||
- qt6-declarative-dev
|
||||
|
||||
@@ -23,8 +23,31 @@ if(BUILD_WITH_KF)
|
||||
|
||||
if(BUILD_KF)
|
||||
|
||||
if(NOT DEFINED KF_GIT_TAG)
|
||||
# this is a more or less random version, taken as an conservative approach
|
||||
# v6.10.0 is a more or less random version, taken as an conservative
|
||||
# approach. Pinned to the commits v6.10.0 points at, not to the tags
|
||||
# themselves; see the note in fetch_pugixml.cmake. Each module lives in its
|
||||
# own repository, so the same v6.10.0 release is a different commit in
|
||||
# each.
|
||||
#
|
||||
# KDE uses annotated tags, so "git ls-remote <repo> 'refs/tags/v6.10.0*'"
|
||||
# prints two hashes per module: refs/tags/v6.10.0 is the tag object (the
|
||||
# tagger, the date and the tag message) and refs/tags/v6.10.0^{} is the
|
||||
# commit that object points at. The hashes below are the "^{}" ones, i.e.
|
||||
# the commits. Lightweight tags, such as pugixml's v1.15 and
|
||||
# SingleApplication's v3.2.0, have no tag object and print only the
|
||||
# commit line.
|
||||
set(KF_ECM_GIT_COMMIT 7dd28cc56c339c3f8fb356f7c53c0e8f61433d81) # v6.10.0
|
||||
set(KF_KCOREADDONS_GIT_COMMIT c569f974dab24b4784ad186a3db4b76b2fa36612) # v6.10.0
|
||||
set(KF_KWIDGETSADDONS_GIT_COMMIT 1abbed8a280d6626c59fb197f2c4667d2b1e7445) # v6.10.0
|
||||
|
||||
if(DEFINED KF_GIT_TAG)
|
||||
# Explicit override: -DKF_GIT_TAG=<ref> selects one ref for all three
|
||||
# modules, unpinned, exactly as it did before.
|
||||
set(KF_ECM_GIT_COMMIT ${KF_GIT_TAG})
|
||||
set(KF_KCOREADDONS_GIT_COMMIT ${KF_GIT_TAG})
|
||||
set(KF_KWIDGETSADDONS_GIT_COMMIT ${KF_GIT_TAG})
|
||||
else()
|
||||
# Keep KF_GIT_TAG defined: define_definitions.cmake reports it.
|
||||
set(KF_GIT_TAG v6.10.0)
|
||||
endif()
|
||||
# using a function in order to limit the scope of the variables
|
||||
@@ -53,19 +76,19 @@ if(BUILD_WITH_KF)
|
||||
FetchContent_Declare(
|
||||
ecm
|
||||
GIT_REPOSITORY https://invent.kde.org/frameworks/extra-cmake-modules.git
|
||||
GIT_TAG ${KF_GIT_TAG})
|
||||
GIT_TAG ${KF_ECM_GIT_COMMIT})
|
||||
FetchContent_MakeAvailable(ecm)
|
||||
|
||||
FetchContent_Declare(
|
||||
kcoreaddons
|
||||
GIT_REPOSITORY https://invent.kde.org/frameworks/kcoreaddons.git
|
||||
GIT_TAG ${KF_GIT_TAG})
|
||||
GIT_TAG ${KF_KCOREADDONS_GIT_COMMIT})
|
||||
FetchContent_MakeAvailable(kcoreaddons)
|
||||
|
||||
FetchContent_Declare(
|
||||
kwidgetsaddons
|
||||
GIT_REPOSITORY https://invent.kde.org/frameworks/kwidgetsaddons.git
|
||||
GIT_TAG ${KF_GIT_TAG})
|
||||
GIT_TAG ${KF_KWIDGETSADDONS_GIT_COMMIT})
|
||||
FetchContent_MakeAvailable(kwidgetsaddons)
|
||||
endfunction()
|
||||
qet_make_kf_available()
|
||||
|
||||
@@ -22,10 +22,51 @@ option(BUILD_PUGIXML "Build pugixml library, use system one otherwise" YES)
|
||||
|
||||
if(BUILD_PUGIXML)
|
||||
|
||||
# Pinned to the commit v1.15 points at, not to the tag itself.
|
||||
#
|
||||
# A git tag is only a named pointer to a commit, and anyone with push access
|
||||
# to the upstream repository can move it (git push --force) to any other
|
||||
# commit. FetchContent fetches whatever the tag points at when the build
|
||||
# runs, so if a maintainer account or CI token is compromised, the attacker
|
||||
# can retarget a well-known release tag to malicious code: every fresh build
|
||||
# of QElectroTech then compiles it, while nothing changes in this repository
|
||||
# and the tag name still looks correct. A commit hash cannot be moved, because
|
||||
# it is derived from the content: different code always has a different hash.
|
||||
#
|
||||
# This attack has been used in the wild:
|
||||
# - March 2025, tj-actions/changed-files (CVE-2025-30066): tags v1 through
|
||||
# v45.0.7 were retargeted to a commit that dumped CI secrets into build
|
||||
# logs, affecting more than 23,000 repositories.
|
||||
# - March 2026, aquasecurity/trivy-action (CVE-2026-33634): 76 of 77
|
||||
# version tags were force-pushed to a credential stealer and stayed
|
||||
# malicious for about 12 hours.
|
||||
# Both were GitHub Actions rather than CMake dependencies, but the mechanism
|
||||
# is the same one FetchContent relies on here: resolving a git tag at build
|
||||
# time.
|
||||
#
|
||||
# To upgrade, look up the commit the new tag points at with
|
||||
# git ls-remote <repo> 'refs/tags/<tag>*', check that it is the release you
|
||||
# expect, and update both the hash and the trailing tag comment.
|
||||
#
|
||||
# How many lines that prints depends on which of the two kinds of tag
|
||||
# upstream created:
|
||||
# - A lightweight tag is nothing but a ref pointing straight at the commit,
|
||||
# so ls-remote prints a single line, "refs/tags/<tag>", and its hash is
|
||||
# the commit to pin. pugixml tags this way, which is why the v1.15 hash
|
||||
# below is what "git ls-remote ... refs/tags/v1.15" reports directly;
|
||||
# SingleApplication (v3.2.0) does the same.
|
||||
# - An annotated tag is a git object in its own right, carrying a tagger,
|
||||
# a date, a message and optionally a GPG signature, and pointing at the
|
||||
# commit. ls-remote then prints two lines: "refs/tags/<tag>" is the tag
|
||||
# object and "refs/tags/<tag>^{}" is that object dereferenced, i.e. the
|
||||
# commit. The KDE Frameworks modules tag this way, so for them it is the
|
||||
# "^{}" hash that belongs in the pin; the other hash identifies the tag
|
||||
# object itself, which is not the source revision and changes whenever
|
||||
# upstream re-creates the tag, even over the very same commit.
|
||||
FetchContent_Declare(
|
||||
pugixml
|
||||
GIT_REPOSITORY https://github.com/zeux/pugixml.git
|
||||
GIT_TAG v1.15)
|
||||
GIT_TAG ee86beb30e4973f5feffe3ce63bfa4fbadf72f38) # v1.15
|
||||
set(PUGIXML_INSTALL OFF CACHE INTERNAL "")
|
||||
FetchContent_MakeAvailable(pugixml)
|
||||
else()
|
||||
|
||||
@@ -31,9 +31,15 @@ if(EXISTS "${CMAKE_SOURCE_DIR}/SingleApplication/CMakeLists.txt")
|
||||
set(FETCHCONTENT_SOURCE_DIR_SINGLEAPPLICATION "${CMAKE_SOURCE_DIR}/SingleApplication")
|
||||
endif()
|
||||
|
||||
# Pinned to the commit v3.2.0 points at, not to the tag itself; see the note in
|
||||
# fetch_pugixml.cmake. v3.2.0 is a lightweight tag, a ref pointing straight at
|
||||
# the commit, so "git ls-remote <repo> refs/tags/v3.2.0" prints that commit and
|
||||
# nothing else. An annotated tag, as KDE uses in fetch_kdeaddons.cmake, would
|
||||
# print the tag object under refs/tags/v3.2.0 as well, with the commit on the
|
||||
# refs/tags/v3.2.0^{} line.
|
||||
FetchContent_Declare(
|
||||
SingleApplication
|
||||
GIT_REPOSITORY https://github.com/itay-grudev/SingleApplication.git
|
||||
GIT_TAG v3.2.0)
|
||||
GIT_TAG aede311d28d20179216c5419b581087be2a8409f) # v3.2.0
|
||||
set(QT_DEFAULT_MAJOR_VERSION 6)
|
||||
FetchContent_MakeAvailable(SingleApplication)
|
||||
|
||||
@@ -62,7 +62,9 @@ if(WIN32)
|
||||
# puts the .qm files (see build-aux/windows/QElectroTech.wxs and the
|
||||
# windows-build workflow), and what the shortcuts pass as --lang-dir.
|
||||
set(QET_LANG_PATH "lang/")
|
||||
set(QET_EXAMPLES_PATH "examples/")
|
||||
set(QET_LICENSE_PATH "./")
|
||||
set(QET_ICONS_PATH "icons/hicolor/")
|
||||
# Liste des ressources Windows
|
||||
#RC_FILE = qelectrotech.rc
|
||||
endif()
|
||||
|
||||
@@ -248,6 +248,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/qet.h
|
||||
${QET_DIR}/sources/qeticons.cpp
|
||||
${QET_DIR}/sources/qeticons.h
|
||||
${QET_DIR}/sources/palettegraphicsview.cpp
|
||||
${QET_DIR}/sources/palettegraphicsview.h
|
||||
${QET_DIR}/sources/qetpalette.cpp
|
||||
${QET_DIR}/sources/qetpalette.h
|
||||
${QET_DIR}/sources/qetstyle.cpp
|
||||
@@ -310,6 +312,8 @@ set(QET_SRC_FILES
|
||||
|
||||
${QET_DIR}/sources/dataBase/projectdatabase.cpp
|
||||
${QET_DIR}/sources/dataBase/projectdatabase.h
|
||||
${QET_DIR}/sources/dataBase/sqlreadonly.cpp
|
||||
${QET_DIR}/sources/dataBase/sqlreadonly.h
|
||||
|
||||
${QET_DIR}/sources/dataBase/ui/elementquerywidget.cpp
|
||||
${QET_DIR}/sources/dataBase/ui/elementquerywidget.h
|
||||
@@ -505,10 +509,6 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/PropertiesEditor/propertieseditorwidget.cpp
|
||||
${QET_DIR}/sources/PropertiesEditor/propertieseditorwidget.h
|
||||
|
||||
${QET_DIR}/pugixml/src/pugiconfig.hpp
|
||||
${QET_DIR}/pugixml/src/pugixml.cpp
|
||||
${QET_DIR}/pugixml/src/pugixml.hpp
|
||||
|
||||
${QET_DIR}/sources/qetgraphicsitem/conductor.cpp
|
||||
${QET_DIR}/sources/qetgraphicsitem/conductor.h
|
||||
${QET_DIR}/sources/qetgraphicsitem/conductortextitem.cpp
|
||||
@@ -705,6 +705,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/ui/conductorpropertiesdialog.h
|
||||
${QET_DIR}/sources/ui/conductorcolortoolbutton.cpp
|
||||
${QET_DIR}/sources/ui/conductorcolortoolbutton.h
|
||||
${QET_DIR}/sources/ui/diagrambgcolorbutton.cpp
|
||||
${QET_DIR}/sources/ui/diagrambgcolorbutton.h
|
||||
${QET_DIR}/sources/ui/conductorpropertieswidget.cpp
|
||||
${QET_DIR}/sources/ui/conductorpropertieswidget.h
|
||||
${QET_DIR}/sources/ui/configsaveloaderwidget.cpp
|
||||
@@ -721,6 +723,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/ui/backupdialog.h
|
||||
${QET_DIR}/sources/ui/dialogwaiting.cpp
|
||||
${QET_DIR}/sources/ui/dialogwaiting.h
|
||||
${QET_DIR}/sources/ui/duplicateoffsetdialog.cpp
|
||||
${QET_DIR}/sources/ui/duplicateoffsetdialog.h
|
||||
${QET_DIR}/sources/ui/dynamicelementtextitemeditor.cpp
|
||||
${QET_DIR}/sources/ui/dynamicelementtextitemeditor.h
|
||||
${QET_DIR}/sources/ui/dynamicelementtextmodel.cpp
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||
<!-- keyboard body with a lighter top edge -->
|
||||
<rect x="6" y="30" width="116" height="68" rx="8" fill="#31363b"/>
|
||||
<rect x="6" y="30" width="116" height="34" rx="8" fill="#4d4d4d"/>
|
||||
<rect x="6" y="46" width="116" height="18" fill="#31363b"/>
|
||||
<!-- key rows -->
|
||||
<g fill="#eff0f1">
|
||||
<rect x="14" y="38" width="8" height="8" rx="1.5"/><rect x="24" y="38" width="8" height="8" rx="1.5"/>
|
||||
<rect x="34" y="38" width="8" height="8" rx="1.5"/><rect x="44" y="38" width="8" height="8" rx="1.5"/>
|
||||
<rect x="54" y="38" width="8" height="8" rx="1.5"/><rect x="64" y="38" width="8" height="8" rx="1.5"/>
|
||||
<rect x="74" y="38" width="8" height="8" rx="1.5"/><rect x="84" y="38" width="8" height="8" rx="1.5"/>
|
||||
<rect x="94" y="38" width="8" height="8" rx="1.5"/><rect x="104" y="38" width="10" height="8" rx="1.5"/>
|
||||
<rect x="14" y="50" width="12" height="8" rx="1.5"/><rect x="28" y="50" width="8" height="8" rx="1.5"/>
|
||||
<rect x="38" y="50" width="8" height="8" rx="1.5"/><rect x="48" y="50" width="8" height="8" rx="1.5"/>
|
||||
<rect x="58" y="50" width="8" height="8" rx="1.5"/><rect x="68" y="50" width="8" height="8" rx="1.5"/>
|
||||
<rect x="78" y="50" width="8" height="8" rx="1.5"/><rect x="88" y="50" width="8" height="8" rx="1.5"/>
|
||||
<rect x="98" y="50" width="16" height="8" rx="1.5"/>
|
||||
<rect x="14" y="62" width="16" height="8" rx="1.5"/><rect x="32" y="62" width="8" height="8" rx="1.5"/>
|
||||
<rect x="42" y="62" width="8" height="8" rx="1.5"/><rect x="52" y="62" width="8" height="8" rx="1.5"/>
|
||||
<rect x="62" y="62" width="8" height="8" rx="1.5"/><rect x="72" y="62" width="8" height="8" rx="1.5"/>
|
||||
<rect x="82" y="62" width="8" height="8" rx="1.5"/><rect x="92" y="62" width="22" height="8" rx="1.5"/>
|
||||
<rect x="14" y="74" width="10" height="8" rx="1.5"/><rect x="26" y="74" width="10" height="8" rx="1.5"/>
|
||||
<rect x="38" y="74" width="52" height="8" rx="1.5"/>
|
||||
<rect x="92" y="74" width="10" height="8" rx="1.5"/><rect x="104" y="74" width="10" height="8" rx="1.5"/>
|
||||
</g>
|
||||
<!-- the two keys of a shortcut, pressed -->
|
||||
<rect x="14" y="62" width="16" height="8" rx="1.5" fill="#3daee9"/>
|
||||
<rect x="42" y="62" width="8" height="8" rx="1.5" fill="#3daee9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,48 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||
<!-- DIN rail -->
|
||||
<rect x="6" y="86" width="116" height="18" rx="3" fill="#7f8c8d"/>
|
||||
<rect x="6" y="92" width="116" height="6" fill="#4d4d4d"/>
|
||||
<!-- six terminal blocks: L, L, N, PE, L, spare -->
|
||||
<g fill="#eff0f1">
|
||||
<rect x="12" y="26" width="16" height="66" rx="2"/>
|
||||
<rect x="30" y="26" width="16" height="66" rx="2"/>
|
||||
<rect x="48" y="26" width="16" height="66" rx="2"/>
|
||||
<rect x="66" y="26" width="16" height="66" rx="2"/>
|
||||
<rect x="84" y="26" width="16" height="66" rx="2"/>
|
||||
<rect x="102" y="26" width="16" height="66" rx="2"/>
|
||||
</g>
|
||||
<!-- right-hand shading of each block -->
|
||||
<g fill="#bdc3c7">
|
||||
<rect x="24" y="28" width="4" height="62"/>
|
||||
<rect x="42" y="28" width="4" height="62"/>
|
||||
<rect x="60" y="28" width="4" height="62"/>
|
||||
<rect x="78" y="28" width="4" height="62"/>
|
||||
<rect x="96" y="28" width="4" height="62"/>
|
||||
<rect x="114" y="28" width="4" height="62"/>
|
||||
</g>
|
||||
<!-- colored marker bands -->
|
||||
<rect x="12" y="26" width="16" height="10" rx="2" fill="#7f8c8d"/>
|
||||
<rect x="30" y="26" width="16" height="10" rx="2" fill="#7f8c8d"/>
|
||||
<rect x="48" y="26" width="16" height="10" rx="2" fill="#3daee9"/>
|
||||
<rect x="66" y="26" width="16" height="10" rx="2" fill="#27ae60"/>
|
||||
<rect x="70" y="26" width="8" height="10" fill="#fdbc4b"/>
|
||||
<rect x="84" y="26" width="16" height="10" rx="2" fill="#7f8c8d"/>
|
||||
<rect x="102" y="26" width="16" height="10" rx="2" fill="#da4453"/>
|
||||
<!-- screws -->
|
||||
<g fill="#31363b">
|
||||
<circle cx="20" cy="50" r="4.5"/><circle cx="20" cy="74" r="4.5"/>
|
||||
<circle cx="38" cy="50" r="4.5"/><circle cx="38" cy="74" r="4.5"/>
|
||||
<circle cx="56" cy="50" r="4.5"/><circle cx="56" cy="74" r="4.5"/>
|
||||
<circle cx="74" cy="50" r="4.5"/><circle cx="74" cy="74" r="4.5"/>
|
||||
<circle cx="92" cy="50" r="4.5"/><circle cx="92" cy="74" r="4.5"/>
|
||||
<circle cx="110" cy="50" r="4.5"/><circle cx="110" cy="74" r="4.5"/>
|
||||
</g>
|
||||
<g fill="#eff0f1">
|
||||
<rect x="16" y="49" width="8" height="2"/><rect x="16" y="73" width="8" height="2"/>
|
||||
<rect x="34" y="49" width="8" height="2"/><rect x="34" y="73" width="8" height="2"/>
|
||||
<rect x="52" y="49" width="8" height="2"/><rect x="52" y="73" width="8" height="2"/>
|
||||
<rect x="70" y="49" width="8" height="2"/><rect x="70" y="73" width="8" height="2"/>
|
||||
<rect x="88" y="49" width="8" height="2"/><rect x="88" y="73" width="8" height="2"/>
|
||||
<rect x="106" y="49" width="8" height="2"/><rect x="106" y="73" width="8" height="2"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -216,6 +216,7 @@ Clarification:
|
||||
ico/128x128/diagram.png by the QElectroTech team (License CC BY-ND 3.0)
|
||||
ico/128x128/document-export.png by the QElectroTech team (License CC BY-ND 3.0)
|
||||
ico/128x128/project.png by the QElectroTech team (License CC BY-ND 3.0)
|
||||
ico/128x128/terminalstrip.png and configure-shortcuts.png by Jeff Patterson from the QElectroTech team (License CC BY-ND 3.0), rendered from the .svg files beside them
|
||||
ico/scalable/pdf-import.svg by Jeff Patterson from the QElectroTech team (License CC BY-ND 3.0), laid out like ico/22x22/insert-image.png
|
||||
ico/scalable/diagram.svg, folio-new.svg, folio-delete.svg, folio-properties.svg, label.svg by Jeff Patterson from the QElectroTech team (License CC BY-ND 3.0), the folio icons redrawn in the same style
|
||||
ico/256x256/* by Nuri from the QElectroTech team (License CC BY-ND 3.0)
|
||||
|
||||
@@ -259,12 +259,14 @@
|
||||
<file alias="themes/qet/48x48/user-away-extended.png">48x48/user-away-extended.png</file>
|
||||
<file alias="themes/qet/48x48/user-away.png">48x48/user-away.png</file>
|
||||
<file alias="themes/qet/48x48/view-pim-journal.png">48x48/view-pim-journal.png</file>
|
||||
<file alias="themes/qet/128x128/configure-shortcuts.png">128x128/configure-shortcuts.png</file>
|
||||
<file alias="themes/qet/128x128/diagram.png">128x128/diagram.png</file>
|
||||
<file alias="themes/qet/128x128/document-export.png">128x128/document-export.png</file>
|
||||
<file alias="themes/qet/128x128/plasmagik.png">128x128/plasmagik.png</file>
|
||||
<file alias="themes/qet/128x128/printer.png">128x128/printer.png</file>
|
||||
<file alias="themes/qet/128x128/project.png">128x128/project.png</file>
|
||||
<file alias="themes/qet/128x128/settings.png">128x128/settings.png</file>
|
||||
<file alias="themes/qet/128x128/terminalstrip.png">128x128/terminalstrip.png</file>
|
||||
<file alias="themes/qet/scalable/diagram.svg">scalable/diagram.svg</file>
|
||||
<file alias="themes/qet/scalable/folio-delete.svg">scalable/folio-delete.svg</file>
|
||||
<file alias="themes/qet/scalable/folio-new.svg">scalable/folio-new.svg</file>
|
||||
@@ -469,5 +471,7 @@
|
||||
<file>themes/qet-dark/scalable/ellipse-to-bezier.svg</file>
|
||||
<file>themes/qet-dark/scalable/rect-to-bezier.svg</file>
|
||||
<file>themes/qet-dark/scalable/rect-to-polyline.svg</file>
|
||||
<file alias="themes/qet-dark/128x128/document-export.png">128x128/document-export.png</file>
|
||||
<file alias="themes/qet-dark/128x128/printer.png">128x128/printer.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -34,6 +34,16 @@ expects, so QIcon::fromTheme("name") finds them. The dark theme only holds
|
||||
the icons that need a dark variant: black line art. Colored icons are not
|
||||
touched; the dark theme inherits them from the light one.
|
||||
|
||||
Qt inherits by name, not by size: once a name has any file in the dark
|
||||
theme, the parent theme is never consulted for that name, and a size the
|
||||
dark theme lacks is served by scaling the nearest dark file. An icon that
|
||||
is line art at 22 pixels and colored at 128 (the printer) would then come
|
||||
out as the 22 pixel copy scaled up on a dark palette. So for every name
|
||||
the dark theme holds, the .qrc also aliases the light files of the sizes
|
||||
the dark theme does not have, when they read on the dark window (3:1,
|
||||
measured as tests/qttest/tst_qeticons.cpp does); a light file that does
|
||||
not is left out, and Qt scales the nearest dark copy as before.
|
||||
|
||||
An icon counts as line art when fewer than 20% of its visible pixels are
|
||||
saturated. Its dark copy keeps hue and alpha and inverts lightness, scaled
|
||||
so pure black becomes (220, 220, 220), the dark palette's text color.
|
||||
@@ -101,6 +111,8 @@ WHITE_LIGHTNESS = 0.85 # a visible pixel this light counts as white
|
||||
WHITE_FRACTION = 0.30 # at or above this an icon is "light art"
|
||||
INK = 220 / 255.0 # lightness of pure black after inversion
|
||||
SVG_INK = "#dcdcdc"
|
||||
DARK_WINDOW = (53, 53, 53) # QET::Palette::fusionDark() window color
|
||||
DARK_RATIO = 3.0 # what tst_qeticons requires of a dark theme file
|
||||
|
||||
|
||||
def visible_pixels(image):
|
||||
@@ -131,6 +143,28 @@ def has_light_fill(image):
|
||||
return white / len(pixels) >= WHITE_FRACTION
|
||||
|
||||
|
||||
def relative_luminance(rgb):
|
||||
def linear(c):
|
||||
c /= 255.0
|
||||
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
|
||||
r, g, b = rgb
|
||||
return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b)
|
||||
|
||||
|
||||
def reads_on_dark(image):
|
||||
"""True when the icon's mean visible color reaches DARK_RATIO against
|
||||
the dark window, the test tst_qeticons applies to every dark file."""
|
||||
pixels = visible_pixels(image)
|
||||
if not pixels:
|
||||
return False
|
||||
n = len(pixels)
|
||||
mean = (sum(p[0] for p in pixels) // n, sum(p[1] for p in pixels) // n,
|
||||
sum(p[2] for p in pixels) // n)
|
||||
lighter, darker = sorted((relative_luminance(mean), relative_luminance(DARK_WINDOW)),
|
||||
reverse=True)
|
||||
return (lighter + 0.05) / (darker + 0.05) >= DARK_RATIO
|
||||
|
||||
|
||||
def invert_lightness(image):
|
||||
"""Invert lightness, keeping hue, saturation and alpha.
|
||||
|
||||
@@ -240,6 +274,20 @@ def main():
|
||||
changed += write_if_changed(target, text)
|
||||
dark.append(f"themes/qet-dark/scalable/{name}")
|
||||
|
||||
# Complete each dark name with the light files of its other sizes
|
||||
# that read on a dark window (see the module docstring): aliases, no
|
||||
# copies.
|
||||
dark_sizes = {}
|
||||
for path in dark:
|
||||
size, name = path.split("/")[2:]
|
||||
dark_sizes.setdefault(name, set()).add(size)
|
||||
dark_aliases = []
|
||||
for alias, source in light:
|
||||
size, name = alias.split("/")[2:]
|
||||
if name in dark_sizes and size not in dark_sizes[name] \
|
||||
and reads_on_dark(Image.open(ICO / source).convert("RGBA")):
|
||||
dark_aliases.append((f"themes/qet-dark/{size}/{name}", source))
|
||||
|
||||
# Drop dark files from an earlier run that are no longer generated, so
|
||||
# a reclassified icon falls back to the light theme instead of keeping
|
||||
# a stale copy.
|
||||
@@ -264,6 +312,8 @@ def main():
|
||||
qrc.append(f' <file alias="{alias}">{source}</file>')
|
||||
for path in dark:
|
||||
qrc.append(f" <file>{path}</file>")
|
||||
for alias, source in dark_aliases:
|
||||
qrc.append(f' <file alias="{alias}">{source}</file>')
|
||||
qrc += [" </qresource>", "</RCC>", ""]
|
||||
changed += write_if_changed(QRC, "\n".join(qrc))
|
||||
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
# qet-mcp — a Model Context Protocol server for QElectroTech projects
|
||||
|
||||
A small stdio MCP server that lets an AI assistant read and verify
|
||||
QElectroTech projects: what is in a project, what an edit actually
|
||||
changed, and what a whole corpus of projects contains.
|
||||
|
||||
It has **no third-party dependencies** — Python 3.9+ and the standard
|
||||
library only. The MCP SDK is not required.
|
||||
|
||||
## Why
|
||||
|
||||
Verifying a change by screenshot is unreliable, and this tool exists
|
||||
because that unreliability produced two wrong conclusions in one review
|
||||
session:
|
||||
|
||||
- A drag of a multi-element selection *looked* like it had left the
|
||||
symbols behind and detached their labels. Diffing the saved file showed
|
||||
all four elements had moved by an identical `(0, -80)` and **no label
|
||||
had moved at all**. A bug report was one step away from being filed.
|
||||
- An "Apply" button *looked* like it did nothing. It was disabled,
|
||||
because a required field was empty.
|
||||
|
||||
Both times the pixels misled and the model told the truth. So the tools
|
||||
here read the model.
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | What it answers |
|
||||
|---|---|
|
||||
| `qet_project_info` | title, format version, folios, element and conductor counts |
|
||||
| `qet_elements` | placed elements: uuid, type, position, label, information bag |
|
||||
| `qet_conductors` | conductors and their documentation fields; filter by attribute |
|
||||
| `qet_diff` | **what an edit actually changed** — element moves, adds, removes, relabels; conductor changes; and folio fields, texts, shapes, images, symbol text fields and terminal strips |
|
||||
| `qet_scan` | sweep a directory of projects, counting nodes carrying an attribute |
|
||||
| `qet_element_info` | a `.elmt`: translated names, terminals, info fields, part counts |
|
||||
| `qet_export` | run a headless export (pdf, png, svg, bom, cables, wires, wiring, nets, links, info) |
|
||||
| `qet_edit` | **change a project** — place, move, rotate, label, wire, number, cross-reference, add text, shapes and images, restyle a symbol's text fields, delete; then diff the result |
|
||||
| `qet_element_build` | **author a `.elmt`** — draw a new symbol, with terminals to wire it by |
|
||||
| `qet_project_new` | **start from nothing** — an empty project with a title and folios |
|
||||
| `qet_element_search` | **find a symbol** in a collection by name (any language), type or terminal count |
|
||||
| `qet_check` | **design-rule checks** — duplicate labels, unlabelled masters, unnumbered conductors, empty folios |
|
||||
| `qet_query` | **ask the project database** — read-only SQL over the views and tables |
|
||||
|
||||
`qet_export` and `qet_edit` launch QElectroTech. Everything else parses the
|
||||
file directly, which is faster, needs no display, and cannot be confused by
|
||||
a dialog.
|
||||
|
||||
## Running it
|
||||
|
||||
```bash
|
||||
# list the tools and exit
|
||||
misc/qet-mcp/qet_mcp.py --list
|
||||
|
||||
# speak MCP on stdin/stdout
|
||||
misc/qet-mcp/qet_mcp.py
|
||||
```
|
||||
|
||||
Register it with an MCP client, for example:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"qet": {
|
||||
"command": "python3",
|
||||
"args": ["/path/to/qelectrotech/misc/qet-mcp/qet_mcp.py"],
|
||||
"env": {
|
||||
"QET_MCP_WORKSPACE": "/home/you/drawings",
|
||||
"QET_ENABLE_SCRIPTING": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Five tools need scripting switched on
|
||||
|
||||
A QElectroTech with JavaScript scripting switched off refuses `--run`, and
|
||||
off is the default from
|
||||
[#984](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/984)
|
||||
onwards. Five tools here drive it that way and stop working until it is
|
||||
turned on:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| need `QET_ENABLE_SCRIPTING=1` | `qet_query`, `qet_continuity`, `qet_check`, `qet_project_new`, `qet_edit` |
|
||||
| unaffected | everything else — they read the `.qet` directly, or, in `qet_export`'s case, use a plain CLI flag |
|
||||
|
||||
The variable goes in the environment this server is started in, which for an
|
||||
MCP client is the `env` block above; the server passes its environment
|
||||
straight through to QElectroTech. It does not set the variable itself, on
|
||||
purpose — a switch a program turns on for itself is not a switch. Whoever
|
||||
configured this server and pointed it at a QElectroTech binary made that
|
||||
choice, and their interactive QElectroTech keeps whatever its own setting
|
||||
says.
|
||||
|
||||
Without it, those five come back `"ok": false` with a `hint` naming the
|
||||
variable. Older builds, from before the setting existed, need nothing.
|
||||
|
||||
## What the server is allowed to touch
|
||||
|
||||
Every path in a tool call is chosen by the model, so without a policy this
|
||||
server would be a read/write primitive for anything the operating system
|
||||
lets the process reach: read any project on the disk, export one somewhere
|
||||
else, overwrite an unrelated file, embed an arbitrary local image or PDF.
|
||||
|
||||
So **data paths are confined to a workspace**:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `QET_MCP_WORKSPACE` | the directories tool calls may read and write, separated by `:` (`;` on Windows) |
|
||||
| unset | the directory the server was started in |
|
||||
| `QET_MCP_ALLOW_ANY_PATH=1` | turns the check off entirely |
|
||||
|
||||
Set the workspace to the folder your drawings live in. A path outside it is
|
||||
refused with an error naming what was allowed; symlinks are resolved first,
|
||||
so a link planted inside the workspace is judged by where it points.
|
||||
|
||||
Two arguments are deliberately **not** confined: `binary` (the
|
||||
`qelectrotech` executable) and `elements_dir` (the element collection).
|
||||
Those are configuration, chosen once by whoever runs the server, and both
|
||||
normally live in `/usr` or a build tree — outside any sensible workspace.
|
||||
Confining them would reject the ordinary case while stopping nothing.
|
||||
|
||||
`QET_MCP_ALLOW_ANY_PATH=1` is equivalent to granting the client local
|
||||
filesystem access with this process's privileges. It exists so that is a
|
||||
deliberate choice rather than the default.
|
||||
|
||||
**Nothing is overwritten unasked.** `qet_export`, `qet_edit`,
|
||||
`qet_project_new` and `qet_element_build` refuse an `output` that already
|
||||
exists unless the call passes `"overwrite": true`. Replacing a file is the
|
||||
one step this server cannot undo, so it is the one step it will not take on
|
||||
its own.
|
||||
|
||||
The confinement is applied where tool arguments enter the server, not inside
|
||||
each tool. Importing `qet_mcp` and calling `tool_export()` from your own
|
||||
Python is not confined and is not meant to be — that is your code calling a
|
||||
library, and you already chose the paths.
|
||||
|
||||
## Worked examples
|
||||
|
||||
**What did that edit change?**
|
||||
|
||||
```json
|
||||
{"name": "qet_diff", "arguments": {"before": "a.qet", "after": "b.qet"}}
|
||||
```
|
||||
|
||||
```json
|
||||
"elements": { "moved_count": 4,
|
||||
"distinct_move_deltas": [[0.0, -80.0]],
|
||||
"relabelled": [], "info_changed": [] }
|
||||
```
|
||||
|
||||
Four elements moved by one uniform delta; nothing was relabelled. That is
|
||||
the answer a screenshot gave wrongly.
|
||||
|
||||
**Draw something, and check it landed**
|
||||
|
||||
```json
|
||||
{"name": "qet_edit", "arguments": {
|
||||
"binary": "/path/to/qelectrotech",
|
||||
"project": "in.qet", "output": "out.qet",
|
||||
"elements_dir": "/path/to/qelectrotech/elements",
|
||||
"operations": [
|
||||
{"op": "add_folio", "id": "f"},
|
||||
{"op": "set_folio_title", "folio": "$f", "title": "Starter"},
|
||||
{"op": "add_element", "id": "k1", "folio": "$f", "path": "common://.../coil.elmt", "x": 100, "y": 100},
|
||||
{"op": "add_element", "id": "k2", "folio": "$f", "path": "common://.../coil.elmt", "x": 320, "y": 100},
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$k1", "from_terminal": 0, "to": "$k2", "to_terminal": 0},
|
||||
{"op": "set_conductor", "folio": "$f", "element": "$k1", "terminal": 0, "property": "num", "value": "W7"},
|
||||
{"op": "set_label", "folio": "$f", "element": "$k1", "label": "KM1"}
|
||||
]}}
|
||||
```
|
||||
|
||||
An op that creates something takes an `"id"`; later ops name it as `"$id"`.
|
||||
Terminals are addressed by index — top to bottom, then left to right, **not**
|
||||
the order the `.elmt` lists them. `qet_element_info` and `qet_element_search`
|
||||
both report that index order. The answer carries a per-operation result
|
||||
*and* a `qet_diff`, because "addConductor → true" says the call was
|
||||
accepted, not that the file came out right:
|
||||
|
||||
```json
|
||||
"diff": {"elements": {"before": 11, "after": 13, "added": ["{0aa3…}", "{6f63…}"]},
|
||||
"conductors": {"before": 47, "after": 48, "added": ["4:{0aa3…}/{2904…}--{6f63…}/{2904…}"],
|
||||
"removed": []}}
|
||||
```
|
||||
|
||||
**Draw a symbol that does not exist yet**
|
||||
|
||||
```json
|
||||
{"name": "qet_element_build", "arguments": {
|
||||
"output": "/path/to/collection/99_custom/my_resistor.elmt",
|
||||
"names": {"en": "Test resistor", "fr": "Résistance de test"},
|
||||
"parts": [
|
||||
{"type": "rect", "x": -10, "y": -20, "width": 20, "height": 40},
|
||||
{"type": "line", "x1": 0, "y1": -30, "x2": 0, "y2": -20},
|
||||
{"type": "line", "x1": 0, "y1": 20, "x2": 0, "y2": 30},
|
||||
{"type": "text", "x": 14, "y": -4, "text": "R"}
|
||||
],
|
||||
"terminals": [{"x": 0, "y": -30, "orientation": "n", "name": "1"},
|
||||
{"x": 0, "y": 30, "orientation": "s", "name": "2"}]}}
|
||||
```
|
||||
|
||||
Then place it with `qet_edit` like any catalogue element. Unlike a
|
||||
project, a `.elmt` is not rewritten by QElectroTech on a round trip, so
|
||||
generating one here is safe in a way that generating a `.qet` would not
|
||||
be — there is no `toXml()` waiting to drop what this writer did not know
|
||||
to emit.
|
||||
|
||||
**Ask a question the XML cannot answer**
|
||||
|
||||
```json
|
||||
{"name": "qet_query", "arguments": {
|
||||
"binary": "/path/to/qelectrotech", "project": "industrial.qet",
|
||||
"sql": "SELECT label, COUNT(*) AS n FROM element_nomenclature_view WHERE label <> '' GROUP BY label HAVING n > 1 ORDER BY n DESC"}}
|
||||
```
|
||||
|
||||
```json
|
||||
"rows": [{"label": "V6", "n": 7}, {"label": "V5", "n": 6}, {"label": "V4", "n": 6}]
|
||||
```
|
||||
|
||||
Duplicate element labels in a shipped example — a design-rule question,
|
||||
answered by the database that already knew it.
|
||||
|
||||
**How much of a corpus uses a field?**
|
||||
|
||||
```json
|
||||
{"name": "qet_scan",
|
||||
"arguments": {"directory": "examples", "tag": "conductor", "attribute": "cable"}}
|
||||
```
|
||||
|
||||
```json
|
||||
{ "files": 24, "total": 3190, "non_empty": 0, "distinct_values": [] }
|
||||
```
|
||||
|
||||
Across the shipped examples: 3190 conductors, not one with a cable value.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
python3 test_qet_mcp.py # unit + protocol, no QElectroTech needed
|
||||
QET_BINARY=/path/to/qelectrotech \
|
||||
QET_ELEMENTS=/path/to/qelectrotech/elements \
|
||||
QET_EXAMPLES=/path/to/qelectrotech/examples \
|
||||
QET_ENABLE_SCRIPTING=1 \
|
||||
python3 test_qet_mcp.py # everything
|
||||
```
|
||||
|
||||
`QET_ENABLE_SCRIPTING=1` matters from #984 onwards: without it the
|
||||
integration tests that drive QElectroTech through a script all fail, and
|
||||
they fail as "the edit did nothing" rather than as "scripting is off", which
|
||||
reads like a regression in the thing under test.
|
||||
|
||||
176 tests in three layers: unit (validation, script generation, the terminal
|
||||
order rule, the diff, the part schema), the real stdio transport, and
|
||||
integration against a built QElectroTech. Several exist because the
|
||||
behaviour they pin was once wrong and looked right, and say so in their
|
||||
docstrings. To check the suite itself rather than trust it, each of those
|
||||
bugs was reintroduced in turn and the suite confirmed to fail: ten in the
|
||||
Python, plus the hang guard on `addConductor` and the database refresh in
|
||||
`ConductorCreator` in the C++.
|
||||
|
||||
## Notes and limits
|
||||
|
||||
- **The project database is reachable now, through `qet_query`.** It was
|
||||
not when this server was written, which is why every other structural
|
||||
tool here re-derives its answer from the XML. Prefer the views —
|
||||
`element_nomenclature_view`, `project_summary_view`, `wiring_list_view`
|
||||
— which exist to be queried; the underlying tables are how the cache is
|
||||
arranged today and a column may move. Call `qet_query` with no `sql` to
|
||||
list both. Only `SELECT` and `WITH` are accepted, which is the rule
|
||||
QElectroTech applies to its own custom-query box, not one invented here.
|
||||
An empty result and a failed query are told apart: `row_count` 0 with no
|
||||
`error` means nothing matched, and a typo'd column name says so.
|
||||
- **`qet_export` isolates its launch.** SingleApplication keys its socket
|
||||
on `applicationFilePath()`, so a second launch of the same binary path
|
||||
forwards its request to an already-running instance and returns *that*
|
||||
process's answer with no error. The tool copies the binary to a unique
|
||||
temporary path, gives it a private `HOME`, and runs it on the offscreen
|
||||
platform. A symlink would not work: `applicationFilePath()` resolves it
|
||||
back to the real path.
|
||||
- **The CLI matches its flags exactly.** `--export-bom out.csv` is the
|
||||
supported form; `--export-bom=out.csv` is not recognised as an export
|
||||
at all, so the application starts its interface instead and a headless
|
||||
run hangs. The tool uses the positional form.
|
||||
- **Conductor identity is the hard part of `qet_diff`.** A conductor names
|
||||
its ends with `terminal1`/`terminal2`, and the project format has two
|
||||
schemes: folio-scoped integer ids in older files, terminal-definition
|
||||
uuids plus `element1`/`element2` in newer ones. The integer ids are
|
||||
**renumbered on every save**, so keying on them — which this tool did at
|
||||
first — made all 47 conductors of an untouched folio read as removed and
|
||||
re-added the moment the other side had been through QElectroTech, which
|
||||
is exactly what `qet_edit` produces. They are now keyed by owning element
|
||||
uuid plus terminal, which is stable across a save: measured at 0 colliding
|
||||
keys over 3190 conductors in the 24 shipped examples, and 0 churn on a
|
||||
no-op edit. Where an element predates persisted uuids the end cannot be
|
||||
resolved and keeps a `#`-marked unstable key; the diff then reports
|
||||
`unstable_keys` and says so rather than pretending to be comparable.
|
||||
- **Texts, shapes and images have no uuid**, so `qet_diff` cannot say "the same
|
||||
text, edited": an edited text reads as the old one removed and a new one
|
||||
added, both shown. Shapes and images are keyed by position, so a restyle
|
||||
or rescale *is* reported as a change to that item, but a move reads as a
|
||||
removal plus an addition. The folio `version` attribute is left out of the
|
||||
comparison on purpose: QElectroTech rewrites it on every save, and
|
||||
including it made every folio of any re-saved project look edited.
|
||||
- **Elements** written before persisted uuids fall back to a positional key,
|
||||
which makes a move in such a file read as a remove plus an add.
|
||||
- **`qet_edit` needs a build whose scripting API carries the drawing verbs.**
|
||||
Against an older one it reports exactly which methods are missing and
|
||||
changes nothing. `addElement` and the move/delete verbs shipped with the
|
||||
scripting API; `addConductor`, `rotateElement`, `setElementLabel`,
|
||||
`setElementInfo` and `setFolioTitle` are newer.
|
||||
- **`elements_dir` is not optional for `common://` paths.** The sandboxed
|
||||
run has its own empty HOME, so QElectroTech falls back to the compiled-in
|
||||
collection path, which on a machine that never ran `make install` does not
|
||||
exist. The only symptom is `addElement` reporting that a file plainly
|
||||
present "does not resolve to an element". An absolute `.elmt` path works
|
||||
without it.
|
||||
- **`set_conductor` changes the whole potential, not one segment.** That is
|
||||
what the application does — a wire number describes a potential — so name
|
||||
a terminal carrying exactly one conductor and the change reaches every
|
||||
conductor electrically joined to it. A terminal several conductors meet
|
||||
at names none of them and is refused, so address a potential from one of
|
||||
its leaves. Property names are the file's own, so `qet_conductors` reads
|
||||
back exactly what was set.
|
||||
- **`link_elements` takes a folio for each end**, because a master and its
|
||||
slave are normally on different folios. Whether a pair may be linked is
|
||||
decided by QElectroTech's own `isLinkable()`, so a script cannot make a
|
||||
link the GUI would refuse.
|
||||
- **An element must live inside a collection to be placeable.** This is
|
||||
not about the path syntax: an absolute `.elmt` path works, but only if
|
||||
the file sits under a directory QElectroTech knows as a collection.
|
||||
Write it under the tree you pass as `elements_dir` and `qet_edit` can
|
||||
place it, by absolute path or as `common://…`; write it anywhere else
|
||||
and `add_element` reports only "does not resolve to an element".
|
||||
- **`qet_element_build` computes the `.elmt` size header, and checks it.**
|
||||
`width`/`height`/`hotspot_x`/`hotspot_y` relate to the drawing by a
|
||||
containment constraint, not a formula — the declared box runs from
|
||||
`(-hotspot_x, -hotspot_y)` to `(width - hotspot_x, height - hotspot_y)`
|
||||
and the drawing must fit inside it. The shipped collection shows authors
|
||||
picking their own margins (one element pads 2 units left and 3 right,
|
||||
another 8 and 2), so there is no convention to copy, only an invariant
|
||||
to satisfy. A drawing that escaped its box is the classic way a
|
||||
hand-written element renders clipped in the collection panel while
|
||||
looking fine in XML.
|
||||
- **QElectroTech interrupts a script at 30 s** of its own accord, separately
|
||||
from this tool's `timeout`. A very long operation list will hit that
|
||||
first.
|
||||
- **`qet_edit` never writes the input.** It saves to a separate file and
|
||||
diffs the two, so the original is always the thing the diff is against.
|
||||
@@ -0,0 +1,44 @@
|
||||
<definition version="0.100.0" type="element" link_type="master" width="40" height="60" hotspot_x="17" hotspot_y="32">
|
||||
<uuid uuid="{6d3714f6-a0e1-71b3-e4bd-5a35d4cbbec1}"/>
|
||||
<names>
|
||||
<name lang="ar">ملف KA بمحتفظ مغناطيسي</name>
|
||||
<name lang="ca">Bobina</name>
|
||||
<name lang="cs">Bistabilní remanentní relé</name>
|
||||
<name lang="de">Remanenzrelais</name>
|
||||
<name lang="el">Πηνίο με μανδάλωση</name>
|
||||
<name lang="en">Coil</name>
|
||||
<name lang="es">Bobina KA de remanencia</name>
|
||||
<name lang="fr">Bobine KA à rémanence</name>
|
||||
<name lang="hu">Tekercs</name>
|
||||
<name lang="it">Bobina</name>
|
||||
<name lang="nl">spoel remanent</name>
|
||||
<name lang="nl_BE">Spoel KA remanent</name>
|
||||
<name lang="pl">Cewka przekaźnika remanencyjnego</name>
|
||||
<name lang="pt_BR">Bobina</name>
|
||||
<name lang="ru">Обмотка</name>
|
||||
<name lang="zh">剩磁保持线圈</name>
|
||||
</names>
|
||||
<kindInformations>
|
||||
<kindInformation name="type">plc</kindInformation>
|
||||
</kindInformations>
|
||||
<informations>Author: The QElectroTech team
|
||||
License: see http://qelectrotech.org/wiki/doc/elements_license</informations>
|
||||
<description>
|
||||
<rect x="14" y="-8" width="6" height="16" rx="0" ry="0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="false"/>
|
||||
<line x1="0" y1="-20" x2="0" y2="-8" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="false"/>
|
||||
<dynamic_text x="2" y="11.33" z="5" text_width="-1" Halignment="AlignLeft" Valignment="AlignTop" frame="false" rotation="0" keep_visual_rotation="false" text_from="UserText" uuid="{647e33ed-520b-4d80-bbc4-7b31177b8f26}" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular">
|
||||
<text>A2</text>
|
||||
</dynamic_text>
|
||||
<dynamic_text x="2" y="-24.67" z="6" text_width="-1" Halignment="AlignLeft" Valignment="AlignTop" frame="false" rotation="0" keep_visual_rotation="false" text_from="UserText" uuid="{fb1f8de9-70c0-4801-8dc7-12e6bf88d8f5}" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular">
|
||||
<text>A1</text>
|
||||
</dynamic_text>
|
||||
<line x1="0" y1="8" x2="0" y2="20" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="false"/>
|
||||
<dynamic_text x="30" y="-9.17" z="8" text_width="-1" Halignment="AlignLeft" Valignment="AlignTop" frame="false" rotation="0" keep_visual_rotation="false" text_from="ElementInfo" uuid="{b7fccfc3-05f1-459c-9766-f49a481bd0ff}" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_text>
|
||||
<polygon x1="-14" y1="-8" x2="14" y2="-8" x3="20" y3="8" x4="-14" y4="8" antialias="false" style="line-style:normal;line-weight:normal;filling:none;color:black"/>
|
||||
<terminal uuid="{6a87c921-6f5d-4f5c-8673-228a6d13c5c8}" name="A2" x="0" y="20" orientation="s" type="Generic"/>
|
||||
<terminal uuid="{2904e5fa-6bbe-4127-acdd-f92feadd6ece}" name="A1" x="0" y="-20" orientation="n" type="Generic"/>
|
||||
</description>
|
||||
</definition>
|
||||
@@ -0,0 +1,16 @@
|
||||
<definition version="0.100.0" type="element" link_type="slave" width="20" height="60" hotspot_x="9" hotspot_y="30">
|
||||
<uuid uuid="{94d56f06-6814-5561-9148-2da3a9c4f900}"/>
|
||||
<names>
|
||||
<name lang="en">PLC Slave Test</name>
|
||||
</names>
|
||||
<kindInformations>
|
||||
<kindInformation name="type">plc</kindInformation>
|
||||
<kindInformation name="state">NO</kindInformation>
|
||||
<kindInformation name="number">1</kindInformation>
|
||||
</kindInformations>
|
||||
<description>
|
||||
<rect x="0" y="-8" width="6" height="16" rx="0" ry="0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="false"/>
|
||||
<terminal uuid="{6a87c921-6f5d-4f5c-8673-228a6d13c500}" name="1" x="0" y="20" orientation="s" type="Generic"/>
|
||||
<terminal uuid="{2904e5fa-6bbe-4127-acdd-f92feadd6e00}" name="2" x="0" y="-20" orientation="n" type="Generic"/>
|
||||
</description>
|
||||
</definition>
|
||||
Executable
+2773
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
#Based on raspberry pi 5 8 Gb Trixie
|
||||
#sudo apt install git ssh rsync libqt5svg5-dev qt5-qmake qtbase5-dev libkf5widgetsaddons-dev libkf5coreaddons-dev libsqlite3-dev pkgconf libqt5waylandclient5-dev libqt5waylandcompositor5-dev g++ make
|
||||
#sudo apt install git ssh rsync libqt5svg5-dev qt5-qmake qtbase5-dev libkf5widgetsaddons-dev libkf5coreaddons-dev pkgconf libqt5waylandclient5-dev libqt5waylandcompositor5-dev g++ make
|
||||
#mkdir -p AppImage/0.100.0/aarch64
|
||||
# Get GIT sources
|
||||
#git clone --recursive https://github.com/qelectrotech/qelectrotech-source-mirror.git
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
#include "../NameList/nameslist.h"
|
||||
#include "../diagramcontext.h"
|
||||
#include "pugixml/src/pugixml.hpp"
|
||||
#include "pugixml.hpp"
|
||||
|
||||
#include <QIcon>
|
||||
#include <QString>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
#ifndef NAMES_LIST_H
|
||||
#define NAMES_LIST_H
|
||||
#include "pugixml/src/pugixml.hpp"
|
||||
#include "pugixml.hpp"
|
||||
|
||||
#include <QtXml>
|
||||
/**
|
||||
|
||||
@@ -35,13 +35,17 @@ AddTerminalToStripCommand::AddTerminalToStripCommand(QSharedPointer<RealTerminal
|
||||
const auto t_label = terminal->label();
|
||||
const auto ts_name = strip->name();
|
||||
|
||||
const auto str_1 = t_label.isEmpty() ? QObject::tr("Ajouter une borne") :
|
||||
QObject::tr("Ajouter la borne %1").arg(t_label);
|
||||
|
||||
const auto str_2 = ts_name.isEmpty() ? QObject::tr("à un groupe de bornes") :
|
||||
QObject::tr("au groupe de bornes %1").arg(ts_name);
|
||||
|
||||
setText(str_1 % " " % str_2);
|
||||
QString text;
|
||||
if (ts_name.isEmpty()) {
|
||||
text = t_label.isEmpty()
|
||||
? QObject::tr("Ajouter une borne à un groupe de bornes")
|
||||
: QObject::tr("Ajouter la borne %1 à un groupe de bornes").arg(t_label);
|
||||
} else {
|
||||
text = t_label.isEmpty()
|
||||
? QObject::tr("Ajouter une borne au groupe de bornes %1").arg(ts_name)
|
||||
: QObject::tr("Ajouter la borne %1 au groupe de bornes %2").arg(t_label, ts_name);
|
||||
}
|
||||
setText(text);
|
||||
}
|
||||
|
||||
AddTerminalToStripCommand::AddTerminalToStripCommand(QVector<QSharedPointer<RealTerminal>> terminals, TerminalStrip *strip, QUndoCommand *parent) :
|
||||
@@ -50,14 +54,11 @@ AddTerminalToStripCommand::AddTerminalToStripCommand(QVector<QSharedPointer<Real
|
||||
m_new_strip{strip}
|
||||
{
|
||||
const auto ts_name = strip->name();
|
||||
const auto count = m_terminal.size();
|
||||
|
||||
const auto str_1 = m_terminal.size() > 1 ? QObject::tr("Ajouter %1 bornes").arg(m_terminal.size()) :
|
||||
QObject::tr("Ajouter une borne");
|
||||
|
||||
const auto str_2 = ts_name.isEmpty() ? QObject::tr("à un groupe de bornes") :
|
||||
QObject::tr("au groupe de bornes %1").arg(ts_name);
|
||||
|
||||
setText(str_1 % " " % str_2);
|
||||
setText(ts_name.isEmpty()
|
||||
? QObject::tr("Ajouter %n borne(s) à un groupe de bornes", "", count)
|
||||
: QObject::tr("Ajouter %n borne(s) au groupe de bornes %1", "", count).arg(ts_name));
|
||||
}
|
||||
|
||||
|
||||
@@ -137,13 +138,11 @@ void RemoveTerminalFromStripCommand::redo()
|
||||
void RemoveTerminalFromStripCommand::setCommandTitle()
|
||||
{
|
||||
const auto strip_name = m_strip->name();
|
||||
const auto count = m_terminals.size();
|
||||
|
||||
const auto str_1 = m_terminals.size()>1 ? QObject::tr("Enlever %1 bornes").arg(m_terminals.size()):
|
||||
QObject::tr("Enlever une borne");
|
||||
|
||||
const auto str_2 = strip_name.isEmpty() ? QObject::tr("d'un groupe de bornes") :
|
||||
QObject::tr("du groupe de bornes %1").arg(strip_name);
|
||||
setText(str_1 % " " % str_2);
|
||||
setText(strip_name.isEmpty()
|
||||
? QObject::tr("Enlever %n borne(s) d'un groupe de bornes", "", count)
|
||||
: QObject::tr("Enlever %n borne(s) du groupe de bornes %1", "", count).arg(strip_name));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,19 +165,30 @@ MoveTerminalCommand::MoveTerminalCommand(QSharedPointer<PhysicalTerminal> termin
|
||||
t_label.append(", ");
|
||||
t_label.append(real_t->label());
|
||||
}
|
||||
const auto strip_name = old_strip->name();
|
||||
const auto new_strip_name = new_strip->name();
|
||||
|
||||
auto strip_name = old_strip->name();
|
||||
auto new_strip_name = new_strip->name();
|
||||
|
||||
auto str_1 = t_label.isEmpty() ? QObject::tr("Déplacer une borne") :
|
||||
QObject::tr("Déplacer la borne %1").arg(t_label);
|
||||
|
||||
auto str_2 = strip_name.isEmpty() ? QObject::tr(" d'un groupe de bornes") :
|
||||
QObject::tr(" du groupe de bornes %1").arg(strip_name);
|
||||
|
||||
auto str_3 = new_strip_name.isEmpty() ? QObject::tr("vers un groupe de bornes") :
|
||||
QObject::tr("vers le groupe de bornes %1").arg(new_strip_name);
|
||||
setText(str_1 % " " % str_2 % " " % str_3);
|
||||
QString text;
|
||||
if (t_label.isEmpty()) {
|
||||
if (strip_name.isEmpty() && new_strip_name.isEmpty())
|
||||
text = QObject::tr("Déplacer une borne d'un groupe de bornes vers un autre groupe de bornes");
|
||||
else if (strip_name.isEmpty())
|
||||
text = QObject::tr("Déplacer une borne d'un groupe de bornes vers le groupe de bornes %1").arg(new_strip_name);
|
||||
else if (new_strip_name.isEmpty())
|
||||
text = QObject::tr("Déplacer une borne du groupe de bornes %1 vers un autre groupe de bornes").arg(strip_name);
|
||||
else
|
||||
text = QObject::tr("Déplacer une borne du groupe de bornes %1 vers le groupe de bornes %2").arg(strip_name, new_strip_name);
|
||||
} else {
|
||||
if (strip_name.isEmpty() && new_strip_name.isEmpty())
|
||||
text = QObject::tr("Déplacer la borne %1 d'un groupe de bornes vers un autre groupe de bornes").arg(t_label);
|
||||
else if (strip_name.isEmpty())
|
||||
text = QObject::tr("Déplacer la borne %1 d'un groupe de bornes vers le groupe de bornes %2").arg(t_label, new_strip_name);
|
||||
else if (new_strip_name.isEmpty())
|
||||
text = QObject::tr("Déplacer la borne %1 du groupe de bornes %2 vers un autre groupe de bornes").arg(t_label, strip_name);
|
||||
else
|
||||
text = QObject::tr("Déplacer la borne %1 du groupe de bornes %2 vers le groupe de bornes %3").arg(t_label, strip_name, new_strip_name);
|
||||
}
|
||||
setText(text);
|
||||
}
|
||||
|
||||
MoveTerminalCommand::MoveTerminalCommand(QVector<QSharedPointer<PhysicalTerminal>> terminals, TerminalStrip *old_strip,
|
||||
@@ -191,17 +201,18 @@ MoveTerminalCommand::MoveTerminalCommand(QVector<QSharedPointer<PhysicalTerminal
|
||||
{
|
||||
const auto strip_name = old_strip->name();
|
||||
const auto new_strip_name = new_strip->name();
|
||||
const auto count = m_terminal.size();
|
||||
|
||||
const auto str_1 = m_terminal.size() > 1 ? QObject::tr("Déplacer des bornes") :
|
||||
QObject::tr("Déplacer une borne");
|
||||
|
||||
const auto str_2 = strip_name.isEmpty() ? QObject::tr(" d'un groupe de bornes") :
|
||||
QObject::tr(" du groupe de bornes %1").arg(strip_name);
|
||||
|
||||
const auto str_3 = new_strip_name.isEmpty() ? QObject::tr("vers un groupe de bornes") :
|
||||
QObject::tr("vers le groupe de bornes %1").arg(new_strip_name);
|
||||
|
||||
setText(str_1 % " " % str_2 % " " % str_3);
|
||||
QString text;
|
||||
if (strip_name.isEmpty() && new_strip_name.isEmpty())
|
||||
text = QObject::tr("Déplacer %n borne(s) d'un groupe de bornes vers un autre groupe de bornes", "", count);
|
||||
else if (strip_name.isEmpty())
|
||||
text = QObject::tr("Déplacer %n borne(s) d'un groupe de bornes vers le groupe de bornes %1", "", count).arg(new_strip_name);
|
||||
else if (new_strip_name.isEmpty())
|
||||
text = QObject::tr("Déplacer %n borne(s) du groupe de bornes %1 vers un autre groupe de bornes", "", count).arg(strip_name);
|
||||
else
|
||||
text = QObject::tr("Déplacer %n borne(s) du groupe de bornes %1 vers le groupe de bornes %2", "", count).arg(strip_name, new_strip_name);
|
||||
setText(text);
|
||||
}
|
||||
|
||||
void MoveTerminalCommand::undo()
|
||||
|
||||
@@ -275,3 +275,80 @@ QString NumerotationContext::formatValue(const QStringList &item)
|
||||
return QString("%1").arg(value.toInt(), 3, 10, QChar('0'));
|
||||
return QString::number(value.toInt());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NumerotationContext::saveToSettings
|
||||
Save a hash of named NumerotationContexts to QSettings.
|
||||
@param contexts : the named rules to save
|
||||
@param currentRule : the name of the currently active rule
|
||||
@param settings : QSettings instance
|
||||
@param prefix : settings key prefix (e.g. "autonum/conductor")
|
||||
*/
|
||||
void NumerotationContext::saveToSettings(
|
||||
const QHash<QString, NumerotationContext> &contexts,
|
||||
const QString ¤tRule,
|
||||
QSettings &settings,
|
||||
const QString &prefix)
|
||||
{
|
||||
settings.setValue(prefix + "/current", currentRule);
|
||||
|
||||
// Clear stale array entries before writing (beginWriteArray does not
|
||||
// remove entries beyond the new size).
|
||||
settings.remove(prefix + "/rules");
|
||||
|
||||
QStringList names = contexts.keys();
|
||||
settings.beginWriteArray(prefix + "/rules", names.size());
|
||||
for (int i = 0; i < names.size(); ++i) {
|
||||
settings.setArrayIndex(i);
|
||||
const QString &name = names.at(i);
|
||||
const NumerotationContext &nc = contexts.value(name);
|
||||
|
||||
settings.setValue("name", name);
|
||||
|
||||
// Serialize context to XML string
|
||||
QDomDocument doc;
|
||||
NumerotationContext nc_copy = nc;
|
||||
QDomElement root = nc_copy.toXml(doc, "context");
|
||||
doc.appendChild(root);
|
||||
settings.setValue("xml", doc.toString());
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NumerotationContext::loadFromSettings
|
||||
Load named NumerotationContexts from QSettings.
|
||||
@param settings : QSettings instance
|
||||
@param prefix : settings key prefix (e.g. "autonum/conductor")
|
||||
@return pair of (hash of named rules, name of current rule)
|
||||
*/
|
||||
QPair<QHash<QString, NumerotationContext>, QString> NumerotationContext::loadFromSettings(
|
||||
QSettings &settings,
|
||||
const QString &prefix)
|
||||
{
|
||||
QPair<QHash<QString, NumerotationContext>, QString> result;
|
||||
QHash<QString, NumerotationContext> &contexts = result.first;
|
||||
QString ¤tRule = result.second;
|
||||
|
||||
currentRule = settings.value(prefix + "/current").toString();
|
||||
|
||||
int size = settings.beginReadArray(prefix + "/rules");
|
||||
for (int i = 0; i < size; ++i) {
|
||||
settings.setArrayIndex(i);
|
||||
QString name = settings.value("name").toString();
|
||||
QString xmlStr = settings.value("xml").toString();
|
||||
|
||||
if (name.isEmpty() || xmlStr.isEmpty()) continue;
|
||||
|
||||
QDomDocument doc;
|
||||
if (!doc.setContent(xmlStr)) continue;
|
||||
|
||||
QDomElement root = doc.documentElement();
|
||||
NumerotationContext nc;
|
||||
nc.fromXml(root);
|
||||
contexts.insert(name, nc);
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#include <QStringList>
|
||||
#include <QVariant>
|
||||
#include <QDomElement>
|
||||
#include <QHash>
|
||||
#include <QSettings>
|
||||
|
||||
/**
|
||||
This class represents a numerotation context, i.e. the data (type, value, increase)
|
||||
@@ -60,6 +62,14 @@ class NumerotationContext
|
||||
/// UI preview of a part's value matches what actually gets rendered.
|
||||
static QString formatValue(const QStringList &item);
|
||||
|
||||
static void saveToSettings(const QHash<QString, NumerotationContext> &contexts,
|
||||
const QString ¤tRule,
|
||||
QSettings &settings,
|
||||
const QString &prefix);
|
||||
static QPair<QHash<QString, NumerotationContext>, QString> loadFromSettings(
|
||||
QSettings &settings,
|
||||
const QString &prefix);
|
||||
|
||||
private:
|
||||
QStringList content_;
|
||||
};
|
||||
|
||||
@@ -505,7 +505,10 @@ void BorderTitleBlock::draw(QPainter *painter)
|
||||
{
|
||||
//Set the QPainter
|
||||
painter -> save();
|
||||
QPen pen(Qt::black);
|
||||
//Use a pen color that contrasts with the background
|
||||
QColor border_color = Diagram::background_color.lightness() < 128
|
||||
? QColor(Qt::white) : QColor(Qt::black);
|
||||
QPen pen(border_color);
|
||||
painter -> setPen(pen);
|
||||
painter -> setBrush(Qt::NoBrush);
|
||||
|
||||
@@ -893,10 +896,20 @@ void BorderTitleBlock::updateDiagramContextForTitleBlock(
|
||||
// An empty page-level value means the variable was auto-added to the
|
||||
// folio's Custom tab (#495) but never actually set by the user, so it
|
||||
// must not shadow a real project-level value of the same name (#531).
|
||||
//
|
||||
// That guard has to stop short of removing the key outright, though
|
||||
// (#973). TitleBlockTemplate::interpreteVariables() only replaces a
|
||||
// "%name"/"%{name}" placeholder when "name" is a key in this context at
|
||||
// all -- an unset variable that never makes it in is left as its own
|
||||
// literal placeholder text in the rendered title block, not blank.
|
||||
// So an empty page-level value is skipped only when a real project-level
|
||||
// one is already there to show through; otherwise it still goes in
|
||||
// empty, which is what makes the placeholder resolve to nothing.
|
||||
DiagramContext context = initial_context;
|
||||
foreach (QString key, additional_fields_.keys()) {
|
||||
if (!additional_fields_[key].toString().isEmpty())
|
||||
context.addValue(key, additional_fields_[key]);
|
||||
const QVariant value = additional_fields_[key];
|
||||
if (!value.toString().isEmpty() || !context.contains(key))
|
||||
context.addValue(key, value);
|
||||
}
|
||||
|
||||
// ... overridden by the historical and/or dynamically generated fields
|
||||
|
||||
@@ -499,6 +499,7 @@ void ConductorProperties::applyForEqualAttributes(QList<ConductorProperties> lis
|
||||
horiz_rotate_text = cp.horiz_rotate_text;
|
||||
m_vertical_alignment = cp.m_vertical_alignment;
|
||||
m_horizontal_alignment = cp.m_horizontal_alignment;
|
||||
style = cp.style;
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -555,6 +556,18 @@ void ConductorProperties::applyForEqualAttributes(QList<ConductorProperties> lis
|
||||
m_dash_size = i_value;
|
||||
equal = true;
|
||||
|
||||
//style
|
||||
Qt::PenStyle pen_style;
|
||||
pen_style = clist.first().style;
|
||||
for(ConductorProperties cp : clist)
|
||||
{
|
||||
if (cp.style != pen_style)
|
||||
equal = false;
|
||||
}
|
||||
if (equal)
|
||||
style = pen_style;
|
||||
equal = true;
|
||||
|
||||
//text
|
||||
s_value = clist.first().text;
|
||||
for(ConductorProperties cp : clist)
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
#include "projectdatabase.h"
|
||||
|
||||
#include "sqlreadonly.h"
|
||||
|
||||
#include "../diagram.h"
|
||||
#include "../diagramposition.h"
|
||||
#include "../elementprovider.h"
|
||||
@@ -28,13 +30,15 @@
|
||||
#include "../qetproject.h"
|
||||
|
||||
#include <QLocale>
|
||||
#include <QFile>
|
||||
#include <QRegularExpression>
|
||||
#include <QSqlError>
|
||||
|
||||
#include <QSqlDriver>
|
||||
#include <QSqlError>
|
||||
#include <sqlite3.h>
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
@brief projectDataBase::projectDataBase
|
||||
Default constructor
|
||||
@@ -208,6 +212,12 @@ bool projectDataBase::isReadOnlySelect(const QString &query, QString *error)
|
||||
*/
|
||||
QSqlQuery projectDataBase::newQuery(const QString &query, QString *error) {
|
||||
QString reason;
|
||||
|
||||
// First gate: which kind of statement is acceptable here at all. A
|
||||
// textual check is the right tool for that and the wrong tool for
|
||||
// anything else -- see isReadOnlySelect()'s own comment. It is what
|
||||
// keeps ATTACH, BEGIN and PRAGMA out, none of which SQLite itself
|
||||
// considers writes.
|
||||
if (!isReadOnlySelect(query, &reason)) {
|
||||
qWarning().noquote() << "projectDataBase::newQuery: rejected query:" << reason << "--" << query;
|
||||
if (error) {
|
||||
@@ -215,6 +225,24 @@ QSqlQuery projectDataBase::newQuery(const QString &query, QString *error) {
|
||||
}
|
||||
return QSqlQuery(m_data_base);
|
||||
}
|
||||
|
||||
// Second gate, and the one that actually enforces read-only: SQLite is
|
||||
// asked about the statement it compiled, instead of the text being read
|
||||
// for clues. The first gate cannot see through a CTE prefix --
|
||||
// "WITH x AS (SELECT 1) DELETE FROM element" starts with WITH, contains
|
||||
// no semicolon, and deletes every row. That matters beyond the
|
||||
// custom-query box, because this path is reachable from a file: a
|
||||
// <graphics_table>'s saved <query> is read straight out of the .qet by
|
||||
// ProjectDBModel::fromXml() and executed by fillValue(), so opening or
|
||||
// exporting a project someone else produced would have been enough.
|
||||
if (!QETSql::isSingleReadOnlyStatement(sqliteHandle(&m_data_base), query, &reason)) {
|
||||
qWarning().noquote() << "projectDataBase::newQuery: rejected query:" << reason << "--" << query;
|
||||
if (error) {
|
||||
*error = reason;
|
||||
}
|
||||
return QSqlQuery(m_data_base);
|
||||
}
|
||||
|
||||
return QSqlQuery(query, m_data_base);
|
||||
}
|
||||
|
||||
@@ -1245,7 +1273,6 @@ void projectDataBase::bindDiagramInfoValues(QSqlQuery &query, Diagram *diagram)
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef QET_EXPORT_PROJECT_DB
|
||||
/**
|
||||
@brief projectDataBase::sqliteHandle
|
||||
@param db
|
||||
@@ -1263,6 +1290,7 @@ sqlite3 *projectDataBase::sqliteHandle(QSqlDatabase *db)
|
||||
return handle;
|
||||
}
|
||||
|
||||
#ifdef QET_EXPORT_PROJECT_DB
|
||||
|
||||
/**
|
||||
* @brief projectDataBase::exportDb
|
||||
@@ -1298,27 +1326,19 @@ void projectDataBase::exportDb(projectDataBase *db,
|
||||
return;
|
||||
}
|
||||
|
||||
QString connection_name("export_project_db_" % db->project()->uuid().toString());
|
||||
|
||||
if (true) //Enter in a scope only to nicely use QSqlDatabase::removeDatabase just after the end of the scope
|
||||
{
|
||||
auto file_db = QSqlDatabase::addDatabase("QSQLITE", connection_name);
|
||||
file_db.setDatabaseName(path_);
|
||||
if (!file_db.open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto memory_db_handle = sqliteHandle(&db->m_data_base);
|
||||
auto file_db_handle = sqliteHandle(&file_db);
|
||||
|
||||
auto sqlite_backup = sqlite3_backup_init(file_db_handle, "main", memory_db_handle, "main");
|
||||
if (sqlite_backup)
|
||||
{
|
||||
sqlite3_backup_step(sqlite_backup, -1);
|
||||
sqlite3_backup_finish(sqlite_backup);
|
||||
}
|
||||
file_db.close();
|
||||
// VACUUM INTO requires the destination not to exist. QFileDialog may ask
|
||||
// about overwriting, but it does not remove the existing file for us.
|
||||
if (QFile::exists(path_) && !QFile::remove(path_)) {
|
||||
qWarning() << "Unable to replace project database export:" << path_;
|
||||
return;
|
||||
}
|
||||
|
||||
// VACUUM INTO creates a standalone copy of the current database without
|
||||
// requiring access to the SQLite driver's native connection handle.
|
||||
const auto escaped_path = path_.replace("'", "''");
|
||||
QSqlQuery query(db->m_data_base);
|
||||
if (!query.exec("VACUUM INTO '" % escaped_path % "'")) {
|
||||
qWarning() << "Unable to export project database:" << query.lastError().text();
|
||||
}
|
||||
QSqlDatabase::removeDatabase(connection_name);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -61,6 +61,26 @@ class projectDataBase : public QObject
|
||||
QETProject *project() const;
|
||||
QSqlQuery newQuery(const QString &query = QString(), QString *error = nullptr);
|
||||
static bool isReadOnlySelect(const QString &query, QString *error = nullptr);
|
||||
|
||||
/**
|
||||
The most rows any caller reads out of one query result.
|
||||
|
||||
A SELECT is not bounded by how much data the project holds:
|
||||
SQLite produces rows lazily, so a query that never stops
|
||||
producing them makes the loop that reads them never stop
|
||||
either. A recursive CTE does exactly that in one line, and
|
||||
a <graphics_table>'s <query> is stored in the .qet and run
|
||||
on load -- so the text can arrive from a file rather than
|
||||
from the person at the keyboard, and opening that file is
|
||||
the whole attack.
|
||||
|
||||
100000 is far above any real result: the largest table in
|
||||
the shipped examples is 396 rows. It is a backstop, not a
|
||||
page size -- a caller that hits it has almost certainly
|
||||
been handed something it should not run to completion, and
|
||||
says so rather than truncating quietly.
|
||||
*/
|
||||
static constexpr int MaxResultRows = 100000;
|
||||
QSqlDatabase database() const {return m_data_base;}
|
||||
int excludedConductorCount() const;
|
||||
|
||||
@@ -135,9 +155,14 @@ class projectDataBase : public QObject
|
||||
m_cascade_remove_conductor_query,
|
||||
m_cascade_remove_element_query;
|
||||
|
||||
public:
|
||||
// Deliberately outside the QET_EXPORT_PROJECT_DB guard below:
|
||||
// newQuery() needs the raw connection to ask SQLite whether a
|
||||
// query only reads, and that check runs in every build.
|
||||
static sqlite3 *sqliteHandle(QSqlDatabase *db);
|
||||
|
||||
#ifdef QET_EXPORT_PROJECT_DB
|
||||
public:
|
||||
static sqlite3 *sqliteHandle(QSqlDatabase *db);
|
||||
static void exportDb(projectDataBase *db,
|
||||
QWidget *parent = nullptr,
|
||||
const QString &caption = QString(),
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
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 "sqlreadonly.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
namespace QETSql {
|
||||
|
||||
/**
|
||||
@brief QETSql::isSingleReadOnlyStatement
|
||||
Ask SQLite itself whether @p query is exactly one statement, and whether
|
||||
that statement only reads.
|
||||
|
||||
Why SQLite is asked rather than the text inspected: a check on the
|
||||
query's first keyword cannot see what the statement actually does.
|
||||
SQLite has allowed a CTE prefix in front of a data-modifying statement
|
||||
since 3.8.3, so
|
||||
|
||||
@code
|
||||
WITH x AS (SELECT 1) DELETE FROM element
|
||||
@endcode
|
||||
|
||||
begins with WITH, contains no semicolon, and deletes every row.
|
||||
sqlite3_stmt_readonly() reports on the statement SQLite compiled, not
|
||||
on how it was spelled, so the same query is correctly refused here
|
||||
while an ordinary WITH ... SELECT still passes.
|
||||
|
||||
The statement is compiled and immediately finalised; sqlite3_prepare_v2()
|
||||
does not run it, so nothing is executed to reach this verdict.
|
||||
|
||||
This is a read-only test, NOT a statement-type allowlist: SQLite
|
||||
considers ATTACH, BEGIN and several PRAGMAs read-only too, because none
|
||||
of them change the contents of the database. Callers that need to
|
||||
restrict which *kind* of statement is acceptable must say so separately
|
||||
-- projectDataBase::newQuery() keeps isReadOnlySelect() in front of this
|
||||
for exactly that reason.
|
||||
|
||||
@param handle the connection the query would run on. A null handle is
|
||||
refused rather than waved through: without it there is nothing to ask,
|
||||
and guessing from the text is the weakness this exists to replace.
|
||||
@param query the raw SQL text
|
||||
@param error set to a human-readable reason when this returns false
|
||||
@return true if @p query is a single, read-only statement
|
||||
*/
|
||||
bool isSingleReadOnlyStatement(sqlite3 *handle, const QString &query, QString *error)
|
||||
{
|
||||
if (error) {
|
||||
error->clear();
|
||||
}
|
||||
|
||||
if (!handle) {
|
||||
if (error) {
|
||||
*error = QCoreApplication::translate("QETSql",
|
||||
"Impossible de vérifier la requête : "
|
||||
"aucune connexion SQLite disponible.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray utf8 = query.toUtf8();
|
||||
sqlite3_stmt *statement = nullptr;
|
||||
const char *tail = nullptr;
|
||||
|
||||
if (sqlite3_prepare_v2(handle, utf8.constData(), utf8.size(),
|
||||
&statement, &tail) != SQLITE_OK)
|
||||
{
|
||||
if (error) {
|
||||
*error = QCoreApplication::translate("QETSql",
|
||||
"Requête SQL invalide : %1")
|
||||
.arg(QString::fromUtf8(sqlite3_errmsg(handle)));
|
||||
}
|
||||
sqlite3_finalize(statement);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whitespace or a bare comment compiles successfully to no statement
|
||||
// at all, and sqlite3_stmt_readonly() must not be handed that.
|
||||
if (!statement) {
|
||||
if (error) {
|
||||
*error = QCoreApplication::translate("QETSql",
|
||||
"La requête ne contient aucune instruction.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool read_only = sqlite3_stmt_readonly(statement) != 0;
|
||||
sqlite3_finalize(statement);
|
||||
|
||||
if (!read_only) {
|
||||
if (error) {
|
||||
*error = QCoreApplication::translate("QETSql",
|
||||
"Seules les requêtes en lecture seule sont autorisées : "
|
||||
"cette requête modifierait la base de données.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// tail points just past the first statement, semicolon included.
|
||||
// Anything left once semicolons and spacing are stripped is a second
|
||||
// statement -- caught structurally here, where "SELECT ';'" is a
|
||||
// perfectly ordinary query rather than a suspicious string.
|
||||
if (tail) {
|
||||
QString rest = QString::fromUtf8(tail);
|
||||
rest.remove(QLatin1Char(';'));
|
||||
if (!rest.trimmed().isEmpty()) {
|
||||
if (error) {
|
||||
*error = QCoreApplication::translate("QETSql",
|
||||
"Une seule requête est autorisée.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace QETSql
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
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 SQLREADONLY_H
|
||||
#define SQLREADONLY_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
struct sqlite3;
|
||||
|
||||
/**
|
||||
Deciding whether a piece of SQL only reads.
|
||||
|
||||
Deliberately its own translation unit, depending on nothing but QString
|
||||
and SQLite: it is the enforcement point for every query QElectroTech
|
||||
runs against a project database, including queries that arrive from
|
||||
outside the application (a .qet file's saved report/table query), so it
|
||||
is worth being able to test it in isolation -- see
|
||||
tests/qttest/tst_sqlreadonly.cpp, which links this file and nothing
|
||||
else of QElectroTech.
|
||||
*/
|
||||
namespace QETSql {
|
||||
|
||||
bool isSingleReadOnlyStatement(sqlite3 *handle,
|
||||
const QString &query,
|
||||
QString *error = nullptr);
|
||||
}
|
||||
|
||||
#endif // SQLREADONLY_H
|
||||
+10
-7
@@ -26,6 +26,7 @@
|
||||
#include "diagramposition.h"
|
||||
#include "factory/elementfactory.h"
|
||||
#include "qetapp.h"
|
||||
#include "qetpalette.h"
|
||||
#include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
|
||||
#include "qetgraphicsitem/conductor.h"
|
||||
#include "qetgraphicsitem/conductortextitem.h"
|
||||
@@ -283,10 +284,11 @@ void Diagram::drawBackground(QPainter *p, const QRectF &r) {
|
||||
* if background color is black,
|
||||
* then grid spots shall be white,
|
||||
* else they shall be black in color.
|
||||
* A view that shows the sheet with its lightness inverted
|
||||
* gets softer dots, see QET::Palette::gridDotColor.
|
||||
*/
|
||||
QPen pen;
|
||||
Diagram::background_color == Qt::black? pen.setColor(Qt::white)
|
||||
: pen.setColor(Qt::black);
|
||||
pen.setColor(QET::Palette::gridDotColor(Diagram::background_color, m_inverted_lightness));
|
||||
pen.setCosmetic(true);
|
||||
p->setPen(pen);
|
||||
|
||||
@@ -1707,10 +1709,7 @@ bool Diagram::fromXml(QDomElement &document,
|
||||
//Get the top left corner of the rectangle that contain all added items
|
||||
QRectF items_rect;
|
||||
for (auto item : added_items) {
|
||||
items_rect = items_rect.united(
|
||||
item->mapToScene(
|
||||
item->boundingRect()
|
||||
).boundingRect());
|
||||
items_rect = items_rect.united(item->mapToScene(item->boundingRect()).boundingRect());
|
||||
}
|
||||
|
||||
QPointF point_ = items_rect.topLeft();
|
||||
@@ -1718,8 +1717,12 @@ bool Diagram::fromXml(QDomElement &document,
|
||||
position.y() - point_.y()));
|
||||
|
||||
//Translate all added items
|
||||
for (auto qgi : added_items)
|
||||
for (auto qgi : added_items) {
|
||||
qgi->setPos(qgi->pos() += pos_);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
}
|
||||
|
||||
// Load conductor
|
||||
|
||||
@@ -123,6 +123,7 @@ class Diagram : public QGraphicsScene
|
||||
qreal diagram_qet_version_;
|
||||
|
||||
bool draw_grid_;
|
||||
bool m_inverted_lightness = false;
|
||||
bool use_border_;
|
||||
bool draw_guides_;
|
||||
QList<Diagram::Guide> m_guides_list;
|
||||
@@ -222,6 +223,7 @@ class Diagram : public QGraphicsScene
|
||||
ExportProperties applyProperties(const ExportProperties &);
|
||||
void setDisplayGrid(bool);
|
||||
bool displayGrid();
|
||||
void setInvertedLightness(bool);
|
||||
void setDisplayGuides(bool);
|
||||
bool displayGuides();
|
||||
void updateProjectGuides(const QList<GuideProperties> &guides);
|
||||
@@ -355,6 +357,18 @@ inline void Diagram::setDisplayGrid(bool dg) {
|
||||
draw_grid_ = dg;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Diagram::setInvertedLightness
|
||||
Tell the diagram whether the view painting it will show the result
|
||||
with its lightness inverted (PaletteGraphicsView on a dark palette).
|
||||
drawBackground draws a softer grid in that case. Printing
|
||||
and export never set this.
|
||||
@param inverted
|
||||
*/
|
||||
inline void Diagram::setInvertedLightness(bool inverted) {
|
||||
m_inverted_lightness = inverted;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Diagram::displayGrid
|
||||
@return draw_grid_ true if the grid is drawn, false otherwise.
|
||||
|
||||
@@ -151,8 +151,8 @@ bool DiagramContext::operator!=(const DiagramContext &dc) const
|
||||
void DiagramContext::toXml(QDomElement &e, const QString &tag_name) const
|
||||
{
|
||||
foreach (QString key, keys()) {
|
||||
if ((tag_name == "elementInformation") &&
|
||||
(m_content[key].toString().trimmed().isEmpty())) {
|
||||
const QString raw = m_content[key].toString();
|
||||
if ((tag_name == "elementInformation") && raw.trimmed().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
QDomElement property = e.ownerDocument().createElement(tag_name);
|
||||
@@ -161,7 +161,15 @@ void DiagramContext::toXml(QDomElement &e, const QString &tag_name) const
|
||||
property.removeAttribute("name");
|
||||
property.setAttribute("show", m_content_show[key]);
|
||||
property.setAttribute("name", key);
|
||||
QDomText value = e.ownerDocument().createTextNode(m_content[key].toString().trimmed());
|
||||
// Trim stray leading/trailing whitespace around real content, but
|
||||
// not a value that IS whitespace: unconditionally trimming an
|
||||
// all-whitespace string collapses it to "", which is silently
|
||||
// indistinguishable from a value that was never set. A title-block
|
||||
// custom variable set to a single space -- a workaround for #973,
|
||||
// where an unset variable renders as its own literal placeholder --
|
||||
// would otherwise vanish on the very next save.
|
||||
const QString stored = raw.trimmed().isEmpty() ? raw : raw.trimmed();
|
||||
QDomText value = e.ownerDocument().createTextNode(stored);
|
||||
property.appendChild(value);
|
||||
e.appendChild(property);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
#ifndef DIAGRAM_CONTEXT_H
|
||||
#define DIAGRAM_CONTEXT_H
|
||||
#include "pugixml/src/pugixml.hpp"
|
||||
#include "pugixml.hpp"
|
||||
|
||||
#include <QDomElement>
|
||||
#include <QHash>
|
||||
|
||||
@@ -67,7 +67,19 @@ m_preview_item(nullptr)
|
||||
dummy_diagram->setDisplayGrid(false);
|
||||
dummy_diagram->fromXml(diagram_node, QPointF(0, 0), false, nullptr);
|
||||
|
||||
// Compute bounding rect of TOP-LEVEL items only (matching fromXml's added_items logic)
|
||||
// Child items (DynamicElementTextItem, Terminal) are NOT included - they move with parents
|
||||
QRectF top_level_rect;
|
||||
for (auto *item : dummy_diagram->items()) {
|
||||
if (!item->parentItem()) {
|
||||
top_level_rect = top_level_rect.united(
|
||||
item->mapToScene(item->boundingRect()).boundingRect());
|
||||
}
|
||||
}
|
||||
m_items_top_left = top_level_rect.topLeft();
|
||||
|
||||
QRectF scene_rect = dummy_diagram->itemsBoundingRect();
|
||||
|
||||
if (!scene_rect.isEmpty()) {
|
||||
QPixmap pixmap(scene_rect.toAlignedRect().size());
|
||||
pixmap.fill(Qt::transparent);
|
||||
@@ -80,10 +92,11 @@ m_preview_item(nullptr)
|
||||
}
|
||||
}
|
||||
|
||||
if (m_preview_item) {
|
||||
m_preview_item->setPos(Diagram::snapToGrid(pos));
|
||||
m_preview_item->setOpacity(0.6);
|
||||
m_diagram->addItem(m_preview_item);
|
||||
if (m_preview_item) {
|
||||
QPointF snapped = Diagram::snapToGrid(pos);
|
||||
m_preview_item->setPos(snapped);
|
||||
m_preview_item->setOpacity(0.6);
|
||||
m_diagram->addItem(m_preview_item);
|
||||
m_running = true;
|
||||
}
|
||||
|
||||
@@ -117,6 +130,7 @@ void DiagramEventAddMacro::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (m_preview_item) {
|
||||
const auto pos_{Diagram::snapToGrid(event->scenePos())};
|
||||
|
||||
m_preview_item->setPos(pos_);
|
||||
|
||||
if (m_status_bar) {
|
||||
@@ -141,7 +155,8 @@ void DiagramEventAddMacro::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||
emit finish();
|
||||
}
|
||||
else if (event->button() == Qt::LeftButton) {
|
||||
addMacro(Diagram::snapToGrid(event->scenePos()));
|
||||
QPointF snapped = Diagram::snapToGrid(event->scenePos());
|
||||
addMacro(snapped);
|
||||
}
|
||||
}
|
||||
event->setAccepted(true);
|
||||
@@ -238,10 +253,9 @@ void DiagramEventAddMacro::addMacro(QPointF final_pos)
|
||||
|
||||
if (!diagram_node.isNull()) {
|
||||
QDomElement cloned_node = diagram_node.cloneNode(true).toElement();
|
||||
|
||||
DiagramContent pasted_content;
|
||||
|
||||
m_diagram->fromXml(cloned_node, final_pos, false, &pasted_content);
|
||||
m_diagram->fromXml(cloned_node, final_pos + m_items_top_left, false, &pasted_content);
|
||||
m_diagram->refreshContents();
|
||||
|
||||
// Prevent PasteDiagramCommand from erasing labels (BMK)
|
||||
|
||||
@@ -40,6 +40,7 @@ private:
|
||||
QDomDocument m_macro_doc;
|
||||
QGraphicsPixmapItem *m_preview_item;
|
||||
QPointer<QStatusBar> m_status_bar;
|
||||
QPointF m_items_top_left; // top-left of bounding rect of top-level items in the macro (for correct placement offset)
|
||||
};
|
||||
|
||||
#endif // DIAGRAMEVENTADDMACRO_H
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "../qetapp.h"
|
||||
#include "../qetdiagrameditor.h"
|
||||
#include "../qetgraphicsitem/conductor.h"
|
||||
#include "../qetproject.h"
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
@@ -40,6 +41,7 @@
|
||||
DiagramEventAddPaste::DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos) :
|
||||
DiagramEventInterface(diagram)
|
||||
{
|
||||
Q_UNUSED(start_pos); // items stay at their original XML position
|
||||
//DiagramEventInterface::init() is called by Diagram::setEventInterface
|
||||
//only when it is replacing an earlier interface, so call it here as
|
||||
//DiagramEventAddMacro does.
|
||||
@@ -51,8 +53,25 @@
|
||||
QDomDocument document_xml;
|
||||
if (!document_xml.setContent(clipboard_text)) return;
|
||||
|
||||
//Batch the database work the same way project loading does
|
||||
//(QETProject::readProjectXml): without this, every addItem()
|
||||
//below emits dataBaseUpdated(), which makes each connected
|
||||
//table model re-run its full SQL query -- ~77 queries for a
|
||||
//typical paste, i.e. the multi-second stall on Ctrl+V.
|
||||
auto *db = m_diagram->project() ? m_diagram->project()->dataBase() : nullptr;
|
||||
if (db) {
|
||||
db->blockSignals(true);
|
||||
db->setUpdateBlocked(true);
|
||||
}
|
||||
|
||||
//Load items at their original XML coordinates.
|
||||
m_diagram->fromXml(document_xml, QPointF(), false, &m_content);
|
||||
|
||||
if (db) {
|
||||
db->blockSignals(false);
|
||||
db->setUpdateBlocked(false);
|
||||
db->updateDB();
|
||||
}
|
||||
if (!m_content.count()) return;
|
||||
|
||||
const QList<QGraphicsItem *> movable = m_content.items(MovableItems);
|
||||
@@ -85,33 +104,23 @@
|
||||
};
|
||||
const QPointF grid_origin = snapGrid(top_left);
|
||||
|
||||
//Move the group to the cursor, rather than the cursor to the
|
||||
//group. Both put the copy under the pointer, but warping the
|
||||
//pointer also drags it back to the original's position, so the
|
||||
//copy appears exactly on top of what was copied until the mouse
|
||||
//is moved -- which is the thing pasting under the cursor was
|
||||
//meant to avoid (issue #913). Taking the pointer away from
|
||||
//where the user put it is also its own surprise.
|
||||
m_group_origin = snapGrid(start_pos);
|
||||
const QPointF offset = m_group_origin - grid_origin;
|
||||
|
||||
//Store each item's position after the move. moveTo() applies a
|
||||
//Store each item's original position. moveTo() applies a
|
||||
//grid-snapped delta from the baseline to these, so items
|
||||
//preserve their layout and move in whole grid steps.
|
||||
for (auto *item : movable) {
|
||||
item->setPos(item->pos() + offset);
|
||||
m_relative_pos.insert(item, item->pos());
|
||||
}
|
||||
m_group_origin = grid_origin;
|
||||
|
||||
//The conductors were laid out against the old terminal
|
||||
//The conductors were laid out against the original terminal
|
||||
//positions, so re-route them before anything is drawn.
|
||||
const QList<Conductor *> conductors = m_content.conductors(DiagramContent::AnyConductor);
|
||||
for (auto *conductor : conductors) {
|
||||
conductor->updatePath();
|
||||
}
|
||||
|
||||
//The baseline is known now, so moveTo() does not have to
|
||||
//capture one from the first mouse movement.
|
||||
//The baseline is the group's grid-snapped origin, so moveTo()
|
||||
//does not have to capture one from the first mouse movement.
|
||||
m_initial_cursor = m_group_origin;
|
||||
m_baseline_captured = true;
|
||||
|
||||
@@ -125,6 +134,14 @@
|
||||
if (const auto qde = QETApp::diagramEditorAncestorOf(view)) {
|
||||
m_status_bar = qde->statusBar();
|
||||
}
|
||||
//Warp the cursor to the group's grid-snapped origin so
|
||||
//the actual cursor position matches m_initial_cursor.
|
||||
//Without this the first mouseMoveEvent computes a large
|
||||
//delta (cursor is still at the Ctrl+V press location)
|
||||
//and the items jump on first touch.
|
||||
const QPoint view_pos = view->mapFromScene(m_initial_cursor);
|
||||
const QPoint global_pos = view->viewport()->mapToGlobal(view_pos);
|
||||
QCursor::setPos(global_pos);
|
||||
}
|
||||
}
|
||||
showHint();
|
||||
|
||||
@@ -29,7 +29,14 @@
|
||||
*/
|
||||
DiagramEventAddText::DiagramEventAddText(Diagram *diagram) :
|
||||
DiagramEventInterface(diagram)
|
||||
{}
|
||||
{
|
||||
//The tool is armed from the moment it is attached: the next left
|
||||
//click places a text. DiagramView::keyPressEvent() asks
|
||||
//Diagram::eventInterfaceIsRunning() -- which is isRunning(), i.e.
|
||||
//m_running -- before letting Escape through to the tool, so without
|
||||
//this the view swallows Escape and the tool cannot be cancelled.
|
||||
m_running = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddText::~DiagramEventAddText
|
||||
@@ -52,6 +59,8 @@ void DiagramEventAddText::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
event->scenePos()));
|
||||
text->setTextInteractionFlags(Qt::TextEditorInteraction);
|
||||
text->setFocus(Qt::MouseFocusReason);
|
||||
//Placed: the tool is done before it announces it.
|
||||
m_running = false;
|
||||
emit finish();
|
||||
event->setAccepted(true);
|
||||
}
|
||||
|
||||
+90
-2
@@ -39,7 +39,9 @@
|
||||
#include "ElementsCollection/xmlelementcollection.h"
|
||||
#include "NameList/nameslist.h"
|
||||
#include "elementdialog.h"
|
||||
#include <QApplication>
|
||||
#include <QDropEvent>
|
||||
#include <QPainter>
|
||||
#include <QPointer>
|
||||
|
||||
/**
|
||||
@@ -48,7 +50,7 @@
|
||||
@param parent Le QWidget parent de cette vue de schema
|
||||
*/
|
||||
DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
|
||||
QGraphicsView (parent),
|
||||
PaletteGraphicsView (parent),
|
||||
m_diagram (diagram)
|
||||
{
|
||||
grabGesture(Qt::PinchGesture);
|
||||
@@ -443,6 +445,82 @@ void DiagramView::pasteHere()
|
||||
paste(mapToScene(m_paste_here_pos));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramView::duplicate
|
||||
Copy the current selection and place the copy at @p stepOffset grid
|
||||
steps from it, landing immediately rather than following the cursor
|
||||
like Ctrl+V does (bugtracker #991). @p stepOffset comes from
|
||||
DuplicateOffsetDialog: (1, 0) is one grid step right, (0, -1) is one
|
||||
grid step up, and so on -- QET's own scene axes, X right and Y down.
|
||||
|
||||
No interactive placement step on purpose: the point of a duplicate
|
||||
shortcut is unattended, repeatable stamping (configure the offset
|
||||
once, then tap Ctrl+D to lay out a row), which following the cursor
|
||||
would interrupt on every press. QET already reselects whatever a
|
||||
paste just added (see PasteDiagramCommand::redo()), so the next
|
||||
Ctrl+D naturally continues from the copy just placed, not the
|
||||
original -- a press-and-hold row falls out of that for free, with no
|
||||
special-casing needed here for "keep going from the last one".
|
||||
|
||||
The offset is applied by hand rather than by asking paste()/
|
||||
Diagram::fromXml() to place the copy at a target position. Both of
|
||||
those feed the position through Diagram::snapToGrid(), which reads
|
||||
QApplication::keyboardModifiers() and rounds to the nearest PIXEL
|
||||
instead of the grid whenever Ctrl is held -- and Ctrl is always held
|
||||
here, this action's own shortcut being Ctrl+D. Measured the hard way
|
||||
before settling on this: routing the offset through paste() first
|
||||
produced copies off-grid on both axes, by an amount that tracked the
|
||||
selection's own bounding-box geometry rather than being a fixed
|
||||
error. fromXml() is instead called with no position at all, which
|
||||
leaves every item at its source coordinates (landing the copy
|
||||
exactly on top of the originals -- (0, 0) is not a position, this is
|
||||
"keep the source coordinates"), and the offset is added directly
|
||||
with setPos(). A plain addition cannot be off by a rounding rule
|
||||
that never runs.
|
||||
|
||||
Conductors are not in the translated set: fromXml() itself does not
|
||||
reposition them either -- they are loaded from XML after elements
|
||||
are already in their final place and take their geometry from their
|
||||
terminals, which have already moved with the elements that own
|
||||
them. Likewise dynamic element texts are not translated separately:
|
||||
they are children of their element and move with it under Qt's
|
||||
normal parent-child transform.
|
||||
*/
|
||||
void DiagramView::duplicate(const QPoint &stepOffset)
|
||||
{
|
||||
if (!isInteractive() || m_diagram->isReadOnly()) return;
|
||||
|
||||
const QList<QGraphicsItem *> selection = m_diagram->selectedItems();
|
||||
if (selection.isEmpty()) return;
|
||||
|
||||
QSettings settings;
|
||||
const int x_grid = settings.value(QStringLiteral("diagrameditor/Xgrid"),
|
||||
Diagram::xGrid).toInt();
|
||||
const int y_grid = settings.value(QStringLiteral("diagrameditor/Ygrid"),
|
||||
Diagram::yGrid).toInt();
|
||||
const QPointF offset(stepOffset.x() * x_grid, stepOffset.y() * y_grid);
|
||||
|
||||
// Mirrors copy(), but does not touch the system clipboard: Ctrl+D
|
||||
// should not clobber whatever the user last copied with Ctrl+C.
|
||||
QDomDocument document = m_diagram->toXml(false, true);
|
||||
|
||||
DiagramContent pasted;
|
||||
// No position argument -- see the function comment above for why
|
||||
// the offset is not passed here.
|
||||
m_diagram->fromXml(document, QPointF(), false, &pasted);
|
||||
if (!pasted.count()) return;
|
||||
|
||||
const int movable = DiagramContent::Elements | DiagramContent::TextFields
|
||||
| DiagramContent::Images | DiagramContent::Shapes
|
||||
| DiagramContent::Tables | DiagramContent::TerminalStrip;
|
||||
for (QGraphicsItem *item : pasted.items(movable))
|
||||
item->setPos(item->pos() + offset);
|
||||
|
||||
m_diagram->clearSelection();
|
||||
m_diagram->undoStack().push(new PasteDiagramCommand(m_diagram, pasted));
|
||||
adjustSceneRect();
|
||||
}
|
||||
|
||||
/**
|
||||
Manage the events press click :
|
||||
* click to add an independent text field
|
||||
@@ -1081,6 +1159,16 @@ bool DiagramView::event(QEvent *e) {
|
||||
return(QGraphicsView::event(e));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramView::paintingInverted
|
||||
Reimplemented from PaletteGraphicsView: tell the diagram it is being
|
||||
drawn for an inverted display, so it softens its grid.
|
||||
*/
|
||||
void DiagramView::paintingInverted(bool inverted)
|
||||
{
|
||||
m_diagram->setInvertedLightness(inverted);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramView::paintEvent
|
||||
Reimplemented from QGraphicsView
|
||||
@@ -1088,7 +1176,7 @@ bool DiagramView::event(QEvent *e) {
|
||||
*/
|
||||
void DiagramView::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
QGraphicsView::paintEvent(event);
|
||||
PaletteGraphicsView::paintEvent(event);
|
||||
|
||||
if (m_free_rubberbanding && m_free_rubberband.count() >= 3)
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#include "titleblock/templatelocation.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QGraphicsView>
|
||||
#include "palettegraphicsview.h"
|
||||
|
||||
class Conductor;
|
||||
class Diagram;
|
||||
@@ -35,7 +35,7 @@ class QGestureEvent;
|
||||
This class provides a widget to render an electric diagram in an editable,
|
||||
interactive way.
|
||||
*/
|
||||
class DiagramView : public QGraphicsView
|
||||
class DiagramView : public PaletteGraphicsView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@@ -84,6 +84,7 @@ class DiagramView : public QGraphicsView
|
||||
///Set for one call only, by the Escape handler, to let focus leave the view.
|
||||
bool m_releasing_focus = false;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void paintingInverted(bool inverted) override;
|
||||
void mousePressEvent(QMouseEvent *) override;
|
||||
void mouseMoveEvent(QMouseEvent *) override;
|
||||
void mouseReleaseEvent(QMouseEvent *) override;
|
||||
@@ -136,6 +137,7 @@ class DiagramView : public QGraphicsView
|
||||
void copy();
|
||||
void paste(const QPointF & = QPointF(), QClipboard::Mode = QClipboard::Clipboard);
|
||||
void pasteHere();
|
||||
void duplicate(const QPoint &stepOffset);
|
||||
void adjustSceneRect();
|
||||
void updateWindowTitle();
|
||||
void resetConductors();
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "QPropertyUndoCommand/qpropertyundocommand.h"
|
||||
#include "diagram.h"
|
||||
#include "qetapp.h"
|
||||
#include "qetgraphicsitem/dynamicelementtextitem.h"
|
||||
#include "qetgraphicsitem/elementtextitemgroup.h"
|
||||
|
||||
@@ -137,25 +138,14 @@ void ElementTextsMover::endMovement()
|
||||
|
||||
QString ElementTextsMover::undoText() const
|
||||
{
|
||||
QString undo_text;
|
||||
|
||||
if(m_text_count == 1)
|
||||
undo_text.append(QObject::tr("Déplacer un texte d'élément"));
|
||||
else if(m_text_count > 1)
|
||||
undo_text.append(QObject::tr("Déplacer %1 textes d'élément").arg(m_items_hash.size()));
|
||||
|
||||
if(m_group_count >= 1)
|
||||
{
|
||||
if(undo_text.isEmpty())
|
||||
undo_text.append(QObject::tr("Déplacer"));
|
||||
else
|
||||
undo_text.append(QObject::tr(" et"));
|
||||
|
||||
if(m_group_count == 1)
|
||||
undo_text.append(QObject::tr(" un groupe de texte"));
|
||||
else
|
||||
undo_text.append(QObject::tr((" %1 groupes de textes")).arg(m_group_count));
|
||||
}
|
||||
|
||||
return undo_text;
|
||||
QStringList parts;
|
||||
if (m_text_count)
|
||||
parts << QObject::tr("%n texte(s) d'élément", "", m_text_count);
|
||||
if (m_group_count)
|
||||
parts << QObject::tr("%n groupe(s) de textes", "", m_group_count);
|
||||
|
||||
if (parts.isEmpty())
|
||||
return QString(); // should never occur
|
||||
|
||||
return QObject::tr("Déplacer %1").arg(QLocale(QETApp::interfaceLanguage()).createSeparatedList(parts));
|
||||
}
|
||||
|
||||
@@ -32,8 +32,14 @@ class QetGraphicsTableFactory
|
||||
|
||||
static void createAndAddNomenclature(Diagram *diagram);
|
||||
static void createAndAddSummary(Diagram *diagram);
|
||||
private:
|
||||
// Public so a caller that has already built and configured an
|
||||
// AddTableDialog itself (never shown or exec'd -- the two
|
||||
// methods above always exec() one, which the scripting API
|
||||
// cannot use headlessly) can create a table from it directly.
|
||||
// create() only reads settings already on the dialog; nothing
|
||||
// about it depends on the dialog having been shown.
|
||||
static void create(Diagram *diagram, AddTableDialog *dialog);
|
||||
private:
|
||||
static QetGraphicsTableItem *newTable(
|
||||
Diagram *diagram,
|
||||
AddTableDialog *dialog,
|
||||
|
||||
@@ -72,6 +72,15 @@ bool AddTableDialog::adjustTableToFolio() const
|
||||
return ui->m_adjust_table_size_cb->isChecked();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AddTableDialog::setAdjustTableToFolio
|
||||
@param set
|
||||
*/
|
||||
void AddTableDialog::setAdjustTableToFolio(bool set)
|
||||
{
|
||||
ui->m_adjust_table_size_cb->setChecked(set);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AddTableDialog::addNewTableToNewDiagram
|
||||
@return
|
||||
@@ -81,6 +90,15 @@ bool AddTableDialog::addNewTableToNewDiagram() const
|
||||
return ui->m_add_table_and_folio->isChecked();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AddTableDialog::setAddNewTableToNewDiagram
|
||||
@param set
|
||||
*/
|
||||
void AddTableDialog::setAddNewTableToNewDiagram(bool set)
|
||||
{
|
||||
ui->m_add_table_and_folio->setChecked(set);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AddTableDialog::tableName
|
||||
@return
|
||||
@@ -90,6 +108,18 @@ QString AddTableDialog::tableName() const
|
||||
return ui->m_table_name_le->text();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AddTableDialog::setTableName
|
||||
Set the name field directly, so a caller that builds this dialog to
|
||||
read from (never shows or execs it -- the scripting API's addTable())
|
||||
does not need a name typed by a user who was never there to type one.
|
||||
@param name
|
||||
*/
|
||||
void AddTableDialog::setTableName(const QString &name)
|
||||
{
|
||||
ui->m_table_name_le->setText(name);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AddTableDialog::headerMargins
|
||||
@return
|
||||
|
||||
@@ -44,9 +44,12 @@ class AddTableDialog : public QDialog
|
||||
|
||||
void setQueryWidget(QWidget *widget);
|
||||
bool adjustTableToFolio() const;
|
||||
void setAdjustTableToFolio(bool set);
|
||||
bool addNewTableToNewDiagram() const;
|
||||
void setAddNewTableToNewDiagram(bool set);
|
||||
|
||||
QString tableName() const;
|
||||
void setTableName(const QString &name);
|
||||
|
||||
QMargins headerMargins() const;
|
||||
Qt::Alignment headerAlignment() const;
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
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 "palettegraphicsview.h"
|
||||
|
||||
#include "qetpalette.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QEvent>
|
||||
#include <QPaintEvent>
|
||||
#include <QStyleHintReturnMask>
|
||||
#include <QStyleOptionRubberBand>
|
||||
#include <QtMath>
|
||||
|
||||
namespace {
|
||||
/**
|
||||
QGraphicsScene::drawItems() is protected, and QGraphicsView::drawItems()
|
||||
hands the scene the viewport only when the painter is on it. The
|
||||
view paints into an image, and still needs the scene to get the
|
||||
viewport: that is what makes the scene record where it painted
|
||||
each item, which is where the item is erased from when it moves.
|
||||
Naming the member through a derived class is the standard way to
|
||||
a pointer to a protected member; a call through it dispatches to
|
||||
the scene's own override, if any.
|
||||
*/
|
||||
struct SceneAccess : QGraphicsScene
|
||||
{
|
||||
using DrawItems = void (QGraphicsScene::*)(QPainter *, int, QGraphicsItem *[],
|
||||
const QStyleOptionGraphicsItem[], QWidget *);
|
||||
static DrawItems drawItemsPointer() { return &SceneAccess::drawItems; }
|
||||
};
|
||||
}
|
||||
|
||||
PaletteGraphicsView::PaletteGraphicsView(QWidget *parent) :
|
||||
QGraphicsView(parent)
|
||||
{
|
||||
qApp->installEventFilter(this);
|
||||
}
|
||||
|
||||
PaletteGraphicsView::PaletteGraphicsView(QGraphicsScene *scene, QWidget *parent) :
|
||||
QGraphicsView(scene, parent)
|
||||
{
|
||||
qApp->installEventFilter(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief PaletteGraphicsView::invertsLightness
|
||||
@return true when the scene is shown with its lightness inverted,
|
||||
i.e. when the application palette is dark. The application palette,
|
||||
not the view's own: a style sheet on an ancestor (the folio tab
|
||||
widget has one) makes QStyleSheetStyle pin the palette of every
|
||||
widget under it to the application palette in force when the sheet
|
||||
was applied, so after a live light/dark switch palette() is stale.
|
||||
*/
|
||||
bool PaletteGraphicsView::invertsLightness() const
|
||||
{
|
||||
return QET::Palette::isDark(QApplication::palette());
|
||||
}
|
||||
|
||||
void PaletteGraphicsView::paintingInverted(bool inverted)
|
||||
{
|
||||
Q_UNUSED(inverted)
|
||||
}
|
||||
|
||||
/**
|
||||
@brief PaletteGraphicsView::eventFilter
|
||||
Repaint the whole viewport when the application palette changes. Qt
|
||||
sends that change to the application object and then repaints only
|
||||
the widgets whose own palette changed with it, which under a style
|
||||
sheet is not the case (see invertsLightness()): the scene would then
|
||||
repaint only what it updates itself, and the viewport around the
|
||||
sheet would keep the colors of the previous palette. The filter sits
|
||||
on the application object, the one receiver Qt always notifies.
|
||||
*/
|
||||
bool PaletteGraphicsView::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (watched == qApp && event->type() == QEvent::ApplicationPaletteChange)
|
||||
viewport()->update();
|
||||
return QGraphicsView::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief PaletteGraphicsView::paintEvent
|
||||
Paints as QGraphicsView on a light palette, inverted on a dark one.
|
||||
*/
|
||||
void PaletteGraphicsView::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
if (invertsLightness() && !customBackgroundColor())
|
||||
{
|
||||
paintInverted(event);
|
||||
return;
|
||||
}
|
||||
m_buffer = QImage();
|
||||
QGraphicsView::paintEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief PaletteGraphicsView::paintInverted
|
||||
Run QGraphicsView::paintEvent() with the drawing hooks redirected to
|
||||
an off-screen image of the viewport, then invert the lightness of the
|
||||
exposed part of that image between the palette's Base and Text colors
|
||||
and blit it to the viewport. Inverting the finished rendering turns
|
||||
the white sheet dark and the black ink light in one pass, and keeps
|
||||
the hue of colored strokes. The image is in viewport coordinates, so
|
||||
the hooks paint with the view's own transform and the scene records
|
||||
the items' places in the viewport, as it does on a light palette.
|
||||
@param event the paint event, for the exposed area
|
||||
*/
|
||||
void PaletteGraphicsView::paintInverted(QPaintEvent *event)
|
||||
{
|
||||
const QRect exposed = event->rect().intersected(viewport()->rect());
|
||||
if (exposed.isEmpty())
|
||||
return;
|
||||
|
||||
const qreal ratio = viewport()->devicePixelRatioF();
|
||||
const QSize size(qCeil(viewport()->width() * ratio), qCeil(viewport()->height() * ratio));
|
||||
if (m_buffer.size() != size || m_buffer.devicePixelRatio() != ratio)
|
||||
{
|
||||
m_buffer = QImage(size, QImage::Format_RGB32);
|
||||
m_buffer.setDevicePixelRatio(ratio);
|
||||
}
|
||||
|
||||
m_buffer_painter.begin(&m_buffer);
|
||||
// The hooks paint only what the scene draws; what they leave blank is
|
||||
// the white sheet, which the inversion turns into the Base color.
|
||||
m_buffer_painter.fillRect(exposed, Qt::white);
|
||||
m_buffer_painter.setClipRect(exposed);
|
||||
m_buffer_painter.setRenderHints(renderHints());
|
||||
m_buffer_painter.setWorldTransform(viewportTransform());
|
||||
|
||||
m_inverting = true;
|
||||
paintingInverted(true);
|
||||
const OptimizationFlags flags = optimizationFlags();
|
||||
setOptimizationFlag(QGraphicsView::IndirectPainting, true);
|
||||
QGraphicsView::paintEvent(event);
|
||||
setOptimizationFlags(flags);
|
||||
paintingInverted(false);
|
||||
m_inverting = false;
|
||||
|
||||
m_buffer_painter.end();
|
||||
blitInverted(exposed);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief PaletteGraphicsView::drawBackground
|
||||
Into the off-screen image while painting inverted, else as
|
||||
QGraphicsView.
|
||||
*/
|
||||
void PaletteGraphicsView::drawBackground(QPainter *painter, const QRectF &rect)
|
||||
{
|
||||
QGraphicsView::drawBackground(m_inverting ? &m_buffer_painter : painter, rect);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief PaletteGraphicsView::drawItems
|
||||
Into the off-screen image while painting inverted, with the viewport
|
||||
as the scene's widget (see SceneAccess), else as QGraphicsView.
|
||||
*/
|
||||
void PaletteGraphicsView::drawItems(QPainter *painter, int count, QGraphicsItem *items[],
|
||||
const QStyleOptionGraphicsItem options[])
|
||||
{
|
||||
if (m_inverting && scene())
|
||||
(scene()->*SceneAccess::drawItemsPointer())(&m_buffer_painter, count, items, options, viewport());
|
||||
else
|
||||
QGraphicsView::drawItems(painter, count, items, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief PaletteGraphicsView::drawForeground
|
||||
Into the off-screen image while painting inverted, else as
|
||||
QGraphicsView.
|
||||
*/
|
||||
void PaletteGraphicsView::drawForeground(QPainter *painter, const QRectF &rect)
|
||||
{
|
||||
QGraphicsView::drawForeground(m_inverting ? &m_buffer_painter : painter, rect);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief PaletteGraphicsView::blitInverted
|
||||
Invert the lightness of \a area of the off-screen image and draw it on
|
||||
the viewport, then the selection rubber band on top: the one
|
||||
QGraphicsView::paintEvent() drew went under the blit.
|
||||
@param area the part of the viewport to blit, in viewport coordinates
|
||||
*/
|
||||
void PaletteGraphicsView::blitInverted(const QRect &area)
|
||||
{
|
||||
const qreal ratio = m_buffer.devicePixelRatio();
|
||||
QImage part = m_buffer.copy(QRectF(area.topLeft() * ratio, area.size() * ratio).toAlignedRect());
|
||||
part.setDevicePixelRatio(ratio);
|
||||
// The application palette, for the reason given in invertsLightness().
|
||||
const QPalette application_palette = QApplication::palette();
|
||||
QET::Palette::invertLightness(part, application_palette.color(QPalette::Base),
|
||||
application_palette.color(QPalette::Text));
|
||||
|
||||
QPainter painter(viewport());
|
||||
painter.drawImage(area.topLeft(), part);
|
||||
drawRubberBand(painter);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief PaletteGraphicsView::drawRubberBand
|
||||
Draw the selection rubber band the way QGraphicsView::paintEvent does,
|
||||
after the inversion, in the palette colors.
|
||||
@param painter a painter on the viewport
|
||||
*/
|
||||
void PaletteGraphicsView::drawRubberBand(QPainter &painter)
|
||||
{
|
||||
const QRect band = rubberBandRect();
|
||||
if (band.isNull())
|
||||
return;
|
||||
|
||||
QStyleOptionRubberBand option;
|
||||
option.initFrom(viewport());
|
||||
option.rect = band;
|
||||
option.shape = QRubberBand::Rectangle;
|
||||
|
||||
QStyleHintReturnMask mask;
|
||||
if (viewport()->style()->styleHint(QStyle::SH_RubberBand_Mask, &option,
|
||||
viewport(), &mask))
|
||||
painter.setClipRegion(mask.region, Qt::IntersectClip);
|
||||
viewport()->style()->drawControl(QStyle::CE_RubberBand, &option,
|
||||
&painter, viewport());
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
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 PALETTE_GRAPHICS_VIEW_H
|
||||
#define PALETTE_GRAPHICS_VIEW_H
|
||||
|
||||
#include <QGraphicsView>
|
||||
#include <QImage>
|
||||
#include <QPainter>
|
||||
|
||||
/**
|
||||
A QGraphicsView that shows its scene with inverted lightness while the
|
||||
application palette is dark: white becomes the palette's Base color,
|
||||
black its Text color, and colored strokes keep their hue. The scene
|
||||
itself is left as drawn, so printing and exporting it still give black
|
||||
on white. On a light palette the view paints exactly as QGraphicsView
|
||||
does.
|
||||
|
||||
On a dark palette the view still runs QGraphicsView::paintEvent(), with
|
||||
the IndirectPainting flag set for the duration of that call, so that
|
||||
the background, the items and the foreground come through the
|
||||
drawBackground(), drawItems() and drawForeground() hooks. (That flag
|
||||
selects Qt's older item-painting algorithm, which first builds a list
|
||||
of the exposed items and their style options; it is set only while
|
||||
the view paints inverted.) The hooks
|
||||
paint into an off-screen image the size of the viewport, in viewport
|
||||
coordinates; paintEvent() then inverts the lightness of the exposed
|
||||
part of that image and blits it. Going through the real paint event,
|
||||
and handing the scene the viewport when the items are drawn, keeps the
|
||||
view on QGraphicsView's default update path, which erases a moved item
|
||||
where it was last painted, children included, even a child whose
|
||||
geometry is set while its parent is painted (a terminal's help lines).
|
||||
The alternative, rendering with QGraphicsView::render(), needed a
|
||||
receiver on QGraphicsScene::changed() to keep the scene's updates
|
||||
flowing, and that receiver puts the scene on its Qt 4.4 compatibility
|
||||
path, which erases only the moved item's own old rectangle: a moved
|
||||
element then left its terminals' help lines, which span the whole
|
||||
sheet, behind at every step (#954).
|
||||
|
||||
The CacheBackground cache mode is not supported on the inverted path.
|
||||
*/
|
||||
class PaletteGraphicsView : public QGraphicsView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PaletteGraphicsView(QWidget *parent = nullptr);
|
||||
explicit PaletteGraphicsView(QGraphicsScene *scene, QWidget *parent = nullptr);
|
||||
|
||||
bool invertsLightness() const;
|
||||
|
||||
static void setCustomBackgroundColor(bool custom) { s_custom_bg = custom; }
|
||||
static bool customBackgroundColor() { return s_custom_bg; }
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void drawBackground(QPainter *painter, const QRectF &rect) override;
|
||||
void drawItems(QPainter *painter, int count, QGraphicsItem *items[],
|
||||
const QStyleOptionGraphicsItem options[]) override;
|
||||
void drawForeground(QPainter *painter, const QRectF &rect) override;
|
||||
/**
|
||||
Called with true right before the scene is painted for an
|
||||
inverted display and with false right after, so a scene can
|
||||
adapt what it draws (a softer grid, for instance). Does
|
||||
nothing by default.
|
||||
*/
|
||||
virtual void paintingInverted(bool inverted);
|
||||
|
||||
private:
|
||||
void paintInverted(QPaintEvent *event);
|
||||
void blitInverted(const QRect &area);
|
||||
void drawRubberBand(QPainter &painter);
|
||||
|
||||
/// The off-screen image the hooks paint into while m_inverting:
|
||||
/// the viewport's size, in its coordinates. Kept between paints,
|
||||
/// dropped when the view paints on a light palette again.
|
||||
QImage m_buffer;
|
||||
QPainter m_buffer_painter;
|
||||
/// True while paintEvent() paints for an inverted display.
|
||||
bool m_inverting = false;
|
||||
static inline bool s_custom_bg = false;
|
||||
};
|
||||
|
||||
#endif
|
||||
+55
-50
@@ -16,6 +16,7 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "qet.h"
|
||||
#include "qetapp.h"
|
||||
#include "qeticons.h"
|
||||
#include "shortcutmanager.h"
|
||||
|
||||
@@ -271,84 +272,88 @@ QString QET::ElementsAndConductorsSentence(
|
||||
int tables_count,
|
||||
int terminal_strip_count)
|
||||
{
|
||||
QString text;
|
||||
QStringList parts;
|
||||
if (elements_count) {
|
||||
text += QObject::tr(
|
||||
"%n élément(s)",
|
||||
"part of a sentence listing the content of a diagram",
|
||||
elements_count
|
||||
parts.append(
|
||||
QObject::tr(
|
||||
"%n élément(s)",
|
||||
"Sentence fragment used in an automatically generated list of different objects, e.g. objects moved at the same time, which will be combined into a sentence.",
|
||||
elements_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (conductors_count) {
|
||||
if (!text.isEmpty()) text += ", ";
|
||||
text += QObject::tr(
|
||||
"%n conducteur(s)",
|
||||
"part of a sentence listing the content of a diagram",
|
||||
conductors_count
|
||||
parts.append(
|
||||
QObject::tr(
|
||||
"%n conducteur(s)",
|
||||
"Sentence fragment used in an automatically generated list of different objects, e.g. objects moved at the same time, which will be combined into a sentence.",
|
||||
conductors_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (texts_count) {
|
||||
if (!text.isEmpty()) text += ", ";
|
||||
text += QObject::tr(
|
||||
"%n champ(s) de texte",
|
||||
"part of a sentence listing the content of a diagram",
|
||||
texts_count
|
||||
parts.append(
|
||||
QObject::tr(
|
||||
"%n champ(s) de texte",
|
||||
"Sentence fragment used in an automatically generated list of different objects, e.g. objects moved at the same time, which will be combined into a sentence.",
|
||||
texts_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (images_count) {
|
||||
if (!text.isEmpty()) text += ", ";
|
||||
// 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
|
||||
);
|
||||
parts.append(
|
||||
QObject::tr(
|
||||
"%n image(s)",
|
||||
"Sentence fragment used in an automatically generated list of different objects, e.g. objects moved at the same time, which will be combined into a sentence.",
|
||||
images_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (shapes_count) {
|
||||
if (!text.isEmpty()) text += ", ";
|
||||
text += QObject::tr(
|
||||
"%n forme(s)",
|
||||
"part of a sentence listing the content of a diagram",
|
||||
shapes_count
|
||||
parts.append(
|
||||
QObject::tr(
|
||||
"%n forme(s)",
|
||||
"Sentence fragment used in an automatically generated list of different objects, e.g. objects moved at the same time, which will be combined into a sentence.",
|
||||
shapes_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (element_text_count) {
|
||||
if (!text.isEmpty()) text += ", ";
|
||||
text += QObject::tr(
|
||||
"%n texte(s) d'élément",
|
||||
"part of a sentence listing the content of a diagram",
|
||||
element_text_count);
|
||||
parts.append(
|
||||
QObject::tr(
|
||||
"%n texte(s) d'élément",
|
||||
"Sentence fragment used in an automatically generated list of different objects, e.g. objects moved at the same time, which will be combined into a sentence.",
|
||||
element_text_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (tables_count) {
|
||||
if (!text.isEmpty()) text += ", ";
|
||||
text += QObject::tr(
|
||||
"%n tableau(s)",
|
||||
"part of a sentence listing the content of diagram",
|
||||
tables_count);
|
||||
parts.append(
|
||||
QObject::tr(
|
||||
"%n tableau(s)",
|
||||
"Sentence fragment used in an automatically generated list of different objects, e.g. objects moved at the same time, which will be combined into a sentence.",
|
||||
tables_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (terminal_strip_count) {
|
||||
if (!text.isEmpty()) text += ", ";
|
||||
text += QObject::tr(
|
||||
"%n plan de bornes",
|
||||
"part of a sentence listing the content of a diagram",
|
||||
terminal_strip_count);
|
||||
parts.append(
|
||||
QObject::tr(
|
||||
"%n plan(s) de bornes",
|
||||
"Sentence fragment used in an automatically generated list of different objects, e.g. objects moved at the same time, which will be combined into a sentence.",
|
||||
terminal_strip_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return(text);
|
||||
return QLocale(QETApp::interfaceLanguage()).createSeparatedList(parts);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+90
-1
@@ -235,6 +235,8 @@ QString QETApp::loadedQtTranslationFile()
|
||||
*/
|
||||
void QETApp::setLanguage(const QString &desired_language) {
|
||||
QString languages_path = languagesPath();
|
||||
|
||||
m_interface_language = desired_language;
|
||||
|
||||
// load Qt library translations
|
||||
QString qt_l10n_path = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
|
||||
@@ -1808,6 +1810,85 @@ void QETApp::useSystemPalette(bool use) {
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
// Widgets with their own style sheet keep the palette they were
|
||||
// polished with; after a live light/dark switch they would stay in
|
||||
// the old colors (see QET::Palette::refreshStyleSheets).
|
||||
QET::Palette::refreshStyleSheets();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETApp::useCustomPalette
|
||||
Apply a user-chosen color as the application-wide palette.
|
||||
Builds a full QPalette from \a color, keeping the system palette
|
||||
as a fallback for roles we don't touch.
|
||||
@param color the user-chosen base color
|
||||
*/
|
||||
void QETApp::useCustomPalette(const QColor &color) {
|
||||
if (!color.isValid())
|
||||
return;
|
||||
|
||||
// Derive readable text colors from the chosen color.
|
||||
const bool dark = color.lightness() < 128;
|
||||
const QColor text = dark ? QColor(220, 220, 220) : QColor(30, 30, 30);
|
||||
const QColor disabled_text = dark ? QColor(175, 175, 175) : QColor(128, 128, 128);
|
||||
|
||||
// Slightly lighter/darker for button and window shading.
|
||||
QColor button = color;
|
||||
button = QColor::fromHslF(color.hslHueF(),
|
||||
color.hslSaturationF(),
|
||||
dark ? qMin(color.lightnessF() + 0.08, 1.0)
|
||||
: qMax(color.lightnessF() - 0.08, 0.0));
|
||||
QColor light = QColor::fromHslF(color.hslHueF(),
|
||||
color.hslSaturationF(),
|
||||
dark ? qMin(color.lightnessF() + 0.15, 1.0)
|
||||
: qMax(color.lightnessF() - 0.15, 0.0));
|
||||
QColor mid = QColor::fromHslF(color.hslHueF(),
|
||||
color.hslSaturationF(),
|
||||
dark ? qMin(color.lightnessF() + 0.04, 1.0)
|
||||
: qMax(color.lightnessF() - 0.04, 0.0));
|
||||
QColor dark_c = QColor::fromHslF(color.hslHueF(),
|
||||
color.hslSaturationF(),
|
||||
dark ? qMin(color.lightnessF() - 0.04, 1.0)
|
||||
: qMax(color.lightnessF() - 0.12, 0.0));
|
||||
QColor shadow = QColor::fromHslF(color.hslHueF(),
|
||||
color.hslSaturationF(),
|
||||
dark ? qMin(color.lightnessF() - 0.10, 1.0)
|
||||
: qMax(color.lightnessF() - 0.20, 0.0));
|
||||
|
||||
QPalette p;
|
||||
// Active and Inactive get the same colors; only Disabled differs.
|
||||
for (auto group : {QPalette::Active, QPalette::Inactive}) {
|
||||
p.setColor(group, QPalette::Window, color);
|
||||
p.setColor(group, QPalette::WindowText, text);
|
||||
p.setColor(group, QPalette::Base, color);
|
||||
p.setColor(group, QPalette::AlternateBase, button);
|
||||
p.setColor(group, QPalette::Text, text);
|
||||
p.setColor(group, QPalette::Button, button);
|
||||
p.setColor(group, QPalette::ButtonText, text);
|
||||
p.setColor(group, QPalette::BrightText, dark ? QColor(255,90,90) : Qt::white);
|
||||
p.setColor(group, QPalette::Highlight, QColor(30, 96, 176));
|
||||
p.setColor(group, QPalette::HighlightedText, Qt::white);
|
||||
p.setColor(group, QPalette::ToolTipBase, button);
|
||||
p.setColor(group, QPalette::ToolTipText, text);
|
||||
p.setColor(group, QPalette::Light, light);
|
||||
p.setColor(group, QPalette::Midlight, mid);
|
||||
p.setColor(group, QPalette::Mid, mid);
|
||||
p.setColor(group, QPalette::Dark, dark_c);
|
||||
p.setColor(group, QPalette::Shadow, shadow);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
|
||||
p.setColor(group, QPalette::Accent, QColor(30, 96, 176));
|
||||
#endif
|
||||
}
|
||||
p.setColor(QPalette::Disabled, QPalette::WindowText, disabled_text);
|
||||
p.setColor(QPalette::Disabled, QPalette::Text, disabled_text);
|
||||
p.setColor(QPalette::Disabled, QPalette::ButtonText, disabled_text);
|
||||
|
||||
qApp->setPalette(p);
|
||||
qApp->setStyleSheet(QString());
|
||||
|
||||
// Switch icon theme to match light/dark.
|
||||
applyIconTheme(p);
|
||||
QET::Palette::refreshStyleSheets();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2398,7 +2479,13 @@ void QETApp::initStyle()
|
||||
|
||||
//Apply or not the system style
|
||||
QSettings settings;
|
||||
useSystemPalette(settings.value("usesystemcolors", true).toBool());
|
||||
if (settings.value("usesystemcolors", true).toBool()) {
|
||||
useSystemPalette(true);
|
||||
} else if (settings.contains("customapplicationcolor")) {
|
||||
useCustomPalette(QColor(settings.value("customapplicationcolor").toString()));
|
||||
} else {
|
||||
useSystemPalette(false);
|
||||
}
|
||||
|
||||
#if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
// Setting an application palette stops Qt from following the OS
|
||||
@@ -3039,3 +3126,5 @@ int QETApp::projectId(const QETProject *project) {
|
||||
}
|
||||
return(-1);
|
||||
}
|
||||
|
||||
QString QETApp::m_interface_language;
|
||||
|
||||
@@ -68,6 +68,7 @@ class QETApp : public QObject
|
||||
public:
|
||||
static QETApp *instance();
|
||||
void setLanguage(const QString &);
|
||||
static QString interfaceLanguage() { return m_interface_language; }
|
||||
static QString langFromSetting ();
|
||||
void switchLayout(Qt::LayoutDirection);
|
||||
static void printHelp();
|
||||
@@ -246,6 +247,8 @@ class QETApp : public QObject
|
||||
static QString m_user_company_tbt_dir;
|
||||
static QString m_user_custom_tbt_dir;
|
||||
static QString m_user_macros_dir;
|
||||
|
||||
static QString m_interface_language;
|
||||
|
||||
public slots:
|
||||
void systray(QSystemTrayIcon::ActivationReason);
|
||||
@@ -263,6 +266,7 @@ class QETApp : public QObject
|
||||
void setMainWindowVisible(QMainWindow *, bool);
|
||||
void invertMainWindowVisibility(QWidget *);
|
||||
void useSystemPalette(bool);
|
||||
void useCustomPalette(const QColor &color);
|
||||
void quitQET();
|
||||
void checkRemainingWindows();
|
||||
void openFiles(const QETArguments &);
|
||||
|
||||
@@ -51,11 +51,14 @@
|
||||
#include "shortcutmanager.h"
|
||||
#include "ui/bomexportdialog.h"
|
||||
#include "ui/conductorcolortoolbutton.h"
|
||||
#include "ui/diagrambgcolorbutton.h"
|
||||
#include "ui/duplicateoffsetdialog.h"
|
||||
#include "ui/jumptoelementdialog.h"
|
||||
#include "ui/diagrampropertieseditordockwidget.h"
|
||||
#include "ui/backupdialog.h"
|
||||
#include "ui/dialogwaiting.h"
|
||||
#include "undocommand/addelementtextcommand.h"
|
||||
#include "utils/qetsettings.h"
|
||||
#include "utils/qetutils.h"
|
||||
#include "undocommand/rotateselectioncommand.h"
|
||||
#include "undocommand/rotatetextscommand.h"
|
||||
@@ -373,6 +376,46 @@ void QETDiagramEditor::setUpActions()
|
||||
new DiagramEventAddPaste(dv->diagram(), start_pos));
|
||||
});
|
||||
|
||||
//Duplicate: copy the selection and place it at a configured,
|
||||
//grid-step offset immediately -- no interactive follow-the-
|
||||
//cursor step, unlike Ctrl+V above. That is deliberate (#991):
|
||||
//the point of a duplicate shortcut is repeatable, unattended
|
||||
//stamping (configure the offset once, then tap Ctrl+D to lay
|
||||
//out a row), which an interactive placement would interrupt on
|
||||
//every press.
|
||||
m_duplicate = new QAction(QET::Icons::EditCopy, tr("Dupli&quer"), this);
|
||||
ShortcutManager::instance().registerAction(m_duplicate, "diagrameditor.duplicate", tr("Éditeur de schémas"), Qt::CTRL | Qt::Key_D);
|
||||
m_duplicate->setStatusTip(tr("Copie la sélection, décalée de l'espacement configuré", "status bar tip"));
|
||||
connect(m_duplicate, &QAction::triggered, [this]() {
|
||||
auto *dv = currentDiagramView();
|
||||
if (!dv || !dv->diagram()) return;
|
||||
|
||||
//Ask the first time only -- every later press reuses whatever
|
||||
//was confirmed then, so the shortcut can be tapped repeatedly
|
||||
//without an interruption each time. m_configure_duplicate
|
||||
//below is the deliberate way back into this dialog.
|
||||
if (!DuplicateOffsetDialog::hasSavedStepOffset()) {
|
||||
DuplicateOffsetDialog dialog(this);
|
||||
if (dialog.exec() != QDialog::Accepted) return;
|
||||
DuplicateOffsetDialog::saveStepOffset(dialog.stepOffset());
|
||||
}
|
||||
dv->duplicate(DuplicateOffsetDialog::savedStepOffset());
|
||||
});
|
||||
|
||||
//Reopens the dialog above on demand, to change the spacing or
|
||||
//direction a later Ctrl+D should use. Enabled unconditionally
|
||||
//(see slot_updateComplexActions()): it only ever writes a
|
||||
//setting, so it does not need a diagram open or anything
|
||||
//selected the way m_duplicate itself does.
|
||||
m_configure_duplicate = new QAction(tr("Configurer la duplication..."), this);
|
||||
m_configure_duplicate->setStatusTip(tr("Choisir l'espacement et la direction utilisés par Dupliquer", "status bar tip"));
|
||||
connect(m_configure_duplicate, &QAction::triggered, [this]() {
|
||||
DuplicateOffsetDialog dialog(this);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
DuplicateOffsetDialog::saveStepOffset(dialog.stepOffset());
|
||||
}
|
||||
});
|
||||
|
||||
//Reset conductor path
|
||||
m_conductor_reset = new QAction(QET::Icons::ConductorSettings, tr("Réinitialiser les conducteurs"), this);
|
||||
ShortcutManager::instance().registerAction(m_conductor_reset, "diagrameditor.conductor_reset", tr("Éditeur de schémas"), Qt::CTRL | Qt::Key_K);
|
||||
@@ -413,15 +456,8 @@ void QETDiagramEditor::setUpActions()
|
||||
pv->project()->setAutoBreakConductor(abc);
|
||||
});
|
||||
|
||||
//Switch background color
|
||||
m_grey_background = new QAction (QET::Icons::DiagramBg, tr("Couleur de fond blanc/gris","Tool tip of white/grey background button"), this);
|
||||
m_grey_background -> setStatusTip (tr("Affiche la couleur de fond du folio en blanc ou en gris", "Status tip of white/grey background button"));
|
||||
m_grey_background -> setCheckable (true);
|
||||
connect (m_grey_background, &QAction::triggered, [this](bool checked) {
|
||||
Diagram::background_color = checked ? Qt::darkGray : Qt::white;
|
||||
if (this->currentDiagramView() && this->currentDiagramView()->diagram())
|
||||
this->currentDiagramView()->diagram()->update();
|
||||
});
|
||||
//Diagram background color picker
|
||||
m_background_color_button = new DiagramBgColorToolButton(this, this);
|
||||
|
||||
//Draw or not the background grid
|
||||
m_draw_grid = new QAction ( QET::Icons::Grid, tr("Afficher la grille"), this);
|
||||
@@ -900,6 +936,7 @@ void QETDiagramEditor::setUpToolBar()
|
||||
main_tool_bar -> addAction(m_cut);
|
||||
main_tool_bar -> addAction(m_copy);
|
||||
main_tool_bar -> addAction(m_paste);
|
||||
main_tool_bar -> addAction(m_duplicate);
|
||||
main_tool_bar -> addSeparator();
|
||||
main_tool_bar -> addAction(m_delete_selection);
|
||||
main_tool_bar -> addAction(m_rotate_selection);
|
||||
@@ -912,7 +949,7 @@ void QETDiagramEditor::setUpToolBar()
|
||||
view_tool_bar -> addSeparator();
|
||||
view_tool_bar -> addAction(m_draw_grid);
|
||||
view_tool_bar -> addAction(m_draw_guides);
|
||||
view_tool_bar -> addAction (m_grey_background);
|
||||
view_tool_bar -> addWidget(m_background_color_button);
|
||||
view_tool_bar -> addSeparator();
|
||||
view_tool_bar -> addActions(m_zoom_action_toolBar);
|
||||
|
||||
@@ -991,6 +1028,8 @@ void QETDiagramEditor::setUpMenu()
|
||||
menu_edition -> addAction(m_cut);
|
||||
menu_edition -> addAction(m_copy);
|
||||
menu_edition -> addAction(m_paste);
|
||||
menu_edition -> addAction(m_duplicate);
|
||||
menu_edition -> addAction(m_configure_duplicate);
|
||||
menu_edition -> addSeparator();
|
||||
//The same actions the "Ajouter" toolbar holds. They were toolbar-only,
|
||||
//which left them unreachable for anyone working without a mouse: a
|
||||
@@ -1063,7 +1102,7 @@ void QETDiagramEditor::setUpMenu()
|
||||
menu_affichage -> addSeparator();
|
||||
menu_affichage -> addAction(m_draw_grid);
|
||||
menu_affichage -> addAction(m_draw_guides);
|
||||
menu_affichage -> addAction(m_grey_background);
|
||||
menu_affichage -> addMenu(m_background_color_button->menu());
|
||||
menu_affichage -> addSeparator();
|
||||
menu_affichage -> addActions(m_zoom_actions_group.actions());
|
||||
|
||||
@@ -1880,7 +1919,7 @@ void QETDiagramEditor::slot_updateActions()
|
||||
m_select_actions_group. setEnabled(opened_diagram);
|
||||
m_add_item_actions_group. setEnabled(editable_project);
|
||||
m_row_column_actions_group. setEnabled(editable_project);
|
||||
m_grey_background-> setEnabled(opened_diagram);
|
||||
m_background_color_button-> setEnabled(opened_diagram);
|
||||
m_draw_grid-> setEnabled(opened_diagram);
|
||||
m_draw_guides-> setEnabled(opened_diagram);
|
||||
|
||||
@@ -1953,6 +1992,7 @@ void QETDiagramEditor::slot_updateComplexActions()
|
||||
<< m_find_element
|
||||
<< m_cut
|
||||
<< m_copy
|
||||
<< m_duplicate
|
||||
<< m_delete_selection
|
||||
<< m_rotate_selection
|
||||
<< m_rotate_group_selection
|
||||
@@ -1982,6 +2022,7 @@ void QETDiagramEditor::slot_updateComplexActions()
|
||||
bool deletable_items = dc.hasDeletableItems();
|
||||
m_cut -> setEnabled(!ro && copiable_items);
|
||||
m_copy -> setEnabled(copiable_items);
|
||||
m_duplicate -> setEnabled(!ro && copiable_items);
|
||||
m_delete_selection -> setEnabled(!ro && deletable_items);
|
||||
m_rotate_selection -> setEnabled(!ro && diagram_->canRotateSelection());
|
||||
m_rotate_group_selection -> setEnabled(!ro && diagram_->canRotateSelection());
|
||||
@@ -3128,6 +3169,29 @@ void QETDiagramEditor::slot_runScript() {
|
||||
QETProject *project = currentProject();
|
||||
if (!project) return;
|
||||
|
||||
// Scripting is off until somebody says otherwise, so the first use has
|
||||
// to ask. Asking here rather than greying the action out keeps the
|
||||
// feature discoverable: a disabled menu entry tells a user that
|
||||
// something exists and nothing about how to have it.
|
||||
if (!QetSettings::scriptingEnabled()) {
|
||||
const QMessageBox::StandardButton answer = QET::QetMessageBox::question(
|
||||
this,
|
||||
tr("Exécuter un script"),
|
||||
tr("Les scripts sont désactivés.\n\n"
|
||||
"Un script s'exécute avec vos droits : il peut lire et "
|
||||
"modifier le projet ouvert et écrire des fichiers. "
|
||||
"N'exécutez que des scripts dont vous connaissez "
|
||||
"l'origine.\n\n"
|
||||
"Activer les scripts ? Ce réglage est modifiable dans "
|
||||
"Configurer QElectroTech > Général > Projets."),
|
||||
QMessageBox::Yes | QMessageBox::Cancel,
|
||||
QMessageBox::Cancel);
|
||||
if (answer != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
QetSettings::setScriptingEnabled(true);
|
||||
}
|
||||
|
||||
const QString script_path = QFileDialog::getOpenFileName(
|
||||
this,
|
||||
tr("Exécuter un script"),
|
||||
|
||||
@@ -34,6 +34,7 @@ class QETResult;
|
||||
class ProjectView;
|
||||
class ConductorColorToolButton;
|
||||
class CustomElement;
|
||||
class DiagramBgColorToolButton;
|
||||
class Diagram;
|
||||
class DiagramView;
|
||||
class Element;
|
||||
@@ -198,9 +199,10 @@ class QETDiagramEditor : public QETMainWindow
|
||||
*undo, ///< Cancel the latest action
|
||||
*redo, ///< Redo the latest cancelled operation
|
||||
*m_paste, ///< Paste clipboard content on the current diagram
|
||||
*m_duplicate, ///< Copy selection, offset by the configured step (#991)
|
||||
*m_configure_duplicate, ///< Reopen the duplicate offset/direction dialog (#991)
|
||||
*m_auto_conductor, ///< Enable/Disable the use of auto conductor
|
||||
*m_auto_break_conductor, ///< Enable/Disable the use of auto break conductor
|
||||
*m_grey_background, ///< Switch the background color in white or grey
|
||||
*m_draw_grid, ///< Switch the background grid display or not
|
||||
*m_draw_guides = nullptr, ///< Switch the custom guides display or not
|
||||
*m_project_edit_properties, ///< Edit the properties of the current project.
|
||||
@@ -240,6 +242,8 @@ class QETDiagramEditor : public QETMainWindow
|
||||
|
||||
///< One-click conductor colour, in the "Schéma" toolbar
|
||||
ConductorColorToolButton *m_conductor_color_button = nullptr;
|
||||
///< Diagram background color picker, in the "Affichage" toolbar
|
||||
DiagramBgColorToolButton *m_background_color_button = nullptr;
|
||||
|
||||
QList <QAction *> m_zoom_action_toolBar; ///Only zoom action must displayed in the toolbar
|
||||
|
||||
|
||||
@@ -379,6 +379,21 @@ void ProjectDBModel::fillValue()
|
||||
|
||||
while (query_.next())
|
||||
{
|
||||
//This query text comes out of the project file, so its result is
|
||||
//not bounded by anything the project actually contains: a
|
||||
//recursive CTE produces rows for as long as anyone reads them.
|
||||
//Without this, opening such a file hangs QElectroTech at 100% CPU
|
||||
//while m_record grows until memory runs out. @see
|
||||
//projectDataBase::MaxResultRows.
|
||||
if (m_record.size() >= projectDataBase::MaxResultRows) {
|
||||
qWarning().noquote()
|
||||
<< "ProjectDBModel: query stopped after"
|
||||
<< projectDataBase::MaxResultRows
|
||||
<< "rows, which is far more than a folio table can show."
|
||||
<< "The table is incomplete. Query:" << m_query;
|
||||
break;
|
||||
}
|
||||
|
||||
QStringList record_;
|
||||
auto i=0;
|
||||
while (query_.value(i).isValid())
|
||||
|
||||
@@ -1310,6 +1310,34 @@ const QList<ConductorSegment *> Conductor::segmentsList() const
|
||||
return(segments_vector);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Conductor::moveSegment
|
||||
Move one segment of this conductor by (dx, dy), the same primitive
|
||||
handlerMouseMoveEvent()/handlerMouseReleaseEvent() apply on a manual
|
||||
drag -- moveX()/moveY() each silently no-op on the wrong axis or a
|
||||
static (terminal-anchored) segment, so both are always called and
|
||||
whichever applies takes effect. Unlike a drag this commits the whole
|
||||
move as a single undo step.
|
||||
@param index a segmentsList() index
|
||||
@param dx @param dy the movement, in the diagram's own coordinates
|
||||
@return false if index is out of range
|
||||
*/
|
||||
bool Conductor::moveSegment(int index, qreal dx, qreal dy)
|
||||
{
|
||||
const QList<ConductorSegment *> segs = segmentsList();
|
||||
if (index < 0 || index >= segs.count()) return false;
|
||||
|
||||
before_mov_text_pos_ = m_text_item->pos();
|
||||
ConductorSegment *seg = segs.at(index);
|
||||
seg->moveX(dx);
|
||||
seg->moveY(dy);
|
||||
modified_path = true;
|
||||
segmentsToPath();
|
||||
calculateTextItemPosition();
|
||||
saveProfile();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Conductor::length
|
||||
@return the length of this conductor
|
||||
|
||||
@@ -114,6 +114,7 @@ class Conductor : public QGraphicsObject
|
||||
public:
|
||||
QVector <QPointF> handlerPoints() const;
|
||||
const QList<ConductorSegment *> segmentsList() const;
|
||||
bool moveSegment(int index, qreal dx, qreal dy);
|
||||
|
||||
void setPropertyToPotential(
|
||||
const ConductorProperties &property,
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "../qetgraphicsitem/terminal.h"
|
||||
#include "../qetinformation.h"
|
||||
#include "../utils/qetutils.h"
|
||||
#include "../QetGraphicsItemModeler/qetgraphicshandleritem.h"
|
||||
#include "crossrefitem.h"
|
||||
#include "element.h"
|
||||
#include "elementtextitemgroup.h"
|
||||
@@ -67,7 +68,9 @@ DynamicElementTextItem::DynamicElementTextItem(Element *parent_element) :
|
||||
}
|
||||
|
||||
DynamicElementTextItem::~DynamicElementTextItem()
|
||||
{}
|
||||
{
|
||||
removeResizeHandles();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DynamicElementTextItem::textFromMetaEnum
|
||||
@@ -629,7 +632,13 @@ void DynamicElementTextItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||
int diffx = qRound(current_parent_pos.x() - button_down_parent_pos.x());
|
||||
int diffy = qRound(current_parent_pos.y() - button_down_parent_pos.y());
|
||||
QPointF new_pos = m_initial_position + QPointF(diffx, diffy);
|
||||
setPos(new_pos);
|
||||
//Snap to the grid, Ctrl to place freely -- the same line
|
||||
//ElementTextItemGroup::mouseMoveEvent() and
|
||||
//ElementTextsMover::continueMovement() already use, and
|
||||
//DiagramTextItem::mouseMoveEvent() for independent texts.
|
||||
//Without it this was the only text move in the editor that
|
||||
//ignored the grid.
|
||||
event->modifiers() == Qt::ControlModifier ? setPos(new_pos) : setPos(Diagram::snapToGrid(new_pos));
|
||||
|
||||
if(diagram())
|
||||
diagram()->elementTextsMover().continueMovement(event);
|
||||
@@ -722,7 +731,20 @@ void DynamicElementTextItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
|
||||
void DynamicElementTextItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
|
||||
{
|
||||
DiagramTextItem::paint(painter, option, widget);
|
||||
|
||||
|
||||
//Only ever repositions already-existing sibling items here --
|
||||
//never adds or removes one. paint() runs while QGraphicsScene is
|
||||
//iterating its item list to draw it, and mutating that list mid
|
||||
//-iteration (which addResizeHandles()/removeResizeHandles() do,
|
||||
//through QGraphicsScene::addItem()/removeItem()) crashes. An
|
||||
//earlier version of this fix called them from here and crashed
|
||||
//qelectrotech reproducibly on deselecting a text (SIGABRT); see
|
||||
//refreshResizeHandlesVisibility() for where that now happens
|
||||
//instead -- itemChange(), Qt's own safe hook for exactly this,
|
||||
//already used below for this item's own selection.
|
||||
if (m_left_resize_handle || m_right_resize_handle)
|
||||
updateResizeHandlesPos();
|
||||
|
||||
if (m_frame)
|
||||
{
|
||||
painter->save();
|
||||
@@ -812,15 +834,41 @@ QVariant DynamicElementTextItem::itemChange(QGraphicsItem::GraphicsItemChange ch
|
||||
updateXref();
|
||||
updateXref();
|
||||
}
|
||||
|
||||
else if (change == QGraphicsItem::ItemSelectedHasChanged)
|
||||
{
|
||||
refreshResizeHandlesVisibility();
|
||||
}
|
||||
else if (change == QGraphicsItem::ItemSceneHasChanged && !scene())
|
||||
{
|
||||
removeResizeHandles();
|
||||
}
|
||||
|
||||
return QGraphicsObject::itemChange(change, value);
|
||||
}
|
||||
|
||||
bool DynamicElementTextItem::sceneEventFilter(QGraphicsItem *watched, QEvent *event)
|
||||
{
|
||||
if (watched == m_left_resize_handle || watched == m_right_resize_handle)
|
||||
{
|
||||
auto *handle = static_cast<QetGraphicsHandlerItem *>(watched);
|
||||
if (event->type() == QEvent::GraphicsSceneMousePress) {
|
||||
handlerMousePressEvent(handle, static_cast<QGraphicsSceneMouseEvent *>(event));
|
||||
return true;
|
||||
}
|
||||
else if (event->type() == QEvent::GraphicsSceneMouseMove) {
|
||||
handlerMouseMoveEvent(handle, static_cast<QGraphicsSceneMouseEvent *>(event));
|
||||
return true;
|
||||
}
|
||||
else if (event->type() == QEvent::GraphicsSceneMouseRelease) {
|
||||
handlerMouseReleaseEvent(handle, static_cast<QGraphicsSceneMouseEvent *>(event));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if(watched != m_slave_Xref_item)
|
||||
return false;
|
||||
|
||||
|
||||
if(event->type() == QEvent::GraphicsSceneHoverEnter) {
|
||||
m_slave_Xref_item->setDefaultTextColor(Qt::blue);
|
||||
return true;
|
||||
@@ -833,10 +881,166 @@ bool DynamicElementTextItem::sceneEventFilter(QGraphicsItem *watched, QEvent *ev
|
||||
zoomToLinkedElement();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DynamicElementTextItem::refreshResizeHandlesVisibility
|
||||
Show the resize handles when this text is selected directly, OR when its
|
||||
parent element is -- which is what an ordinary click without Shift
|
||||
selects (DynamicElementTextItem::mousePressEvent() forwards a plain
|
||||
click to the parent, so dragging a symbol by its label moves the whole
|
||||
symbol; a pre-existing, unrelated behaviour, left untouched here).
|
||||
Without this, the handles were reachable only via Shift+click or a
|
||||
right-click's context menu, neither of which a user reaches for to
|
||||
resize a text field (qelectrotech#591, reported by @arummler).
|
||||
|
||||
Called from itemChange() -- both this item's own ItemSelectedHasChanged,
|
||||
below, and Element::itemChange() on the parent's, which calls this on
|
||||
every one of its texts. Not from paint(): see the comment there for why
|
||||
that crashed.
|
||||
*/
|
||||
void DynamicElementTextItem::refreshResizeHandlesVisibility()
|
||||
{
|
||||
const bool handles_wanted = isSelected() || (m_parent_element && m_parent_element->isSelected());
|
||||
if (handles_wanted && !m_left_resize_handle)
|
||||
addResizeHandles();
|
||||
else if (!handles_wanted && m_left_resize_handle)
|
||||
removeResizeHandles();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DynamicElementTextItem::addResizeHandles
|
||||
Create and show the two width-resize handles (left/right edge of
|
||||
frameRect()), reusing QetGraphicsHandlerItem the same way QetShapeItem
|
||||
does for its own resize handles.
|
||||
*/
|
||||
void DynamicElementTextItem::addResizeHandles()
|
||||
{
|
||||
if (m_left_resize_handle || !scene())
|
||||
return;
|
||||
|
||||
qreal size = QETUtils::graphicsHandlerSize(this);
|
||||
m_left_resize_handle = new QetGraphicsHandlerItem(size);
|
||||
m_right_resize_handle = new QetGraphicsHandlerItem(size);
|
||||
|
||||
for (QetGraphicsHandlerItem *handle : {m_left_resize_handle, m_right_resize_handle})
|
||||
{
|
||||
scene()->addItem(handle);
|
||||
handle->setColor(Qt::darkGreen);
|
||||
handle->setZValue(zValue() + 1);
|
||||
handle->installSceneEventFilter(this);
|
||||
}
|
||||
|
||||
updateResizeHandlesPos();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DynamicElementTextItem::removeResizeHandles
|
||||
*/
|
||||
void DynamicElementTextItem::removeResizeHandles()
|
||||
{
|
||||
delete m_left_resize_handle;
|
||||
delete m_right_resize_handle;
|
||||
m_left_resize_handle = nullptr;
|
||||
m_right_resize_handle = nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DynamicElementTextItem::updateResizeHandlesPos
|
||||
Keep the two resize handles at the vertical middle of boundingRect()'s
|
||||
left and right edges, in scene coordinates -- called on every paint() so
|
||||
it stays correct across every kind of change that can move this item or
|
||||
change its size (position, rotation, font, text, textWidth...) without
|
||||
needing a dedicated hook for each one.
|
||||
|
||||
Deliberately boundingRect(), not frameRect(): frameRect() is a tight box
|
||||
around the text's own natural (idealWidth()) size, re-centred inside
|
||||
boundingRect() -- it does not grow with textWidth(). Once a text has
|
||||
been widened, that leaves a growing gap between the tight frame and the
|
||||
dashed selection outline QGraphicsView draws at boundingRect(), which is
|
||||
the box a user actually sees and expects a resize handle to sit on
|
||||
(qelectrotech#591, reported by @arummler: "the drag elements should be
|
||||
on the border of the box"). boundingRect() reflects the full
|
||||
textWidth() (it is QGraphicsTextItem's own, driven by the document's
|
||||
laid-out size), so the handles now track the box that is visibly
|
||||
resized rather than the text glyphs inside it.
|
||||
*/
|
||||
void DynamicElementTextItem::updateResizeHandlesPos()
|
||||
{
|
||||
if (!m_left_resize_handle || !m_right_resize_handle)
|
||||
return;
|
||||
|
||||
QRectF br = boundingRect();
|
||||
m_left_resize_handle->setPos(mapToScene(QPointF(br.left(), br.center().y())));
|
||||
m_right_resize_handle->setPos(mapToScene(QPointF(br.right(), br.center().y())));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DynamicElementTextItem::handlerMousePressEvent
|
||||
@param handle
|
||||
@param event
|
||||
*/
|
||||
void DynamicElementTextItem::handlerMousePressEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
Q_UNUSED(handle)
|
||||
|
||||
//The actual property value, kept as-is (possibly -1, meaning "auto")
|
||||
//so a later undo restores the exact original state rather than a
|
||||
//synthesized fixed width.
|
||||
m_resize_original_width = textWidth();
|
||||
//A concrete baseline for the live drag's delta math, which can't
|
||||
//start from -1.
|
||||
m_resize_baseline_width = (m_resize_original_width < 0) ? frameRect().width() : m_resize_original_width;
|
||||
m_resize_start_local_x = mapFromScene(event->scenePos()).x();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DynamicElementTextItem::handlerMouseMoveEvent
|
||||
Live-resize the text while dragging, exactly like the element editor's
|
||||
resize handles live-update geometry during a drag (undo is only pushed
|
||||
on release). The drag delta is resolved in this item's own local
|
||||
coordinates (not scene coordinates) so a rotated text box still resizes
|
||||
along its own baseline.
|
||||
@param handle
|
||||
@param event
|
||||
*/
|
||||
void DynamicElementTextItem::handlerMouseMoveEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
qreal local_x = mapFromScene(event->scenePos()).x();
|
||||
qreal delta = local_x - m_resize_start_local_x;
|
||||
if (handle == m_left_resize_handle)
|
||||
delta = -delta;
|
||||
|
||||
qreal new_width = qMax(m_resize_baseline_width + delta, qreal(10));
|
||||
setTextWidth(new_width);
|
||||
updateResizeHandlesPos();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DynamicElementTextItem::handlerMouseReleaseEvent
|
||||
Push the same QPropertyUndoCommand the properties-panel width spinbox
|
||||
already pushes (sources/ui/dynamicelementtextmodel.cpp) -- the value is
|
||||
already applied live from the drag, so this only makes it undoable.
|
||||
@param handle
|
||||
@param event
|
||||
*/
|
||||
void DynamicElementTextItem::handlerMouseReleaseEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
Q_UNUSED(handle)
|
||||
Q_UNUSED(event)
|
||||
|
||||
qreal new_width = textWidth();
|
||||
if (!qFuzzyCompare(m_resize_original_width, new_width) && m_parent_element && m_parent_element->diagram())
|
||||
{
|
||||
auto *undo = new QPropertyUndoCommand(this, "textWidth", QVariant(m_resize_original_width), QVariant(new_width));
|
||||
undo->setAnimated(true, false);
|
||||
undo->setText(tr("Redimensionner un texte d'élément"));
|
||||
m_parent_element->diagram()->undoStack().push(undo);
|
||||
}
|
||||
}
|
||||
|
||||
void DynamicElementTextItem::elementInfoChanged()
|
||||
{
|
||||
DiagramContext dc;
|
||||
@@ -1129,9 +1333,15 @@ void DynamicElementTextItem::updateLabel()
|
||||
|
||||
|
||||
if(m_text_from == ElementInfo && element) {
|
||||
setPlainText(element->actualLabel());
|
||||
QString new_label = element->actualLabel();
|
||||
if (toPlainText() != new_label) {
|
||||
setPlainText(new_label);
|
||||
}
|
||||
}
|
||||
else if (m_text_from == CompositeText) {
|
||||
// Use actualLabel() to ensure %{label} reflects the current
|
||||
// resolved label (e.g. after a folio/page-number change)
|
||||
dc.addValue(QStringLiteral("label"), element->actualLabel());
|
||||
setPlainText(autonum::AssignVariables::replaceVariable(m_composite_text, dc));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ class Element;
|
||||
class Conductor;
|
||||
class ElementTextItemGroup;
|
||||
class CrossRefItem;
|
||||
class QetGraphicsHandlerItem;
|
||||
|
||||
/**
|
||||
@brief The DynamicElementTextItem class
|
||||
@@ -120,6 +121,12 @@ class DynamicElementTextItem : public DiagramTextItem
|
||||
void setRotationPointCenter(bool set);
|
||||
bool rotationPointCenter() const;
|
||||
|
||||
//Called by Element::itemChange() when the PARENT's selection
|
||||
//changes, so the parent can keep each of its texts' resize
|
||||
//handles in sync with its own selection state. Public for that;
|
||||
//see the .cpp for why it exists.
|
||||
void refreshResizeHandlesVisibility();
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
@@ -151,6 +158,12 @@ class DynamicElementTextItem : public DiagramTextItem
|
||||
void zoomToLinkedElement();
|
||||
void parentElementRotationChanged();
|
||||
void thisRotationChanged();
|
||||
void addResizeHandles();
|
||||
void removeResizeHandles();
|
||||
void updateResizeHandlesPos();
|
||||
void handlerMousePressEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event);
|
||||
void handlerMouseMoveEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event);
|
||||
void handlerMouseReleaseEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event);
|
||||
|
||||
private:
|
||||
QPointer <Element>
|
||||
@@ -182,6 +195,11 @@ class DynamicElementTextItem : public DiagramTextItem
|
||||
bool m_rotation_point_center = false;
|
||||
qreal m_visual_rotation_ref = 0;
|
||||
bool m_move_parent = true;
|
||||
QetGraphicsHandlerItem *m_left_resize_handle = nullptr;
|
||||
QetGraphicsHandlerItem *m_right_resize_handle = nullptr;
|
||||
qreal m_resize_original_width = -1;
|
||||
qreal m_resize_baseline_width = -1;
|
||||
qreal m_resize_start_local_x = 0;
|
||||
};
|
||||
|
||||
#endif // DYNAMICELEMENTTEXTITEM_H
|
||||
|
||||
@@ -1664,6 +1664,31 @@ void Element::hoverLeaveEvent(QGraphicsSceneHoverEvent *e)
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Element::itemChange
|
||||
On ItemSelectedHasChanged, tell each of this element's own dynamic texts
|
||||
to re-check whether its resize handles should be showing --
|
||||
DynamicElementTextItem::refreshResizeHandlesVisibility() shows them when
|
||||
either the text itself or its parent (this) is selected. An ordinary
|
||||
click with no Shift selects the parent, not the text
|
||||
(DynamicElementTextItem::mousePressEvent() forwards it), so without this
|
||||
a plain click on a symbol never showed the resize handles this PR adds
|
||||
to its texts (qelectrotech#591, reported by @arummler) -- only
|
||||
Shift+click or a right-click's context menu did, since those are the
|
||||
paths that leave the text itself selected.
|
||||
*/
|
||||
QVariant Element::itemChange(GraphicsItemChange change, const QVariant &value)
|
||||
{
|
||||
if (change == QGraphicsItem::ItemSelectedHasChanged)
|
||||
{
|
||||
const QList<DynamicElementTextItem *> texts = dynamicTextItems();
|
||||
for (DynamicElementTextItem *deti : texts) {
|
||||
deti->refreshResizeHandlesVisibility();
|
||||
}
|
||||
}
|
||||
return QetGraphicsItem::itemChange(change, value);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Element::setUpFormula
|
||||
Set up the formula used to create the label of this element
|
||||
|
||||
@@ -255,6 +255,7 @@ class Element : public QetGraphicsItem
|
||||
QGraphicsSceneMouseEvent *event) override;
|
||||
void hoverEnterEvent(QGraphicsSceneHoverEvent *) override;
|
||||
void hoverLeaveEvent(QGraphicsSceneHoverEvent *) override;
|
||||
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
|
||||
|
||||
protected:
|
||||
//ATTRIBUTES related to linked element
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace QET {
|
||||
QIcon ConductorEdit;
|
||||
QIcon ConductorSettings;
|
||||
QIcon Configure;
|
||||
QIcon ConfigureShortcuts;
|
||||
QIcon ConfigureToolbars;
|
||||
QIcon IC_CopyFile;
|
||||
QIcon DefaultConductor;
|
||||
@@ -408,6 +409,7 @@ void QET::Icons::initIcons()
|
||||
ConductorEdit = QIcon::fromTheme("conductor-edit");
|
||||
ConductorSettings = QIcon::fromTheme("conductor-reset");
|
||||
Configure = QIcon::fromTheme("configure");
|
||||
ConfigureShortcuts = QIcon::fromTheme("configure-shortcuts");
|
||||
ConfigureToolbars = QIcon::fromTheme("configure-toolbars");
|
||||
IC_CopyFile = QIcon::fromTheme("item-copy");
|
||||
DiagramAdd = QIcon::fromTheme("folio-new");
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace QET {
|
||||
extern QIcon ConductorEdit;
|
||||
extern QIcon ConductorSettings;
|
||||
extern QIcon Configure;
|
||||
extern QIcon ConfigureShortcuts;
|
||||
extern QIcon ConfigureToolbars;
|
||||
extern QIcon IC_CopyFile;
|
||||
extern QIcon DefaultConductor;
|
||||
|
||||
@@ -17,8 +17,13 @@
|
||||
*/
|
||||
#include "qetpalette.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QColor>
|
||||
#include <QImage>
|
||||
#include <QMdiArea>
|
||||
#include <QStyle>
|
||||
#include <QTabBar>
|
||||
#include <QWidget>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
@@ -75,6 +80,57 @@ bool QET::Palette::isDark(const QPalette &palette)
|
||||
return palette.color(QPalette::Active, QPalette::Window).lightness() < 128;
|
||||
}
|
||||
|
||||
void QET::Palette::invertLightness(QImage &image, const QColor &sheet,
|
||||
const QColor &ink)
|
||||
{
|
||||
if (image.format() != QImage::Format_RGB32)
|
||||
image.convertTo(QImage::Format_RGB32);
|
||||
|
||||
// One table per channel maps the inverted value (0 = was white,
|
||||
// 255 = was black) onto the sheet..ink span.
|
||||
uchar red_of[256], green_of[256], blue_of[256];
|
||||
for (int v = 0; v < 256; ++v) {
|
||||
red_of[v] = uchar(sheet.red() + (ink.red() - sheet.red()) * v / 255);
|
||||
green_of[v] = uchar(sheet.green() + (ink.green() - sheet.green()) * v / 255);
|
||||
blue_of[v] = uchar(sheet.blue() + (ink.blue() - sheet.blue()) * v / 255);
|
||||
}
|
||||
|
||||
/* Inverting the lightness of an HSL color while keeping its hue and
|
||||
saturation leaves the distance between the highest and the lowest
|
||||
channel unchanged, so it comes down to one offset per pixel:
|
||||
c + 255 - max - min. The offset turns the highest channel into
|
||||
255 - min and the lowest into 255 - max, so no channel can leave
|
||||
the 0..255 range and no clamping is needed. The loop runs on every
|
||||
repaint of a folio, hence the plain integer arithmetic. */
|
||||
for (int y = 0; y < image.height(); ++y) {
|
||||
quint32 *line = reinterpret_cast<quint32 *>(image.scanLine(y));
|
||||
for (int x = 0, width = image.width(); x < width; ++x) {
|
||||
const quint32 pixel = line[x];
|
||||
const int red = (pixel >> 16) & 0xff;
|
||||
const int green = (pixel >> 8) & 0xff;
|
||||
const int blue = pixel & 0xff;
|
||||
int highest = red > green ? red : green;
|
||||
int lowest = red < green ? red : green;
|
||||
if (blue > highest) highest = blue;
|
||||
if (blue < lowest) lowest = blue;
|
||||
const int offset = 255 - highest - lowest;
|
||||
line[x] = 0xff000000u
|
||||
| (quint32(red_of[red + offset]) << 16)
|
||||
| (quint32(green_of[green + offset]) << 8)
|
||||
| quint32(blue_of[blue + offset]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QColor QET::Palette::gridDotColor(const QColor &sheet, bool inverted)
|
||||
{
|
||||
if (sheet == QColor(Qt::black))
|
||||
return Qt::white;
|
||||
if (inverted)
|
||||
return QColor(sheet.red() * 2 / 3, sheet.green() * 2 / 3, sheet.blue() * 2 / 3);
|
||||
return Qt::black;
|
||||
}
|
||||
|
||||
double QET::Palette::contrastRatio(const QColor &a, const QColor &b)
|
||||
{
|
||||
double lighter = relativeLuminance(a);
|
||||
@@ -243,3 +299,20 @@ QPixmap QET::Palette::forPalette(const QPixmap &pixmap, const QPalette &palette)
|
||||
result.setDevicePixelRatio(pixmap.devicePixelRatio());
|
||||
return result;
|
||||
}
|
||||
|
||||
void QET::Palette::refreshStyleSheets()
|
||||
{
|
||||
// Setting the same sheet again is not a no-op: QWidget::setStyleSheet()
|
||||
// asks QStyleSheetStyle to repolish the widget, which recomputes its
|
||||
// palette from the application palette now in force.
|
||||
const QWidgetList widgets = QApplication::allWidgets();
|
||||
for (QWidget *widget : widgets)
|
||||
if (!widget->styleSheet().isEmpty())
|
||||
widget->setStyleSheet(widget->styleSheet());
|
||||
|
||||
// Force an immediate repaint on tab bars and MDI areas so their text
|
||||
// updates together with the rest of the UI, not one event loop later.
|
||||
for (QWidget *widget : widgets)
|
||||
if (qobject_cast<QTabBar *>(widget) || qobject_cast<QMdiArea *>(widget))
|
||||
widget->update();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <QPalette>
|
||||
#include <QPixmap>
|
||||
|
||||
class QImage;
|
||||
class QStyle;
|
||||
|
||||
/**
|
||||
@@ -54,6 +55,28 @@ namespace QET {
|
||||
*/
|
||||
bool isDark(const QPalette &palette);
|
||||
|
||||
/**
|
||||
Invert the lightness of every pixel of \a image, keeping its hue
|
||||
and saturation, then stretch the result between two colors: pure
|
||||
white becomes \a sheet, pure black becomes \a ink, and a red
|
||||
line stays red, only lighter. Made for a rendering of a white
|
||||
sheet that has to read on a dark palette, with sheet = Base and
|
||||
ink = Text. The image must be opaque; an image in another format
|
||||
is converted to RGB32 first.
|
||||
*/
|
||||
void invertLightness(QImage &image, const QColor &sheet = Qt::black,
|
||||
const QColor &ink = Qt::white);
|
||||
|
||||
/**
|
||||
The color of the grid dots on a sheet of color \a sheet: black,
|
||||
or white on a black sheet. With \a inverted the sheet is about to
|
||||
be shown with its lightness inverted (PaletteGraphicsView), where
|
||||
black dots would come out as bright as the ink; the dots are then
|
||||
a third of the way from the sheet color to black, which shows as
|
||||
a soft gray.
|
||||
*/
|
||||
QColor gridDotColor(const QColor &sheet, bool inverted);
|
||||
|
||||
/**
|
||||
WCAG 2 contrast ratio between two opaque colors, from 1 (equal)
|
||||
to 21 (black on white). Normal text needs at least 4.5, large
|
||||
@@ -112,6 +135,18 @@ namespace QET {
|
||||
left alone either way.
|
||||
*/
|
||||
QPixmap forPalette(const QPixmap &pixmap, const QPalette &palette);
|
||||
|
||||
/**
|
||||
Make every widget that carries a style sheet take the current
|
||||
application palette. QApplication::setPalette() reaches plain
|
||||
widgets, but a widget with a style sheet keeps the palette
|
||||
QStyleSheetStyle resolved when the sheet was applied, so after a
|
||||
live light/dark switch it is drawn in the old colors (the folio
|
||||
tab bar and its buttons, the element info widgets, several
|
||||
configuration pages). Re-applying each widget's own sheet makes
|
||||
QStyleSheetStyle resolve it again. Call after setPalette().
|
||||
*/
|
||||
void refreshStyleSheets();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+36
-1
@@ -83,6 +83,30 @@ m_project_properties_handler{this}
|
||||
m_default_guides.append(g);
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
//Load global auto-numbering defaults from QSettings
|
||||
{
|
||||
auto conductorData = NumerotationContext::loadFromSettings(settings, QStringLiteral("autonum/conductor"));
|
||||
for (auto it = conductorData.first.constBegin(); it != conductorData.first.constEnd(); ++it) {
|
||||
addConductorAutoNum(it.key(), it.value());
|
||||
}
|
||||
if (!conductorData.second.isEmpty()) {
|
||||
setCurrentConductorAutoNum(conductorData.second);
|
||||
}
|
||||
|
||||
auto elementData = NumerotationContext::loadFromSettings(settings, QStringLiteral("autonum/element"));
|
||||
for (auto it = elementData.first.constBegin(); it != elementData.first.constEnd(); ++it) {
|
||||
addElementAutoNum(it.key(), it.value());
|
||||
}
|
||||
if (!elementData.second.isEmpty()) {
|
||||
setCurrrentElementAutonum(elementData.second);
|
||||
}
|
||||
|
||||
auto folioData = NumerotationContext::loadFromSettings(settings, QStringLiteral("autonum/folio"));
|
||||
for (auto it = folioData.first.constBegin(); it != folioData.first.constEnd(); ++it) {
|
||||
addFolioAutoNum(it.key(), it.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProjectPropertiesHandler &QETProject::projectPropertiesHandler()
|
||||
@@ -310,7 +334,18 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
|
||||
//file without a persisted uuid derives its uuid from them.
|
||||
const QByteArray content = file->readAll();
|
||||
QDomDocument xml_project;
|
||||
if (!xml_project.setContent(content))
|
||||
// PreserveSpacingOnlyNodes: without it, a text node that is entirely
|
||||
// whitespace -- e.g. a title-block custom variable deliberately set to
|
||||
// a single space, the only way to give it a value other than blank
|
||||
// (bugtracker #973) -- is silently dropped by Qt's default parsing,
|
||||
// and QDomElement::text() then returns "" for it exactly as if it had
|
||||
// never been set. Confirmed in isolation: <a> </a> parses to text()=="",
|
||||
// this option makes it text()==" ". Every place in this codebase that
|
||||
// walks a QDomNode's children already filters on isElement() (see
|
||||
// QET::findInDomElement()), so the extra whitespace-only text nodes
|
||||
// this keeps around are inert everywhere but the two elements that
|
||||
// call .text() on themselves -- which is exactly where the bug was.
|
||||
if (!xml_project.setContent(content, QDomDocument::ParseOption::PreserveSpacingOnlyNodes))
|
||||
{
|
||||
if(opened_here) {
|
||||
file->close();
|
||||
|
||||
+11
-4
@@ -67,20 +67,27 @@ bool QETStyle::isLineArt(const QImage &image)
|
||||
/**
|
||||
@brief QETStyle::hoverColor
|
||||
The palette's highlight color is the accent users already know from
|
||||
selections. On a dark palette it is too dark to read on a hovered
|
||||
button face, so it is lightened, a step at a time, until it reaches
|
||||
selections, moved away from the hovered button face until it reaches
|
||||
3:1 (WCAG 1.4.11) against the Light role: Fusion paints a hovered
|
||||
auto-raise button with a gradient that runs from Button up to about
|
||||
that color, and the icon has to read on the lightest part of it.
|
||||
that color, and the icon has to read on the lightest part of it. On a
|
||||
dark face the accent is lightened, a step at a time; on a light face it
|
||||
is darkened, which keeps a pale accent (macOS's green or yellow
|
||||
selection color, which comes with black selection text) from being
|
||||
pushed to white. Should twenty steps not get there, the button text
|
||||
color serves, which reads on the face by construction.
|
||||
*/
|
||||
QColor QETStyle::hoverColor(const QPalette &palette)
|
||||
{
|
||||
const QColor face = palette.color(QPalette::Active, QPalette::Light);
|
||||
const bool light_face = face.lightnessF() > 0.5;
|
||||
QColor ink = palette.color(QPalette::Active, QPalette::Highlight);
|
||||
// 3.5 rather than 3.0: the top of Fusion's hover gradient is a shade
|
||||
// lighter than the Light role, so the icon needs some headroom there.
|
||||
for (int step = 0; step < 20 && QET::Palette::contrastRatio(ink, face) < 3.5; ++step)
|
||||
ink = ink.lighter(110);
|
||||
ink = light_face ? ink.darker(110) : ink.lighter(110);
|
||||
if (QET::Palette::contrastRatio(ink, face) < 3.5)
|
||||
ink = palette.color(QPalette::Active, QPalette::ButtonText);
|
||||
return ink;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,10 +21,19 @@
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVariantList>
|
||||
#include <QVariantMap>
|
||||
|
||||
class QETProject;
|
||||
class DiagramView;
|
||||
class Element;
|
||||
class Terminal;
|
||||
class Conductor;
|
||||
class IndependentTextItem;
|
||||
class QetShapeItem;
|
||||
class DiagramImageItem;
|
||||
class DynamicElementTextItem;
|
||||
class QetGraphicsTableItem;
|
||||
|
||||
/**
|
||||
@brief The QetScriptApi class
|
||||
@@ -65,12 +74,246 @@ class Element;
|
||||
QPropertyUndoCommand merges consecutive commands on the same
|
||||
object+property when their text() also matches
|
||||
(QPropertyUndoCommand::mergeWith(), pre-existing), and
|
||||
setElementPosition()/moveElement() always use the same text for a
|
||||
given element -- so several position changes to the same element in a
|
||||
row collapse into one undo step, the same way dragging an element
|
||||
does, not one step per call. Verified against exactly that: two
|
||||
setElementPosition()/moveElement()/rotateElement() always use the
|
||||
same text for a given element -- so several position changes, or
|
||||
several rotations, of the same element in a row collapse into one
|
||||
undo step, the same way dragging or repeatedly rotating an element
|
||||
does, not one step per call. setElementInfo()/setElementLabel()
|
||||
behave the same way for the same reason, through
|
||||
ChangeElementInformationCommand::mergeWith(). Verified against exactly that: two
|
||||
consecutive calls on one element, then undo/undo/redo/redo, land
|
||||
where a merge predicts, not where two independent steps would.
|
||||
- @b Wiring, @b labelling and @b folios: create a conductor between two
|
||||
terminals (ConductorCreator, the same class the GUI's
|
||||
drag-a-rectangle-over-terminals path uses, so the result inherits an
|
||||
existing potential's properties and joins conductor auto-numbering),
|
||||
change an element's label or any other information key
|
||||
(ChangeElementInformationCommand, which also tells the project
|
||||
database what changed), add a folio (QETProject::addNewDiagram(),
|
||||
already undoable) and set its title (ChangeTitleBlockCommand). With
|
||||
addElement() these are what make a script able to draw rather than
|
||||
only rearrange: before them a script could place two symbols and had
|
||||
no way to connect them.
|
||||
|
||||
Terminals are addressed by their @b index in Element::terminals(),
|
||||
not by uuid, and elementTerminals() prints that indexing so a script
|
||||
can see what it is about to wire. The index is the terminal's place
|
||||
in the element's own top-to-bottom, left-to-right ordering, not the
|
||||
order its definition file lists them. Terminal uuids look like the
|
||||
obvious key and are not one: Terminal::uuid() is a property of the
|
||||
catalog .elmt definition, empty for most of the installed base and,
|
||||
where present, identical across every instance of that element -- so
|
||||
it does not distinguish one placed coil's A1 from another's.
|
||||
- @b Conductor properties and @b cross-references: set a conductor's
|
||||
number, formula, colour or section (and its look: style normal/
|
||||
dashed/dashdotted, two-colour mode and second colour, dash size,
|
||||
line width, text size, whether its number is shown -- all under the
|
||||
names the .qet file uses for them), and link a master to a slave or
|
||||
one report to another. Both follow the application's own rules rather
|
||||
than writing the field: a conductor property is applied to every
|
||||
conductor of the same electrical potential, which is what the GUI and
|
||||
search-and-replace both do -- a wire number belongs to a potential,
|
||||
not to one drawn segment -- and a link is refused unless
|
||||
LinkElementCommand::isLinkable() allows it, which is where the
|
||||
master/slave, PLC-pairing and report-direction rules already live.
|
||||
linkElements() takes a folio index for each end because a master and
|
||||
its slave are usually on different ones.
|
||||
|
||||
A conductor is addressed as "the conductor on terminal i of element
|
||||
U", not by an identity of its own: conductors have no persisted uuid,
|
||||
and the folio-scoped integer ids the file uses for their ends are
|
||||
renumbered on every save, so there is nothing stable to name one by.
|
||||
Since the change is potential-wide anyway, any terminal of the
|
||||
potential names it equally well. A terminal carrying more than one
|
||||
conductor is ambiguous and is refused rather than guessed at -- which
|
||||
in practice means a potential is addressed from one of its leaf
|
||||
terminals, not from the hub several conductors meet at.
|
||||
- @b Text and @b shapes: the drawing furniture a folio carries beside
|
||||
its circuit -- a free-standing note, a line, a rectangle, an ellipse
|
||||
-- added with the same AddGraphicsObjectCommand the corresponding GUI
|
||||
tools use, and changed through the plainText/color/rotation
|
||||
properties those items already publish. A shape's look is set with
|
||||
setShapeProperty(): color and fill (a colour name, or "none" for no
|
||||
fill), width, line-style (solid, dashed, dotted, dashdot) and
|
||||
rotation, through the pen/brush/rotation properties the shape's own
|
||||
style editor changes.
|
||||
|
||||
These are addressed by @b index into a listing sorted by position
|
||||
(top to bottom, then left to right), because unlike an element they
|
||||
carry no uuid and unlike a conductor they have no terminal to be
|
||||
named by. Position is the only identity they have, and it persists,
|
||||
so the ordering is the same after a save and reload -- verified
|
||||
against exactly that. What it is @b not stable against is adding or
|
||||
deleting one: indexes after the affected position shift, the way a
|
||||
list's do. Call texts() or shapes() again rather than holding an
|
||||
index across an edit that adds or removes one.
|
||||
- @b Querying the project database: run a read-only SELECT against the
|
||||
SQLite database QElectroTech builds from the project, and get rows
|
||||
back as objects. This is not a new door. QET already ships a
|
||||
"Requête SQL personnalisée" box in the element-query dialog where a
|
||||
user types arbitrary SQL, and it is guarded by the same
|
||||
projectDataBase::isReadOnlySelect() this calls through
|
||||
projectDataBase::newQuery(). A script gets what a user already has,
|
||||
under the same rule, and neither can write.
|
||||
|
||||
What is worth knowing is what the database @b is: a cache, rebuilt
|
||||
from the XML on every load and never written to disk. The three
|
||||
views -- element_nomenclature_view, project_summary_view and
|
||||
wiring_list_view -- exist to be queried and are the surface to
|
||||
depend on. The underlying tables are how the cache happens to be
|
||||
arranged today, and a column may move. tables() lists both so a
|
||||
script can see what it is querying rather than guess.
|
||||
- @b Terminal @b strips: create a strip, put terminal-type elements on
|
||||
it, remove it. Strips are addressed by index into terminalStrips(),
|
||||
which is the project's own order (unlike texts and shapes it is not
|
||||
re-sorted) and does shift when one is removed. Only elements whose
|
||||
link type is "terminal" can be added, the same restriction the
|
||||
editor enforces by construction.
|
||||
|
||||
stripRealTerminals() lists the strip's real terminals -- the actual
|
||||
wire-ends added by addTerminalToStrip(), one per index -- which
|
||||
physical position (clamp) each currently sits on and how many
|
||||
neighbours share it, since that is what groupTerminals() and
|
||||
bridgeTerminals() address by index into.
|
||||
|
||||
groupTerminals() merges several real terminals onto one physical
|
||||
position, choosing the receiving position the same way the terminal
|
||||
strip editor's own "group" button does: the position among the ones
|
||||
named that already carries the most real terminals, not necessarily
|
||||
the first one given -- a script asking to group indices [0, 1] is not
|
||||
guaranteed index 0's position is where they end up. bridgeTerminals()
|
||||
wires several real terminals together electrically without merging
|
||||
their positions, refused (TerminalStrip::isBridgeable()) when they
|
||||
are not all at the same level -- the same check the editor's bridge
|
||||
button applies, not a rule reimplemented here. sortTerminalStrip()
|
||||
reorders the strip's physical positions into the canonical order the
|
||||
editor's own sort button computes.
|
||||
- @b Tables: a BOM/nomenclature or a summary (table of contents) placed
|
||||
on a folio, through QetGraphicsTableFactory::create() -- the same
|
||||
factory call the "add table" menu action makes, minus the modal
|
||||
AddTableDialog it collects its settings from first. That dialog is
|
||||
still built here, off-screen and never shown or exec'd: addTable()
|
||||
calls setTableName() and the query widget's setQuery() on it, the
|
||||
same as a user filling in the form, and forces its two checkboxes
|
||||
("adjust table to folio" and "add a new folio if the table overflows"
|
||||
it) off regardless of their .ui-file default of checked -- a script
|
||||
calling addTable() once should create exactly the one table it asked
|
||||
for, not possibly several spread across folios it never asked to add.
|
||||
A script that wants either behaviour can resize the result itself or
|
||||
add its own folio.
|
||||
|
||||
Neither creating nor deleting a table is undoable:
|
||||
QetGraphicsTableFactory::newTable(), which create() calls, calls
|
||||
Diagram::addItem() directly, with no undo command of its own, in the
|
||||
stock "add table" action as much as here -- a pre-existing gap in the
|
||||
application, not something introduced by this API. Tables are
|
||||
addressed by index in a position-sorted listing, like texts, shapes
|
||||
and images.
|
||||
- @b Auto-numbering: define a named numbering context of kind
|
||||
"conductor", "element" or "folio", built from parts written
|
||||
"type[:value[:increase]]" -- types are the ones the auto-numbering
|
||||
dialog offers (string, unit, ten, hundred, alpha, idfolio, folio,
|
||||
plant, locmach, elementline, elementcolumn, elementprefix, wrap,
|
||||
unitfolio, tenfolio, hundredfolio) -- and select which one a folio's
|
||||
new conductors use. Defining or removing a context is not undoable,
|
||||
because the application itself does it through direct project calls
|
||||
and only the counter advance is on the undo stack; the numbering
|
||||
actually applied to a conductor is.
|
||||
|
||||
For elements, useElementAutoNum() selects the current context and
|
||||
numberElement() applies it to one element, as the "add element" tool
|
||||
does right after placing one. addElement() deliberately does not
|
||||
number what it places: doing it silently would change what an existing
|
||||
script produces the moment its project happens to have a context
|
||||
selected, so it is a separate, explicit call. Folio auto-numbering is
|
||||
not offered: in the application it spawns whole new folios from a
|
||||
context, which is a different operation from labelling.
|
||||
- @b Duplicating: copy elements, together with the conductors that run
|
||||
between them, to a position on the same or another folio, through
|
||||
Diagram::toXml() and fromXml() and PasteDiagramCommand -- what Ctrl+C
|
||||
and Ctrl+V do, so a paste behaves as a paste does there: the copies
|
||||
come without their labels and without their conductors' wire numbers,
|
||||
which the application clears on paste (measured: '' on both).
|
||||
The position is the top left of the pasted group's bounding rectangle,
|
||||
so an element's own origin ends up offset from it by its hotspot
|
||||
(measured: +20, +30 for a coil); (0, 0) is not a position but means
|
||||
"keep the source coordinates", as Diagram::fromXml() treats it. The
|
||||
result lists the copies in the order the elements were named --
|
||||
the application's own list is in scene order, and a caller pairing by
|
||||
index would otherwise be wired to the wrong copies -- paired by
|
||||
position, which a paste preserves, so two elements at the same point
|
||||
cannot be told apart. A conductor is copied
|
||||
only if both its ends are among the copied elements. The previous
|
||||
selection is put back afterwards, since copying works by selecting.
|
||||
- @b Project title and folio frame: setProjectTitle(), and the grid that
|
||||
frames each folio -- columns and rows, their size, and whether the
|
||||
headers show (columns, column-width, display-columns, rows, row-height,
|
||||
display-rows) -- through ChangeBorderCommand. These are the six fields
|
||||
the folio properties panel offers; the title block's header sizes,
|
||||
which it does not, are left alone. Changing the project title is not
|
||||
undoable: the application sets it directly too.
|
||||
|
||||
A folio's title block @b template is a seventh, separate case:
|
||||
Diagram::setTitleBlockTemplate() resolves a name only against
|
||||
QETProject::embeddedTitleBlockTemplatesCollection() -- the same
|
||||
copy-into-the-project step addElement() already does for elements,
|
||||
and for the same reason (a project opened on another machine must not
|
||||
depend on files only this one has). titleBlockTemplates() lists what
|
||||
is embedded and what is available to embed from the common/company
|
||||
/custom collections, each name suffixed with its source;
|
||||
embedTitleBlockTemplate() does the copy (QDomElement in, unmodified,
|
||||
via *TemplatesCollection::get/setTemplateXmlDescription() -- neither
|
||||
side is scripting-specific code, both already exist for the template
|
||||
editor to call). setFolioProperty(folio, "template", name) then
|
||||
embeds it first if it is not already, refusing only if no collection
|
||||
has that name at all. Embedding is not undoable, the same as defining
|
||||
an auto-numbering context is not: the application does both through
|
||||
direct collection/project calls with no undo command of their own.
|
||||
A template literally named "default" reads back as folioProperty()
|
||||
"" afterwards, not "default": BorderTitleBlock::titleBlockTemplateName()
|
||||
treats the two as the same thing, since "no override" already renders
|
||||
with the template named "default".
|
||||
- @b Geometry and folio order: elementGeometry() reads where an element
|
||||
is -- x, y (its origin), rotation, and the box it occupies on the folio
|
||||
(left, top, right, bottom) -- so a script can lay one thing out relative
|
||||
to another instead of only setting absolute coordinates, and can check
|
||||
that a move landed. insertFolio() puts a new folio at a position
|
||||
instead of at the end, which is what reordering is mostly for while
|
||||
moving an existing folio still needs the application's project view.
|
||||
- @b Images: place a picture from a file. The pixels are copied into
|
||||
the project, which stores them inline in the .qet -- the saved file
|
||||
does not refer to the original path, so it opens on another machine,
|
||||
and it grows by roughly the size of the image, which is why files
|
||||
over 10 MB are refused. Images are addressed by index in a
|
||||
position-sorted listing, like texts and shapes -- by the on-screen
|
||||
bounding box, so scaling or rotating an image, which turns about its
|
||||
centre, can change where it sorts. Re-list after either.
|
||||
- @b Element @b texts: the text fields drawn on a symbol -- its label,
|
||||
the names beside its terminals, any value the definition placed there.
|
||||
A symbol arrives with the fields its definition gives it; setElementLabel()
|
||||
fills the value one of them shows, and these methods control the fields
|
||||
themselves: where each sits, its size, whether it draws a frame, what it
|
||||
shows, and adding or deleting one. Addressed by index in the element's
|
||||
own list, which follows the definition's order and shifts when one is
|
||||
deleted -- and undoing a deletion puts the field back at the end, so
|
||||
list again after either.
|
||||
|
||||
Two things called text, which differ for a field bound to an
|
||||
information key: the @b "text" property is the field's stored string,
|
||||
which for an information-bound field is an unused placeholder (empty,
|
||||
or "Texte" once one has been added), and @b "shows" is what is drawn,
|
||||
which follows the element's information straight away -- compared
|
||||
against elementInfo() at seven points across relabel, rebinding,
|
||||
setting and undo, with no difference. Read "shows".
|
||||
|
||||
Consecutive setElementInfo()/setElementLabel() calls on one element
|
||||
merge into a single undo step, as ChangeElementInformationCommand
|
||||
does, so one undo can revert several.
|
||||
|
||||
A field's @b source is "text" (a fixed string), "info" (the value of one
|
||||
of the element's information keys, so it follows setElementInfo() and
|
||||
setElementLabel()) or "composite" (a formula over several). Position is in
|
||||
the element's own coordinates, not the folio's.
|
||||
- @b Navigating and @b messaging: select an element, zoom the active
|
||||
view, and show the user a message. Deliberately narrow: selection and
|
||||
messaging work with no view at all (headless `--run`); zoom is a no-op
|
||||
@@ -87,7 +330,11 @@ class Element;
|
||||
import-collision case that would otherwise reach
|
||||
QETProject::importElement()'s own ImportElementDialog::exec() and
|
||||
refuses instead, rather than let a plain QDialog (not routed through
|
||||
QetMessageBox) block a script the same way.
|
||||
QetMessageBox) block a script the same way. addConductor() declines the
|
||||
same way, for the same reason, when the two terminals belong to two
|
||||
different existing potentials and ConductorCreator would therefore ask
|
||||
which one's properties to inherit -- measured: with that check removed,
|
||||
exactly that call never returns.
|
||||
*/
|
||||
class QetScriptApi : public QObject
|
||||
{
|
||||
@@ -128,15 +375,187 @@ class QetScriptApi : public QObject
|
||||
Q_INVOKABLE QString addElement(int folioIndex, const QString &locationPath, double x, double y);
|
||||
Q_INVOKABLE bool setElementPosition(int folioIndex, const QString &elementUuid, double x, double y);
|
||||
Q_INVOKABLE bool moveElement(int folioIndex, const QString &elementUuid, double dx, double dy);
|
||||
Q_INVOKABLE bool rotateElement(int folioIndex, const QString &elementUuid, double angle);
|
||||
Q_INVOKABLE bool deleteElement(int folioIndex, const QString &elementUuid);
|
||||
|
||||
// -- address what is already there --
|
||||
Q_INVOKABLE QStringList elementUuids(int folioIndex) const;
|
||||
Q_INVOKABLE QString elementName(int folioIndex, const QString &elementUuid) const;
|
||||
Q_INVOKABLE QStringList elementTerminals(int folioIndex, const QString &elementUuid) const;
|
||||
|
||||
// -- element information, through ChangeElementInformationCommand --
|
||||
Q_INVOKABLE QString elementInfo(int folioIndex, const QString &elementUuid, const QString &key) const;
|
||||
Q_INVOKABLE bool setElementInfo(int folioIndex, const QString &elementUuid, const QString &key, const QString &value);
|
||||
Q_INVOKABLE QString elementLabel(int folioIndex, const QString &elementUuid) const;
|
||||
Q_INVOKABLE bool setElementLabel(int folioIndex, const QString &elementUuid, const QString &label);
|
||||
|
||||
// -- wire two terminals together --
|
||||
Q_INVOKABLE bool addConductor(int folioIndex,
|
||||
const QString &elementUuidA, int terminalIndexA,
|
||||
const QString &elementUuidB, int terminalIndexB);
|
||||
|
||||
// -- conductor properties, applied to the whole potential --
|
||||
Q_INVOKABLE QStringList conductors(int folioIndex) const;
|
||||
Q_INVOKABLE QString conductorProperty(int folioIndex, const QString &elementUuid,
|
||||
int terminalIndex, const QString &property) const;
|
||||
Q_INVOKABLE bool setConductorProperty(int folioIndex, const QString &elementUuid,
|
||||
int terminalIndex, const QString &property,
|
||||
const QString &value);
|
||||
|
||||
// -- a conductor's own drawn path, not the whole potential's
|
||||
// properties above -- one conductor only, addressed the same way --
|
||||
Q_INVOKABLE QStringList conductorSegments(int folioIndex, const QString &elementUuid,
|
||||
int terminalIndex) const;
|
||||
Q_INVOKABLE bool moveConductorSegment(int folioIndex, const QString &elementUuid,
|
||||
int terminalIndex, int segmentIndex,
|
||||
double dx, double dy);
|
||||
|
||||
// -- cross-references: master/slave and report links --
|
||||
Q_INVOKABLE QString elementLinkType(int folioIndex, const QString &elementUuid) const;
|
||||
Q_INVOKABLE QStringList linkedElements(int folioIndex, const QString &elementUuid) const;
|
||||
Q_INVOKABLE bool linkElements(int folioIndexA, const QString &elementUuidA,
|
||||
int folioIndexB, const QString &elementUuidB,
|
||||
int groupIndex = -1);
|
||||
Q_INVOKABLE bool unlinkElement(int folioIndex, const QString &elementUuid);
|
||||
Q_INVOKABLE int elementLinkGroupIndex(int folioIndex, const QString &elementUuid,
|
||||
int otherFolioIndex, const QString &otherElementUuid) const;
|
||||
|
||||
// -- a PLC master's IO table: address/function/comment rows a PLC
|
||||
// slave links onto via linkElements()'s groupIndex --
|
||||
Q_INVOKABLE QStringList plcIOs(int folioIndex, const QString &elementUuid) const;
|
||||
Q_INVOKABLE int addPlcIO(int folioIndex, const QString &elementUuid, const QString &type,
|
||||
const QString &address, const QString &functionText,
|
||||
const QString &comment);
|
||||
Q_INVOKABLE bool setPlcIO(int folioIndex, const QString &elementUuid, int ioIndex,
|
||||
const QString &property, const QString &value);
|
||||
Q_INVOKABLE bool removePlcIO(int folioIndex, const QString &elementUuid, int ioIndex);
|
||||
|
||||
// -- independent text and drawing shapes --
|
||||
Q_INVOKABLE QStringList texts(int folioIndex) const;
|
||||
Q_INVOKABLE int addText(int folioIndex, const QString &text, double x, double y);
|
||||
Q_INVOKABLE QString textContent(int folioIndex, int textIndex) const;
|
||||
Q_INVOKABLE bool setTextContent(int folioIndex, int textIndex, const QString &text);
|
||||
Q_INVOKABLE bool setTextColor(int folioIndex, int textIndex, const QString &color);
|
||||
Q_INVOKABLE bool setTextRotation(int folioIndex, int textIndex, double angle);
|
||||
Q_INVOKABLE bool deleteText(int folioIndex, int textIndex);
|
||||
|
||||
Q_INVOKABLE QStringList shapes(int folioIndex) const;
|
||||
Q_INVOKABLE int addShape(int folioIndex, const QString &type,
|
||||
double x1, double y1, double x2, double y2);
|
||||
Q_INVOKABLE bool deleteShape(int folioIndex, int shapeIndex);
|
||||
Q_INVOKABLE QString shapeProperty(int folioIndex, int shapeIndex, const QString &property) const;
|
||||
Q_INVOKABLE bool setShapeProperty(int folioIndex, int shapeIndex,
|
||||
const QString &property, const QString &value);
|
||||
|
||||
// -- polygon and path shapes: more than addShape()'s two-point box --
|
||||
Q_INVOKABLE int addPolygon(int folioIndex, const QVariantList &points, bool closed);
|
||||
Q_INVOKABLE QVariantList shapePolygon(int folioIndex, int shapeIndex) const;
|
||||
Q_INVOKABLE bool setShapePolygon(int folioIndex, int shapeIndex, const QVariantList &points);
|
||||
Q_INVOKABLE int addPath(int folioIndex, const QVariantList &nodes, bool closed);
|
||||
Q_INVOKABLE QVariantList shapePathNodes(int folioIndex, int shapeIndex) const;
|
||||
Q_INVOKABLE bool setShapePathNodes(int folioIndex, int shapeIndex, const QVariantList &nodes);
|
||||
Q_INVOKABLE bool setShapeClosed(int folioIndex, int shapeIndex, bool closed);
|
||||
|
||||
// -- query the project database --
|
||||
Q_INVOKABLE QStringList tables() const;
|
||||
Q_INVOKABLE QVariantList query(const QString &sql);
|
||||
Q_INVOKABLE QString queryError() const;
|
||||
|
||||
// -- removing a conductor or a folio; folio properties beyond the title --
|
||||
Q_INVOKABLE bool deleteConductor(int folioIndex, const QString &elementUuid, int terminalIndex);
|
||||
Q_INVOKABLE bool removeFolio(int folioIndex);
|
||||
Q_INVOKABLE bool setFolioProperty(int folioIndex, const QString &property, const QString &value);
|
||||
Q_INVOKABLE QString folioProperty(int folioIndex, const QString &property) const;
|
||||
|
||||
// -- terminal strips (borniers) --
|
||||
Q_INVOKABLE QStringList terminalStrips() const;
|
||||
Q_INVOKABLE int addTerminalStrip(const QString &installation, const QString &location,
|
||||
const QString &name);
|
||||
Q_INVOKABLE bool removeTerminalStrip(int stripIndex);
|
||||
Q_INVOKABLE bool addTerminalToStrip(int stripIndex, int folioIndex,
|
||||
const QString &elementUuid);
|
||||
Q_INVOKABLE QStringList stripRealTerminals(int stripIndex) const;
|
||||
Q_INVOKABLE bool groupTerminals(int stripIndex, const QVariantList &realTerminalIndices);
|
||||
Q_INVOKABLE bool bridgeTerminals(int stripIndex, const QVariantList &realTerminalIndices);
|
||||
Q_INVOKABLE bool sortTerminalStrip(int stripIndex);
|
||||
|
||||
// -- a BOM/nomenclature or summary table placed on a folio --
|
||||
Q_INVOKABLE QStringList tables(int folioIndex) const;
|
||||
Q_INVOKABLE int addTable(int folioIndex, const QString &kind, const QString &name,
|
||||
const QString &query);
|
||||
Q_INVOKABLE bool deleteTable(int folioIndex, int tableIndex);
|
||||
Q_INVOKABLE bool setTablePosition(int folioIndex, int tableIndex, double x, double y);
|
||||
|
||||
// -- auto-numbering contexts (conductor, element, folio) --
|
||||
Q_INVOKABLE QStringList autoNums(const QString &kind) const;
|
||||
Q_INVOKABLE bool addAutoNum(const QString &kind, const QString &name, const QStringList &parts);
|
||||
Q_INVOKABLE bool removeAutoNum(const QString &kind, const QString &name);
|
||||
Q_INVOKABLE bool useConductorAutoNum(int folioIndex, const QString &name);
|
||||
Q_INVOKABLE bool useElementAutoNum(const QString &name);
|
||||
Q_INVOKABLE bool numberElement(int folioIndex, const QString &elementUuid);
|
||||
|
||||
// -- images, embedded in the project --
|
||||
Q_INVOKABLE QStringList images(int folioIndex) const;
|
||||
Q_INVOKABLE int addImage(int folioIndex, const QString &filePath, double x, double y);
|
||||
Q_INVOKABLE bool setImageScale(int folioIndex, int imageIndex, double factor);
|
||||
Q_INVOKABLE bool setImageRotation(int folioIndex, int imageIndex, double angle);
|
||||
Q_INVOKABLE bool deleteImage(int folioIndex, int imageIndex);
|
||||
Q_INVOKABLE int addPdfPage(int folioIndex, const QString &pdfPath, int pageNumber,
|
||||
int dpi, double x, double y);
|
||||
|
||||
// -- the text fields shown on a symbol (label, terminal names, ...) --
|
||||
Q_INVOKABLE QStringList elementTexts(int folioIndex, const QString &elementUuid) const;
|
||||
Q_INVOKABLE int addElementText(int folioIndex, const QString &elementUuid,
|
||||
const QString &source, const QString &value,
|
||||
double x, double y);
|
||||
Q_INVOKABLE bool setElementTextProperty(int folioIndex, const QString &elementUuid,
|
||||
int textIndex, const QString &property,
|
||||
const QString &value);
|
||||
Q_INVOKABLE QString elementTextProperty(int folioIndex, const QString &elementUuid,
|
||||
int textIndex, const QString &property) const;
|
||||
Q_INVOKABLE bool deleteElementText(int folioIndex, const QString &elementUuid, int textIndex);
|
||||
|
||||
// -- copy elements (with the conductors between them) to a position --
|
||||
Q_INVOKABLE QStringList duplicateElements(int fromFolioIndex, const QStringList &elementUuids,
|
||||
int toFolioIndex, double x, double y);
|
||||
|
||||
// -- the project title, and each folio's frame (grid of columns and rows) --
|
||||
Q_INVOKABLE bool setProjectTitle(const QString &title);
|
||||
Q_INVOKABLE QString folioBorder(int folioIndex, const QString &property) const;
|
||||
Q_INVOKABLE bool setFolioBorder(int folioIndex, const QString &property, const QString &value);
|
||||
|
||||
// -- title block templates: which exist, embedding one into the project --
|
||||
Q_INVOKABLE QStringList titleBlockTemplates() const;
|
||||
Q_INVOKABLE bool embedTitleBlockTemplate(const QString &name);
|
||||
|
||||
// -- read an element's geometry --
|
||||
Q_INVOKABLE QVariantMap elementGeometry(int folioIndex, const QString &elementUuid) const;
|
||||
|
||||
// -- folios --
|
||||
Q_INVOKABLE int addFolio();
|
||||
Q_INVOKABLE int insertFolio(int position);
|
||||
Q_INVOKABLE bool setFolioTitle(int folioIndex, const QString &title);
|
||||
|
||||
Q_INVOKABLE bool undo();
|
||||
Q_INVOKABLE bool redo();
|
||||
Q_INVOKABLE bool canUndo() const;
|
||||
Q_INVOKABLE bool canRedo() const;
|
||||
|
||||
// -- project-wide text search & replace, one undo step for the
|
||||
// whole run, in the same spirit as the "Search and replace" panel --
|
||||
Q_INVOKABLE int searchAndReplace(const QString &kind, const QString &field,
|
||||
const QString &pattern, const QString &replacement,
|
||||
bool useRegex, bool caseSensitive);
|
||||
|
||||
// -- electrical continuity / ERC: read-only, structural checks
|
||||
// against the live object graph rather than the XML -- see the
|
||||
// .cpp doc comment for exactly what is and is not covered --
|
||||
Q_INVOKABLE QVariantList checkContinuity(int folioIndex);
|
||||
|
||||
// -- navigate and message --
|
||||
Q_INVOKABLE bool selectElement(const QString &elementUuid);
|
||||
Q_INVOKABLE void deselectAll(int folioIndex);
|
||||
Q_INVOKABLE QStringList selectedElements(int folioIndex) const;
|
||||
Q_INVOKABLE bool zoomFit();
|
||||
Q_INVOKABLE bool zoomToContent();
|
||||
Q_INVOKABLE bool zoomReset();
|
||||
@@ -148,9 +567,23 @@ class QetScriptApi : public QObject
|
||||
private:
|
||||
bool runFlag(const QString &flag, const QStringList &args);
|
||||
Element *findElement(int folioIndex, const QString &elementUuid) const;
|
||||
Terminal *findTerminal(int folioIndex, const QString &elementUuid, int terminalIndex,
|
||||
const QString &caller);
|
||||
Conductor *findConductor(int folioIndex, const QString &elementUuid, int terminalIndex,
|
||||
const QString &caller);
|
||||
QList<IndependentTextItem *> sortedTexts(int folioIndex) const;
|
||||
QList<QetShapeItem *> sortedShapes(int folioIndex) const;
|
||||
QList<QetGraphicsTableItem *> sortedTables(int folioIndex) const;
|
||||
QList<DiagramImageItem *> sortedImages(int folioIndex) const;
|
||||
DynamicElementTextItem *findElementText(int folioIndex, const QString &elementUuid,
|
||||
int textIndex, const QString &caller) const;
|
||||
IndependentTextItem *findText(int folioIndex, int textIndex, const QString &caller);
|
||||
bool setInfoKey(int folioIndex, const QString &elementUuid,
|
||||
const QString &key, const QString &value, const QString &caller);
|
||||
|
||||
QETProject *m_project;
|
||||
DiagramView *m_view;
|
||||
QString m_query_error;
|
||||
};
|
||||
|
||||
#endif // QET_SCRIPT_API_H
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "qetscriptapi.h"
|
||||
#include "../qetmessagebox.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../utils/qetsettings.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
@@ -48,8 +49,33 @@ bool isRunRequest(const QStringList &args)
|
||||
|
||||
#ifdef QET_HAS_SCRIPTING
|
||||
|
||||
namespace {
|
||||
/**
|
||||
@brief refusalMessage
|
||||
What to tell somebody whose script was not run, and how to change
|
||||
that. Written once because the command line and the graphical
|
||||
editor both need to say it, and an explanation that names only one
|
||||
of the two ways out sends half the people down the wrong path.
|
||||
*/
|
||||
QString refusalMessage()
|
||||
{
|
||||
return QObject::tr(
|
||||
"Les scripts sont désactivés.\n\n"
|
||||
"Un script a accès à l'ensemble du projet et peut écrire des "
|
||||
"fichiers, aussi cette fonction est-elle désactivée par défaut.\n\n"
|
||||
"Pour l'activer : Configurer QElectroTech > Général > Projets, "
|
||||
"ou définir la variable d'environnement QET_ENABLE_SCRIPTING=1 "
|
||||
"pour une exécution sans interface (CI, traitement par lot).");
|
||||
}
|
||||
}
|
||||
|
||||
int run(const QStringList &args)
|
||||
{
|
||||
if (!QetSettings::scriptingEnabled()) {
|
||||
err << refusalMessage() << "\n";
|
||||
return 3;
|
||||
}
|
||||
|
||||
const int idx = args.indexOf(QStringLiteral("--run"));
|
||||
const QString script_path = args.value(idx + 1);
|
||||
const QString project_path = args.value(idx + 2);
|
||||
@@ -88,6 +114,20 @@ namespace {
|
||||
|
||||
bool runOnProject(const QString &scriptPath, QETProject *project, DiagramView *view)
|
||||
{
|
||||
// Checked here as well as at each caller, deliberately: this is the
|
||||
// one function that actually evaluates JavaScript, so it is the one
|
||||
// place a future caller cannot forget to ask. The callers check first
|
||||
// only to give a better answer than this one can -- a usable exit code
|
||||
// on the command line, an offer to switch the setting on in the editor.
|
||||
if (!QetSettings::scriptingEnabled()) {
|
||||
err << refusalMessage() << "\n";
|
||||
if (view) {
|
||||
QET::QetMessageBox::warning(nullptr, QObject::tr("Script"),
|
||||
refusalMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QFile file(scriptPath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
err << "Cannot open script: " << scriptPath << "\n";
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "NameList/nameslist.h"
|
||||
#include "createdxf.h"
|
||||
#include "diagram.h"
|
||||
#include "qet.h"
|
||||
#include "qetapp.h"
|
||||
// uncomment the line below to get more debug information
|
||||
@@ -1589,8 +1590,10 @@ void TitleBlockTemplate::render(QPainter &painter,
|
||||
int titleblock_height = height();
|
||||
|
||||
painter.save();
|
||||
//Setup the QPainter
|
||||
QPen pen(Qt::black);
|
||||
//Setup the QPainter - use a color that contrasts with the background
|
||||
QColor ink = Diagram::background_color.lightness() < 128
|
||||
? QColor(Qt::white) : QColor(Qt::black);
|
||||
QPen pen(ink);
|
||||
painter.setPen(pen);
|
||||
|
||||
// draw the titleblock border
|
||||
@@ -1737,7 +1740,9 @@ void TitleBlockTemplate::renderCell(QPainter &painter,
|
||||
{
|
||||
// draw the border rect of the current cell
|
||||
QPen pen(QBrush(), 1, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin);
|
||||
pen.setColor(Qt::black);
|
||||
QColor ink = Diagram::background_color.lightness() < 128
|
||||
? QColor(Qt::white) : QColor(Qt::black);
|
||||
pen.setColor(ink);
|
||||
painter.setPen(pen);
|
||||
painter.drawRect(cell_rect);
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
#include "../titleblockpropertieswidget.h"
|
||||
#include "../xrefpropertieswidget.h"
|
||||
#include "guidespropertieswidget.h"
|
||||
#include "../autoNum/numerotationcontext.h"
|
||||
#include "../autoNum/ui/selectautonumw.h"
|
||||
#include <QFont>
|
||||
#include <QFontDialog>
|
||||
#include <QSizePolicy>
|
||||
@@ -101,6 +103,32 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
|
||||
}
|
||||
m_gpw->setGuides(loaded_guides);
|
||||
|
||||
// global auto-numbering defaults (only when editing global settings, not a project)
|
||||
if (!m_project) {
|
||||
auto saw_conductor = new SelectAutonumW(1);
|
||||
auto saw_element = new SelectAutonumW(0);
|
||||
auto saw_folio = new SelectAutonumW(2);
|
||||
|
||||
initAutoNumTab(m_autonum_conductor, saw_conductor, QStringLiteral("autonum/conductor"));
|
||||
initAutoNumTab(m_autonum_element, saw_element, QStringLiteral("autonum/element"));
|
||||
initAutoNumTab(m_autonum_folio, saw_folio, QStringLiteral("autonum/folio"));
|
||||
|
||||
QSettings autonum_settings;
|
||||
loadAutoNumTab(m_autonum_conductor, autonum_settings);
|
||||
loadAutoNumTab(m_autonum_element, autonum_settings);
|
||||
loadAutoNumTab(m_autonum_folio, autonum_settings);
|
||||
|
||||
// Intercept Return key in the combo line edits so it doesn't
|
||||
// activate the dialog's default button (OK).
|
||||
for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) {
|
||||
if (QComboBox *combo = tab->widget->contextComboBox()) {
|
||||
if (combo->lineEdit()) {
|
||||
combo->lineEdit()->installEventFilter(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//If there is a project, we edit his properties
|
||||
if (m_project) {
|
||||
bpw -> setProperties (m_project -> defaultBorderProperties());
|
||||
@@ -114,6 +142,7 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
|
||||
|
||||
// main tab widget
|
||||
QTabWidget *tab_widget = new QTabWidget(this);
|
||||
m_tab_widget = tab_widget;
|
||||
QWidget *diagram_widget = new QWidget();
|
||||
QVBoxLayout *diagram_layout = new QVBoxLayout(diagram_widget);
|
||||
|
||||
@@ -127,6 +156,19 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
|
||||
tab_widget -> addTab (xrefpw, tr("Références croisées"));
|
||||
tab_widget -> addTab (m_gpw, tr("Guides"));
|
||||
|
||||
// add auto-numbering tab only for global settings (not per project)
|
||||
if (!m_project) {
|
||||
QWidget *autonum_widget = new QWidget();
|
||||
QVBoxLayout *autonum_layout = new QVBoxLayout(autonum_widget);
|
||||
autonum_layout->addWidget(new QLabel(tr("Définir les règles de numérotation automatique par défaut pour les nouveaux projets :")));
|
||||
QTabWidget *autonum_inner_tab = new QTabWidget();
|
||||
autonum_inner_tab->addTab(m_autonum_conductor.widget, tr("Conducteurs"));
|
||||
autonum_inner_tab->addTab(m_autonum_element.widget, tr("Eléments"));
|
||||
autonum_inner_tab->addTab(m_autonum_folio.widget, tr("Folios"));
|
||||
autonum_layout->addWidget(autonum_inner_tab);
|
||||
tab_widget -> addTab (autonum_widget, tr("Numérotation auto"));
|
||||
}
|
||||
|
||||
QVBoxLayout *vlayout1 = new QVBoxLayout();
|
||||
vlayout1->addWidget(tab_widget);
|
||||
|
||||
@@ -230,6 +272,9 @@ void NewDiagramPage::applyConf()
|
||||
settings.setValue(QStringLiteral("color"), current_guides[i].color.name());
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
// save global auto-numbering defaults
|
||||
persistAutonumSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,6 +340,134 @@ void NewDiagramPage::loadSavedTbp()
|
||||
applyConf();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::isPlaceholder
|
||||
Return true if @a name matches the combo box's built-in placeholder text
|
||||
(first item). This is locale-independent because it reads the actual item text.
|
||||
*/
|
||||
bool NewDiagramPage::isPlaceholder(QComboBox *combo, const QString &name)
|
||||
{
|
||||
return !combo->count() || name == combo->itemText(0);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::initAutoNumTab
|
||||
Initialise an AutoNumTab struct and connect its signals.
|
||||
*/
|
||||
void NewDiagramPage::initAutoNumTab(AutoNumTab &tab, SelectAutonumW *w, const QString &prefix)
|
||||
{
|
||||
tab.widget = w;
|
||||
tab.prefix = prefix;
|
||||
|
||||
connect(w, &SelectAutonumW::applyPressed, this, [this, &tab]() { saveAutoNumContext(tab); });
|
||||
connect(w, &SelectAutonumW::removeClicked, this, [this, &tab]() { removeAutoNumContext(tab); });
|
||||
connect(w->contextComboBox(), &QComboBox::activated, this, [this, &tab](int index) {
|
||||
if (index >= 0) {
|
||||
QString name = tab.widget->contextComboBox()->itemText(index);
|
||||
if (tab.contexts.contains(name)) {
|
||||
tab.widget->setContext(tab.contexts.value(name));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::loadAutoNumTab
|
||||
Load saved rules from QSettings into an AutoNumTab.
|
||||
*/
|
||||
void NewDiagramPage::loadAutoNumTab(AutoNumTab &tab, QSettings &settings)
|
||||
{
|
||||
auto data = NumerotationContext::loadFromSettings(settings, tab.prefix);
|
||||
tab.contexts = data.first;
|
||||
for (auto it = tab.contexts.constBegin(); it != tab.contexts.constEnd(); ++it) {
|
||||
tab.widget->contextComboBox()->addItem(it.key());
|
||||
}
|
||||
if (!tab.contexts.isEmpty() && !data.second.isEmpty()
|
||||
&& tab.contexts.contains(data.second)) {
|
||||
tab.widget->contextComboBox()->setCurrentText(data.second);
|
||||
tab.widget->setContext(tab.contexts.value(data.second));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::saveAutoNumContext
|
||||
Save the current context from an AutoNumTab's widget into its hash and
|
||||
persist to QSettings immediately.
|
||||
*/
|
||||
void NewDiagramPage::saveAutoNumContext(AutoNumTab &tab)
|
||||
{
|
||||
QString name = tab.widget->contextComboBox()->currentText().trimmed();
|
||||
if (name.isEmpty() || isPlaceholder(tab.widget->contextComboBox(), name)) {
|
||||
return;
|
||||
}
|
||||
tab.contexts.insert(name, tab.widget->toNumContext());
|
||||
if (tab.widget->contextComboBox()->findText(name) == -1) {
|
||||
tab.widget->contextComboBox()->addItem(name);
|
||||
}
|
||||
persistAutonumSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::removeAutoNumContext
|
||||
Remove the current context from an AutoNumTab's hash and persist.
|
||||
*/
|
||||
void NewDiagramPage::removeAutoNumContext(AutoNumTab &tab)
|
||||
{
|
||||
QString name = tab.widget->contextComboBox()->currentText().trimmed();
|
||||
if (name.isEmpty() || isPlaceholder(tab.widget->contextComboBox(), name)) {
|
||||
return;
|
||||
}
|
||||
int idx = tab.widget->contextComboBox()->findText(name);
|
||||
if (idx == -1) return;
|
||||
tab.contexts.remove(name);
|
||||
tab.widget->contextComboBox()->removeItem(idx);
|
||||
tab.widget->contextComboBox()->setCurrentText(QString());
|
||||
tab.widget->setContext(NumerotationContext());
|
||||
persistAutonumSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::persistAutonumSettings
|
||||
Save all autonum contexts to QSettings immediately.
|
||||
*/
|
||||
void NewDiagramPage::persistAutonumSettings()
|
||||
{
|
||||
QSettings settings;
|
||||
for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) {
|
||||
QString current;
|
||||
QComboBox *combo = tab->widget->contextComboBox();
|
||||
if (!isPlaceholder(combo, combo->currentText().trimmed())
|
||||
&& tab->contexts.contains(combo->currentText().trimmed())) {
|
||||
current = combo->currentText().trimmed();
|
||||
}
|
||||
NumerotationContext::saveToSettings(tab->contexts, current,
|
||||
settings, tab->prefix);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::eventFilter
|
||||
Intercept Return/Enter in combo box line edits so it doesn't close the
|
||||
settings dialog.
|
||||
*/
|
||||
bool NewDiagramPage::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::KeyPress) {
|
||||
auto *ke = static_cast<QKeyEvent *>(event);
|
||||
if ((ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter)) {
|
||||
// Check if this is a line edit inside one of our autonum combos
|
||||
for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) {
|
||||
if (QComboBox *combo = tab->widget->contextComboBox()) {
|
||||
if (combo->lineEdit() && combo->lineEdit() == obj) {
|
||||
return true; // eat the event
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ConfigPage::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
/**
|
||||
Constructeur
|
||||
@param parent QWidget parent
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "configpage.h"
|
||||
#include "../projectpropertiesdialog.h"
|
||||
#include "../titleblockpropertieswidget.h"
|
||||
#include "../autoNum/numerotationcontext.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QtWidgets>
|
||||
@@ -32,6 +33,7 @@ class XRefPropertiesWidget;
|
||||
class GuidesPropertiesWidget;
|
||||
class QETProject;
|
||||
class TitleBlockProperties;
|
||||
class SelectAutonumW;
|
||||
|
||||
/**
|
||||
@brief The NewDiagramPage class
|
||||
@@ -48,6 +50,7 @@ class NewDiagramPage : public ConfigPage {
|
||||
~NewDiagramPage() override;
|
||||
private:
|
||||
NewDiagramPage(const NewDiagramPage &);
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
public slots:
|
||||
void changeToAutoFolioTab();
|
||||
void setFolioAutonum(QString);
|
||||
@@ -72,7 +75,25 @@ public slots:
|
||||
XRefPropertiesWidget *xrefpw; ///< Widget to edit default xref properties
|
||||
GuidesPropertiesWidget *m_gpw; ///< Widget to edit guides
|
||||
TitleBlockProperties savedTbp; ///< Used to save current TBP and retrieve later
|
||||
QTabWidget *m_tab_widget; ///< Main tab widget (stored for later access)
|
||||
|
||||
// auto-numbering tab data
|
||||
struct AutoNumTab {
|
||||
SelectAutonumW *widget = nullptr;
|
||||
QHash<QString, NumerotationContext> contexts;
|
||||
QString prefix;
|
||||
};
|
||||
AutoNumTab m_autonum_conductor;
|
||||
AutoNumTab m_autonum_element;
|
||||
AutoNumTab m_autonum_folio;
|
||||
|
||||
void initAutoNumTab(AutoNumTab &tab, SelectAutonumW *w, const QString &prefix);
|
||||
void loadAutoNumTab(AutoNumTab &tab, QSettings &settings);
|
||||
void saveAutoNumContext(AutoNumTab &tab);
|
||||
void removeAutoNumContext(AutoNumTab &tab);
|
||||
void persistAutonumSettings();
|
||||
|
||||
static bool isPlaceholder(QComboBox *combo, const QString &name);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "../../utils/qetsettings.h"
|
||||
#include "../../utils/qetutils.h"
|
||||
#include "../../qetmessagebox.h"
|
||||
#include "../nokde/kcolorbutton.h"
|
||||
#include <QFileDialog>
|
||||
#include <QFontDialog>
|
||||
#include <QSettings>
|
||||
@@ -74,6 +75,12 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
|
||||
ui->DiagramEditor_Grid_PointSize_min_sb->setValue(settings.value("diagrameditor/grid_pointsize_min", 1).toInt());
|
||||
ui->DiagramEditor_Grid_PointSize_max_sb->setValue(settings.value("diagrameditor/grid_pointsize_max", 1).toInt());
|
||||
ui->m_use_system_color_cb->setChecked(settings.value("usesystemcolors", "true").toBool());
|
||||
bool sysColors = ui->m_use_system_color_cb->isChecked();
|
||||
ui->m_custom_app_color_kpb->setEnabled(!sysColors);
|
||||
if (settings.contains("customapplicationcolor"))
|
||||
ui->m_custom_app_color_kpb->setColor(QColor(settings.value("customapplicationcolor").toString()));
|
||||
else
|
||||
ui->m_custom_app_color_kpb->setColor(QApplication::palette().color(QPalette::Window));
|
||||
bool tabbed = settings.value("diagrameditor/viewmode", "tabbed") == "tabbed";
|
||||
if(tabbed)
|
||||
ui->m_use_tab_mode_rb->setChecked(true);
|
||||
@@ -82,6 +89,25 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
|
||||
ui->m_zoom_out_beyond_folio->setChecked(settings.value("diagrameditor/zoom-out-beyond-of-folio", false).toBool());
|
||||
ui->m_use_gesture_trackpad->setChecked(settings.value("diagramview/gestures", false).toBool());
|
||||
ui->m_save_label_paste->setChecked(settings.value("diagramcommands/erase-label-on-copy", true).toBool());
|
||||
ui->m_enable_scripting->setChecked(QetSettings::scriptingEnabled());
|
||||
#ifdef QET_HAS_SCRIPTING
|
||||
if (QetSettings::scriptingForcedByEnvironment()) {
|
||||
//QET_ENABLE_SCRIPTING wins over the stored value, so let the box
|
||||
//say so rather than offer a tick that changes nothing.
|
||||
ui->m_enable_scripting->setEnabled(false);
|
||||
ui->m_enable_scripting->setToolTip(
|
||||
tr("Activé par la variable d'environnement "
|
||||
"QET_ENABLE_SCRIPTING ; ce réglage est sans effet "
|
||||
"tant qu'elle est définie."));
|
||||
}
|
||||
#else
|
||||
//Built without Qt Qml: there is no scripting to allow. Disabled as
|
||||
//well as hidden, so applyConf() leaves the stored value alone --
|
||||
//a hidden box still reports its state, and writing it here would
|
||||
//quietly clear a preference set on a build that does have Qml.
|
||||
ui->m_enable_scripting->setVisible(false);
|
||||
ui->m_enable_scripting->setEnabled(false);
|
||||
#endif
|
||||
ui->m_use_folio_label->setChecked(settings.value("genericpanel/folio", true).toBool());
|
||||
ui->m_border_0->setChecked(settings.value("border-columns_0", false).toBool());
|
||||
ui->m_autosave_sb->setValue(settings.value("diagrameditor/autosave-interval", 0).toInt());
|
||||
@@ -206,7 +232,17 @@ void GeneralConfigurationPage::applyConf()
|
||||
bool must_use_system_colors = ui->m_use_system_color_cb->isChecked();
|
||||
settings.setValue("usesystemcolors", must_use_system_colors);
|
||||
if (was_using_system_colors != must_use_system_colors) {
|
||||
QETApp::instance()->useSystemPalette(must_use_system_colors);
|
||||
if (must_use_system_colors) {
|
||||
QETApp::instance()->useSystemPalette(true);
|
||||
} else {
|
||||
QColor custom_color = ui->m_custom_app_color_kpb->color();
|
||||
settings.setValue("customapplicationcolor", custom_color.name());
|
||||
QETApp::instance()->useCustomPalette(custom_color);
|
||||
}
|
||||
} else if (!must_use_system_colors) {
|
||||
QColor custom_color = ui->m_custom_app_color_kpb->color();
|
||||
settings.setValue("customapplicationcolor", custom_color.name());
|
||||
QETApp::instance()->useCustomPalette(custom_color);
|
||||
}
|
||||
settings.setValue("border-columns_0",ui->m_border_0->isChecked());
|
||||
settings.setValue("lang", ui->m_lang_cb->itemData(ui->m_lang_cb->currentIndex()).toString());
|
||||
@@ -227,6 +263,14 @@ void GeneralConfigurationPage::applyConf()
|
||||
//DIAGRAM COMMAND
|
||||
settings.setValue("diagramcommands/erase-label-on-copy", ui->m_save_label_paste->isChecked());
|
||||
|
||||
//SCRIPTING
|
||||
//Left alone while the environment forces it on: the box is disabled
|
||||
//in that case and writing its state would silently clear the user's
|
||||
//real preference the first time this dialog is accepted.
|
||||
if (ui->m_enable_scripting->isEnabled()) {
|
||||
QetSettings::setScriptingEnabled(ui->m_enable_scripting->isChecked());
|
||||
}
|
||||
|
||||
//GENERIC PANEL
|
||||
settings.setValue("genericpanel/folio",ui->m_use_folio_label->isChecked());
|
||||
|
||||
@@ -625,3 +669,14 @@ void GeneralConfigurationPage::on_m_hdpi_round_cb_clicked(bool checked)
|
||||
ui->m_hdpi_round_policy_cb->setEnabled(checked);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief GeneralConfigurationPage::on_m_use_system_color_cb_toggled
|
||||
Enable/disable the custom color picker when the system color
|
||||
checkbox is toggled.
|
||||
@param checked
|
||||
*/
|
||||
void GeneralConfigurationPage::on_m_use_system_color_cb_toggled(bool checked)
|
||||
{
|
||||
ui->m_custom_app_color_kpb->setEnabled(!checked);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ class GeneralConfigurationPage : public ConfigPage
|
||||
void on_ElementEditor_Grid_PointSize_min_sb_valueChanged(int value);
|
||||
|
||||
void on_m_hdpi_round_cb_clicked(bool checked);
|
||||
void on_m_use_system_color_cb_toggled(bool checked);
|
||||
|
||||
private:
|
||||
void fillLang();
|
||||
|
||||
@@ -23,15 +23,26 @@
|
||||
<attribute name="title">
|
||||
<string>Apparence</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="m_use_system_color_cb">
|
||||
<property name="text">
|
||||
<string>Utiliser les couleurs du système</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="m_system_color_layout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="m_use_system_color_cb">
|
||||
<property name="text">
|
||||
<string>Utiliser les couleurs du système</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="KColorButton" name="m_custom_app_color_kpb">
|
||||
<property name="toolTip">
|
||||
<string>Couleur de l'application</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
@@ -207,6 +218,16 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QCheckBox" name="m_enable_scripting">
|
||||
<property name="text">
|
||||
<string>Autoriser l'exécution de scripts JavaScript (Projet > Exécuter un script, et --run)</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Un script s'exécute avec vos droits : il peut lire et modifier le projet ouvert et écrire des fichiers. Désactivé par défaut ; n'exécutez que des scripts dont vous connaissez l'origine.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@@ -1161,4 +1182,11 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>KColorButton</class>
|
||||
<extends>QPushButton</extends>
|
||||
<header>nokde/kcolorbutton.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
</ui>
|
||||
|
||||
@@ -381,5 +381,5 @@ QString ShortcutsConfigPage::title() const
|
||||
|
||||
QIcon ShortcutsConfigPage::icon() const
|
||||
{
|
||||
return QET::Icons::ConfigureToolbars;
|
||||
return QET::Icons::ConfigureShortcuts;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
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 "diagrambgcolorbutton.h"
|
||||
|
||||
#include "../diagram.h"
|
||||
#include "../diagramview.h"
|
||||
#include "../palettegraphicsview.h"
|
||||
#include "../qetdiagrameditor.h"
|
||||
#include "../projectview.h"
|
||||
#include "../qetproject.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QColorDialog>
|
||||
#include <QMenu>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
|
||||
namespace {
|
||||
struct NamedColor { const char *context_name; QColor color; };
|
||||
|
||||
QList<NamedColor> standardColors()
|
||||
{
|
||||
return {
|
||||
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Blanc"), QColor(0xFF, 0xFF, 0xFF)},
|
||||
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Blanc cassé"), QColor(0xFD, 0xFB, 0xF5)},
|
||||
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Gris clair"), QColor(0xE0, 0xE0, 0xE0)},
|
||||
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Gris"), QColor(0x80, 0x80, 0x80)},
|
||||
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Gris foncé"), QColor(0x40, 0x40, 0x40)},
|
||||
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Noir"), QColor(0x00, 0x00, 0x00)},
|
||||
};
|
||||
}
|
||||
|
||||
const int MAX_RECENT = 6;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::DiagramBgColorToolButton
|
||||
@param editor : the diagram editor this button acts on
|
||||
@param parent
|
||||
*/
|
||||
DiagramBgColorToolButton::DiagramBgColorToolButton(QETDiagramEditor *editor, QWidget *parent) :
|
||||
QToolButton(parent),
|
||||
m_editor(editor)
|
||||
{
|
||||
setPopupMode(QToolButton::InstantPopup);
|
||||
setToolTip(tr("Couleur de fond du folio"));
|
||||
setStatusTip(tr("Choisir la couleur de fond du folio",
|
||||
"status bar tip"));
|
||||
|
||||
m_is_system_color = true;
|
||||
m_current = QApplication::palette().color(QPalette::Base);
|
||||
setMenu(new QMenu(this));
|
||||
rebuildMenu();
|
||||
setSwatch(m_current);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::rebuildMenu
|
||||
*/
|
||||
void DiagramBgColorToolButton::rebuildMenu()
|
||||
{
|
||||
QMenu *m = menu();
|
||||
m->clear();
|
||||
|
||||
QAction *sys = m->addAction(tr("Couleur système"));
|
||||
connect(sys, &QAction::triggered, this, &DiagramBgColorToolButton::applySystemColor);
|
||||
m->addSeparator();
|
||||
|
||||
for (const auto &nc : standardColors())
|
||||
{
|
||||
const QColor c = nc.color;
|
||||
QAction *a = m->addAction(swatchIcon(c),
|
||||
tr(nc.context_name));
|
||||
connect(a, &QAction::triggered, this, [this, c]() { applyColor(c); });
|
||||
}
|
||||
|
||||
if (!m_recent.isEmpty())
|
||||
{
|
||||
m->addSeparator();
|
||||
QAction *title = m->addAction(tr("Récemment utilisées"));
|
||||
title->setEnabled(false);
|
||||
for (const QColor &c : std::as_const(m_recent))
|
||||
{
|
||||
QAction *a = m->addAction(swatchIcon(c), c.name());
|
||||
connect(a, &QAction::triggered, this, [this, c]() { applyColor(c); });
|
||||
}
|
||||
}
|
||||
|
||||
m->addSeparator();
|
||||
QAction *other = m->addAction(tr("Autre couleur…"));
|
||||
connect(other, &QAction::triggered, this, &DiagramBgColorToolButton::chooseOtherColor);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::applyColor
|
||||
Set a custom background color on all open diagrams.
|
||||
@param color
|
||||
*/
|
||||
void DiagramBgColorToolButton::applyColor(const QColor &color)
|
||||
{
|
||||
if (!color.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_is_system_color = false;
|
||||
rememberRecent(color);
|
||||
setSwatch(color);
|
||||
|
||||
PaletteGraphicsView::setCustomBackgroundColor(true);
|
||||
Diagram::background_color = color;
|
||||
|
||||
QETDiagramEditor *editor = m_editor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
for (ProjectView *pv : editor->openedProjects())
|
||||
for (Diagram *d : pv->project()->diagrams())
|
||||
d->update();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::applySystemColor
|
||||
Restore the system-default background and re-enable dark-mode
|
||||
inversion.
|
||||
*/
|
||||
void DiagramBgColorToolButton::applySystemColor()
|
||||
{
|
||||
m_is_system_color = true;
|
||||
m_current = QApplication::palette().color(QPalette::Base);
|
||||
setSwatch(m_current);
|
||||
|
||||
PaletteGraphicsView::setCustomBackgroundColor(false);
|
||||
Diagram::background_color = Qt::white;
|
||||
|
||||
QETDiagramEditor *editor = m_editor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
for (ProjectView *pv : editor->openedProjects())
|
||||
for (Diagram *d : pv->project()->diagrams())
|
||||
d->update();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::chooseOtherColor
|
||||
*/
|
||||
void DiagramBgColorToolButton::chooseOtherColor()
|
||||
{
|
||||
const QColor c = QColorDialog::getColor(m_current, this,
|
||||
tr("Choisir une couleur de fond"));
|
||||
if (c.isValid()) {
|
||||
applyColor(c);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::rememberRecent
|
||||
Most recent first, no duplicates, capped.
|
||||
@param color
|
||||
*/
|
||||
void DiagramBgColorToolButton::rememberRecent(const QColor &color)
|
||||
{
|
||||
for (const auto &nc : standardColors()) {
|
||||
if (nc.color == color) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_recent.removeAll(color);
|
||||
m_recent.prepend(color);
|
||||
while (m_recent.size() > MAX_RECENT) {
|
||||
m_recent.removeLast();
|
||||
}
|
||||
rebuildMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::setSwatch
|
||||
@param color
|
||||
*/
|
||||
void DiagramBgColorToolButton::setSwatch(const QColor &color)
|
||||
{
|
||||
m_current = color;
|
||||
setIcon(swatchIcon(color));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::syncFromDiagram
|
||||
Sync the button swatch to whatever Diagram::background_color is
|
||||
currently set to. Called when the active folio changes.
|
||||
*/
|
||||
void DiagramBgColorToolButton::syncFromDiagram()
|
||||
{
|
||||
if (m_is_system_color) {
|
||||
m_current = QApplication::palette().color(QPalette::Base);
|
||||
} else {
|
||||
m_current = Diagram::background_color;
|
||||
}
|
||||
setSwatch(m_current);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::updateEnabledState
|
||||
*/
|
||||
void DiagramBgColorToolButton::updateEnabledState()
|
||||
{
|
||||
setEnabled(m_editor && m_editor->currentProjectView());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::swatchIcon
|
||||
@param color
|
||||
@return a plain square of that colour, outlined so that white and very
|
||||
light colours are still visible against the toolbar.
|
||||
*/
|
||||
QIcon DiagramBgColorToolButton::swatchIcon(const QColor &color)
|
||||
{
|
||||
QPixmap pix(16, 16);
|
||||
pix.fill(Qt::transparent);
|
||||
QPainter p(&pix);
|
||||
p.setRenderHint(QPainter::Antialiasing, false);
|
||||
p.setBrush(color);
|
||||
p.setPen(QPen(QColor(0x40, 0x40, 0x40), 1));
|
||||
p.drawRect(0, 0, 15, 15);
|
||||
p.end();
|
||||
return QIcon(pix);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
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 DIAGRAMBGCOLORTOOLBUTTON_H
|
||||
#define DIAGRAMBGCOLORTOOLBUTTON_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QList>
|
||||
#include <QToolButton>
|
||||
|
||||
class QETDiagramEditor;
|
||||
|
||||
/**
|
||||
@brief The DiagramBgColorToolButton class
|
||||
Color picker for the diagram sheet background, placed in the
|
||||
"Affichage" toolbar. Mirrors the ConductorColorToolButton UX:
|
||||
preset colors in the dropdown, "Autre couleur..." at the bottom,
|
||||
and a swatch icon on the button itself.
|
||||
|
||||
Picking "Couleur système" clears any custom colour and lets the
|
||||
dark-mode inversion handle the background as before.
|
||||
*/
|
||||
class DiagramBgColorToolButton : public QToolButton
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DiagramBgColorToolButton(QETDiagramEditor *editor,
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
public slots:
|
||||
void updateEnabledState();
|
||||
void syncFromDiagram();
|
||||
|
||||
private:
|
||||
void rebuildMenu();
|
||||
void applyColor(const QColor &color);
|
||||
void applySystemColor();
|
||||
void chooseOtherColor();
|
||||
void rememberRecent(const QColor &color);
|
||||
void setSwatch(const QColor &color);
|
||||
static QIcon swatchIcon(const QColor &color);
|
||||
|
||||
QETDiagramEditor *m_editor = nullptr;
|
||||
QList<QColor> m_recent;
|
||||
QColor m_current;
|
||||
bool m_is_system_color = false;
|
||||
};
|
||||
|
||||
#endif // DIAGRAMBGCOLORTOOLBUTTON_H
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
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 "duplicateoffsetdialog.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QSettings>
|
||||
#include <QSpinBox>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace {
|
||||
// Stored as one integer per axis, in grid steps -- not pixels, so the
|
||||
// remembered offset still makes sense if the grid size ever changes.
|
||||
const QString kOffsetXKey = QStringLiteral("diagrameditor/duplicate_offset_x");
|
||||
const QString kOffsetYKey = QStringLiteral("diagrameditor/duplicate_offset_y");
|
||||
}
|
||||
|
||||
DuplicateOffsetDialog::DuplicateOffsetDialog(QWidget *parent) :
|
||||
QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Dupliquer"));
|
||||
|
||||
auto *form = new QFormLayout;
|
||||
|
||||
m_spacing = new QSpinBox(this);
|
||||
m_spacing->setRange(1, 1000);
|
||||
m_spacing->setSuffix(tr(" pas de grille"));
|
||||
form->addRow(tr("Espacement :"), m_spacing);
|
||||
|
||||
m_direction = new QComboBox(this);
|
||||
// Order matches the Direction enum, so currentIndex() can be used
|
||||
// directly wherever Direction is needed.
|
||||
m_direction->addItem(tr("Haut"));
|
||||
m_direction->addItem(tr("Bas"));
|
||||
m_direction->addItem(tr("Gauche"));
|
||||
m_direction->addItem(tr("Droite"));
|
||||
form->addRow(tr("Direction :"), m_direction);
|
||||
|
||||
const QPoint saved = savedStepOffset();
|
||||
// The saved value is a signed (dx, dy) pair, not itself a
|
||||
// spacing+direction pair, so it has to be decomposed back into the
|
||||
// two the dialog shows. Exactly one axis is ever non-zero (see
|
||||
// stepOffset()), so whichever one is picks the direction; a value
|
||||
// that somehow has neither (only possible if QSettings was hand-
|
||||
// edited) falls back to the same default stepOffset() would.
|
||||
int spacing = 1;
|
||||
Direction direction = Right;
|
||||
if (saved.x() > 0) { direction = Right; spacing = saved.x(); }
|
||||
else if (saved.x() < 0) { direction = Left; spacing = -saved.x(); }
|
||||
else if (saved.y() > 0) { direction = Down; spacing = saved.y(); }
|
||||
else if (saved.y() < 0) { direction = Up; spacing = -saved.y(); }
|
||||
m_spacing->setValue(spacing);
|
||||
m_direction->setCurrentIndex(static_cast<int>(direction));
|
||||
|
||||
auto *buttons = new QDialogButtonBox(
|
||||
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addLayout(form);
|
||||
layout->addWidget(buttons);
|
||||
}
|
||||
|
||||
QPoint DuplicateOffsetDialog::stepOffset() const
|
||||
{
|
||||
const int spacing = m_spacing->value();
|
||||
switch (static_cast<Direction>(m_direction->currentIndex())) {
|
||||
case Up: return QPoint(0, -spacing);
|
||||
case Down: return QPoint(0, spacing);
|
||||
case Left: return QPoint(-spacing, 0);
|
||||
case Right: return QPoint(spacing, 0);
|
||||
}
|
||||
return QPoint(spacing, 0); // unreachable; keeps -Wreturn-type quiet
|
||||
}
|
||||
|
||||
QPoint DuplicateOffsetDialog::savedStepOffset()
|
||||
{
|
||||
QSettings settings;
|
||||
if (!hasSavedStepOffset()) {
|
||||
return QPoint(1, 0); // default: one grid step to the right
|
||||
}
|
||||
return QPoint(settings.value(kOffsetXKey).toInt(),
|
||||
settings.value(kOffsetYKey).toInt());
|
||||
}
|
||||
|
||||
void DuplicateOffsetDialog::saveStepOffset(const QPoint &steps)
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue(kOffsetXKey, steps.x());
|
||||
settings.setValue(kOffsetYKey, steps.y());
|
||||
}
|
||||
|
||||
bool DuplicateOffsetDialog::hasSavedStepOffset()
|
||||
{
|
||||
QSettings settings;
|
||||
return settings.contains(kOffsetXKey) && settings.contains(kOffsetYKey);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
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 DUPLICATEOFFSETDIALOG_H
|
||||
#define DUPLICATEOFFSETDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPoint>
|
||||
|
||||
class QSpinBox;
|
||||
class QComboBox;
|
||||
|
||||
/**
|
||||
@brief The DuplicateOffsetDialog class
|
||||
Asks how far, and in which of the four cardinal directions, Ctrl+D
|
||||
(DiagramView::duplicate(), bugtracker #991) should offset a copy from
|
||||
its source. Shown once, then remembered: the answer is stored in
|
||||
QSettings and reused by every later Ctrl+D press without asking
|
||||
again, until this dialog is reopened deliberately.
|
||||
*/
|
||||
class DuplicateOffsetDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Direction { Up, Down, Left, Right };
|
||||
|
||||
explicit DuplicateOffsetDialog(QWidget *parent = nullptr);
|
||||
|
||||
/// The offset in grid steps, positive along X to the right
|
||||
/// and positive along Y downward -- QET's own scene axes,
|
||||
/// matching the sign convention setPos() already uses
|
||||
/// everywhere else in this codebase.
|
||||
QPoint stepOffset() const;
|
||||
|
||||
/// Reads the last-confirmed spacing/direction from QSettings,
|
||||
/// or the default (1 step, right) if none was ever set.
|
||||
static QPoint savedStepOffset();
|
||||
/// Writes @p steps to QSettings, in the same X/Y convention
|
||||
/// as stepOffset().
|
||||
static void saveStepOffset(const QPoint &steps);
|
||||
/// Whether a direction/spacing has already been confirmed
|
||||
/// once, i.e. whether Ctrl+D can skip the dialog.
|
||||
static bool hasSavedStepOffset();
|
||||
|
||||
private:
|
||||
QSpinBox *m_spacing = nullptr;
|
||||
QComboBox *m_direction = nullptr;
|
||||
};
|
||||
|
||||
#endif // DUPLICATEOFFSETDIALOG_H
|
||||
@@ -314,31 +314,15 @@ void LinkElementCommand::redo()
|
||||
if(m_element->diagram()) m_element->diagram()->showMe();
|
||||
makeLink(m_linked_after);
|
||||
|
||||
//If the action is to link two reports together, we check if the conductors
|
||||
//of the new potential have the same text, function, and protocol.
|
||||
//if not, a dialog ask what do to.
|
||||
//If the action is to link two reports together, and the conductors
|
||||
//of the new potential disagree on a property that matters, a
|
||||
//dialog asks what to do. See reportLinkNeedsPotentialChoice() for
|
||||
//what "disagree" checks and the bug fixed there (bugtracker #974).
|
||||
if (m_first_redo && (m_element->linkType() & Element::AllReport) \
|
||||
&& m_element->conductors().size() \
|
||||
&& m_linked_after.size() && m_linked_after.first()->conductors().size())
|
||||
{
|
||||
//fill list of potential
|
||||
QSet <Conductor *> c_list = m_element->conductors().first()->relatedPotentialConductors();
|
||||
c_list << m_element->conductors().first();
|
||||
//fill list of text
|
||||
QStringList str_txt;
|
||||
QStringList str_funct;
|
||||
QStringList str_tens;
|
||||
for (const Conductor *c : c_list)
|
||||
{
|
||||
str_txt << c->properties().text;
|
||||
str_funct << c->properties().m_function;
|
||||
str_tens << c->properties().m_tension_protocol;
|
||||
str_tens << c->properties().m_wire_color;
|
||||
str_tens << c->properties().m_wire_section;
|
||||
}
|
||||
|
||||
//check text list, isn't same in potential, ask user what to do
|
||||
if (!QET::eachStrIsEqual(str_txt) || !QET::eachStrIsEqual(str_funct) || !QET::eachStrIsEqual(str_tens))
|
||||
if (reportLinkNeedsPotentialChoice(m_element, m_linked_after.first()))
|
||||
{
|
||||
PotentialSelectorDialog psd(m_element, this);
|
||||
psd.exec();
|
||||
@@ -348,6 +332,64 @@ void LinkElementCommand::redo()
|
||||
QUndoCommand::redo();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LinkElementCommand::reportLinkNeedsPotentialChoice
|
||||
Whether linking these two report elements (next_report/previous_report)
|
||||
would pop PotentialSelectorDialog -- i.e. whether their conductors (if
|
||||
any exist yet, on either side) disagree on a property redo() cares
|
||||
about. Exposed as its own static method, rather than left inline in
|
||||
redo(), for the same reason ConductorCreator::needsPotentialChoice()
|
||||
is: a caller with nobody there to answer a modal dialog (the scripting
|
||||
API) can check first and decline, and the condition cannot drift away
|
||||
from the one redo() actually applies.
|
||||
|
||||
Bug fixed here (bugtracker #974): the original check built ONE
|
||||
combined list from three unrelated fields (tension_protocol,
|
||||
wire_color, wire_section) and tested that whole list for equality --
|
||||
comparing a tension-protocol string against a wire-colour string is
|
||||
never equal even when each field individually matches across every
|
||||
conductor, and wire_color/wire_section are ConductorProperties::
|
||||
m_wire_color/m_wire_section, a separate free-text documentation pair
|
||||
that says nothing about how the wire is actually drawn (that is
|
||||
"color"/"style"). Net effect: the dialog could not reliably detect a
|
||||
real mismatch, including the exact case #974 reported -- two
|
||||
report-linked conductors drawn in different colours -- and could just
|
||||
as easily fire on conductors that matched in every way that mattered.
|
||||
Comparing each relevant field (text/num, function, tension protocol,
|
||||
colour, line style) on its own fixes both.
|
||||
|
||||
@param element_a @param element_b the two elements about to be (or
|
||||
already) linked; order does not matter
|
||||
@return true if the dialog would (or does) open
|
||||
*/
|
||||
bool LinkElementCommand::reportLinkNeedsPotentialChoice(Element *element_a, Element *element_b)
|
||||
{
|
||||
if (!element_a || !element_b) return false;
|
||||
if (element_a->conductors().isEmpty() || element_b->conductors().isEmpty()) return false;
|
||||
|
||||
QSet<Conductor *> c_list;
|
||||
for (Element *e : {element_a, element_b})
|
||||
{
|
||||
if (e->conductors().isEmpty()) continue;
|
||||
c_list << e->conductors().first();
|
||||
c_list += e->conductors().first()->relatedPotentialConductors();
|
||||
}
|
||||
if (c_list.size() < 2) return false;
|
||||
|
||||
QStringList str_txt, str_funct, str_tens, str_color, str_style;
|
||||
for (const Conductor *c : std::as_const(c_list))
|
||||
{
|
||||
str_txt << c->properties().text;
|
||||
str_funct << c->properties().m_function;
|
||||
str_tens << c->properties().m_tension_protocol;
|
||||
str_color << c->properties().color.name();
|
||||
str_style << QString::number(int(c->properties().style));
|
||||
}
|
||||
return !QET::eachStrIsEqual(str_txt) || !QET::eachStrIsEqual(str_funct)
|
||||
|| !QET::eachStrIsEqual(str_tens) || !QET::eachStrIsEqual(str_color)
|
||||
|| !QET::eachStrIsEqual(str_style);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LinkElementCommand::setUpNewLink
|
||||
Update the content of m_link_after with the content of element_list.
|
||||
|
||||
@@ -38,6 +38,7 @@ class LinkElementCommand : public QUndoCommand
|
||||
bool mergeWith(const QUndoCommand *other) override;
|
||||
|
||||
static bool isLinkable (Element *element_a, Element *element_b, bool already_linked = false);
|
||||
static bool reportLinkNeedsPotentialChoice(Element *element_a, Element *element_b);
|
||||
|
||||
void setLink (const QList<Element *>& element_list);
|
||||
void setLink (Element *element_);
|
||||
|
||||
@@ -55,21 +55,13 @@ m_diagram(diagram)
|
||||
{
|
||||
openDialog();
|
||||
|
||||
QString text;
|
||||
if(texts_list.count())
|
||||
text.append(QObject::tr("Pivoter %1 textes").arg(texts_list.count()));
|
||||
if(groups_list.count())
|
||||
{
|
||||
if(text.isEmpty())
|
||||
text.append(QObject::tr("Pivoter"));
|
||||
else
|
||||
text.append(QObject::tr(" et"));
|
||||
|
||||
text.append(QObject::tr(" %1 groupes de textes").arg(groups_list.count()));
|
||||
}
|
||||
if(!text.isNull())
|
||||
setText(text);
|
||||
|
||||
QStringList parts;
|
||||
if (texts_list.count())
|
||||
parts << QObject::tr("%n texte(s)", "", texts_list.count());
|
||||
if (groups_list.count())
|
||||
parts << QObject::tr("%n groupe(s) de textes", "", groups_list.count());
|
||||
setText(QObject::tr("Pivoter %1").arg(QLocale(QETApp::interfaceLanguage()).createSeparatedList(parts)));
|
||||
|
||||
for(DiagramTextItem *dti : texts_list)
|
||||
setupAnimation(dti, "rotation", dti->rotation(), m_rotation);
|
||||
for(ElementTextItemGroup *grp : groups_list)
|
||||
@@ -77,7 +69,6 @@ m_diagram(diagram)
|
||||
}
|
||||
else
|
||||
setObsolete(true);
|
||||
|
||||
}
|
||||
|
||||
void RotateTextsCommand::undo()
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
#include "conductorcreator.h"
|
||||
|
||||
#include "../conductorautonumerotation.h"
|
||||
#include "../dataBase/projectdatabase.h"
|
||||
#include "../diagram.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../undocommand/addgraphicsobjectcommand.h"
|
||||
#include "../qetgraphicsitem/conductor.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
@@ -68,6 +70,15 @@ ConductorCreator::ConductorCreator(Diagram *d, QList<Terminal *> terminals_list)
|
||||
|
||||
for(Conductor *c : c_list) {
|
||||
c->refreshText();
|
||||
//refreshText() resolves an auto-numbering formula into
|
||||
//properties.text without emitting propertiesChange, which is
|
||||
//what the project database listens to. The row was inserted
|
||||
//while text was still the raw formula ("W%sequ_1"), so without
|
||||
//this the wiring list and BOM read the formula, not "W1",
|
||||
//until something forces a full rebuild.
|
||||
if (d->project() && d->project()->dataBase()) {
|
||||
d->project()->dataBase()->updateConductor(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +106,29 @@ void ConductorCreator::create(Diagram *d, const QPolygonF &polygon)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorCreator::needsPotentialChoice
|
||||
Whether creating a potential between these terminals would ask the user
|
||||
to choose which of several existing potentials to inherit from -- that
|
||||
is, whether the constructor would reach PotentialSelectorDialog.
|
||||
|
||||
This exists for callers with nobody there to answer: the dialog is a
|
||||
plain QDialog::exec(), not routed through QET::QetMessageBox, so its
|
||||
non-interactive mode does not cover it and a headless caller would hang
|
||||
on it indefinitely. Such a caller can check this first and decline.
|
||||
Exposed here, rather than reimplemented by the caller, so the condition
|
||||
cannot drift away from the one setUpPropertieToUse() actually applies.
|
||||
@param terminals_list the terminals a potential would be created between
|
||||
@return true if the constructor would open the dialog
|
||||
*/
|
||||
bool ConductorCreator::needsPotentialChoice(const QList<Terminal *> &terminals_list)
|
||||
{
|
||||
if (terminals_list.size() <= 1) {
|
||||
return false;
|
||||
}
|
||||
return existingPotential(terminals_list).size() >= 2;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorCreator::propertieToUse
|
||||
@return true if the caller should proceed with conductor creation,
|
||||
@@ -104,7 +138,7 @@ void ConductorCreator::create(Diagram *d, const QPolygonF &polygon)
|
||||
*/
|
||||
bool ConductorCreator::setUpPropertieToUse()
|
||||
{
|
||||
QList<Conductor *> potentials = existingPotential();
|
||||
QList<Conductor *> potentials = existingPotential(m_terminals_list);
|
||||
|
||||
//There is an existing potential
|
||||
//we get one of them
|
||||
@@ -145,14 +179,15 @@ bool ConductorCreator::setUpPropertieToUse()
|
||||
@brief ConductorCreator::existingPotential
|
||||
Return the list of existing potential of
|
||||
the terminal list
|
||||
@param terminals_list the terminals to inspect
|
||||
@return c_list QList<Conductor *>
|
||||
*/
|
||||
QList<Conductor *> ConductorCreator::existingPotential()
|
||||
QList<Conductor *> ConductorCreator::existingPotential(const QList<Terminal *> &terminals_list)
|
||||
{
|
||||
QList<Conductor *> c_list;
|
||||
QList<Terminal *> t_exclude;
|
||||
|
||||
for (Terminal *t : m_terminals_list)
|
||||
for (Terminal *t : terminals_list)
|
||||
{
|
||||
if (t_exclude.contains(t)) {
|
||||
continue;
|
||||
@@ -166,9 +201,9 @@ QList<Conductor *> ConductorCreator::existingPotential()
|
||||
//in the same potential of c, and if true, exclude this terminal from the search.
|
||||
for (Conductor *c : t->conductors().first()->relatedPotentialConductors(false))
|
||||
{
|
||||
if (m_terminals_list.contains(c->terminal1)) {
|
||||
if (terminals_list.contains(c->terminal1)) {
|
||||
t_exclude.append(c->terminal1);
|
||||
} else if (m_terminals_list.contains(c->terminal2)) {
|
||||
} else if (terminals_list.contains(c->terminal2)) {
|
||||
t_exclude.append(c->terminal2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +38,11 @@ class ConductorCreator
|
||||
public:
|
||||
ConductorCreator(Diagram *d, QList<Terminal *> terminals_list);
|
||||
static void create(Diagram *d, const QPolygonF &polygon);
|
||||
|
||||
static bool needsPotentialChoice(const QList<Terminal *> &terminals_list);
|
||||
|
||||
private:
|
||||
static QList<Conductor *> existingPotential(const QList<Terminal *> &terminals_list);
|
||||
bool setUpPropertieToUse();
|
||||
QList<Conductor *> existingPotential();
|
||||
Terminal *hubTerminal();
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "qetsettings.h"
|
||||
#include <QSettings>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
|
||||
namespace QetSettings
|
||||
{
|
||||
@@ -105,4 +106,50 @@ namespace QetSettings
|
||||
return default_policy;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief scriptingForcedByEnvironment
|
||||
* @return true if QET_ENABLE_SCRIPTING is set to 1 in the environment.
|
||||
*
|
||||
* The way to turn scripting on where there is nobody to tick a box:
|
||||
* a headless run, a CI job, a build server. Those have no settings
|
||||
* file worth writing to -- and on a machine whose HOME is created
|
||||
* fresh for the run, writing one would not survive anyway.
|
||||
*/
|
||||
bool scriptingForcedByEnvironment()
|
||||
{
|
||||
return qgetenv("QET_ENABLE_SCRIPTING") == QByteArray("1");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief scriptingEnabled
|
||||
* @return whether QElectroTech may run a JavaScript script.
|
||||
*
|
||||
* Off unless the user turned it on. A script reaches the whole project
|
||||
* and the filesystem through the export calls, so it is capability the
|
||||
* great majority of users never asked for; leaving it on by default
|
||||
* would hand it to them anyway. @sa setScriptingEnabled
|
||||
*
|
||||
* The environment override wins over the stored value, and is checked
|
||||
* first so that a machine with no settings at all still answers.
|
||||
*/
|
||||
bool scriptingEnabled()
|
||||
{
|
||||
if (scriptingForcedByEnvironment()) {
|
||||
return true;
|
||||
}
|
||||
QSettings settings;
|
||||
return settings.value("scripting/enabled", false).toBool();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief setScriptingEnabled
|
||||
* Store whether scripting is allowed. @sa scriptingEnabled
|
||||
* @param enabled
|
||||
*/
|
||||
void setScriptingEnabled(bool enabled)
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue("scripting/enabled", enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ namespace QetSettings
|
||||
void setHdpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy policy);
|
||||
Qt::HighDpiScaleFactorRoundingPolicy hdpiScaleFactorRoundingPolicy(
|
||||
Qt::HighDpiScaleFactorRoundingPolicy default_policy = Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
||||
|
||||
bool scriptingEnabled();
|
||||
void setScriptingEnabled(bool enabled);
|
||||
bool scriptingForcedByEnvironment();
|
||||
}
|
||||
|
||||
#endif // QETSETTINGS_H
|
||||
|
||||
@@ -25,10 +25,6 @@ message(". PROJECT_SOURCE_DIR :" ${PROJECT_SOURCE_DIR})
|
||||
# Add sub directories
|
||||
message(". Add sub directory catch")
|
||||
add_subdirectory(catch)
|
||||
message(". Add sub directory googletest")
|
||||
add_subdirectory(googletest)
|
||||
message(". Add sub directory googlemock")
|
||||
add_subdirectory(googlemock)
|
||||
message(". Add sub directory modal-quit-regression")
|
||||
add_subdirectory(modal-quit-regression)
|
||||
message(". Add sub directory qttest")
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
BasedOnStyle: LLVM
|
||||
AlignAfterOpenBracket: AlwaysBreak
|
||||
AlignConsecutiveMacros: 'true'
|
||||
AlignConsecutiveAssignments: 'true'
|
||||
AlignConsecutiveDeclarations: 'true'
|
||||
AlignEscapedNewlines: Right
|
||||
AlignOperands: 'true'
|
||||
AlignTrailingComments: 'true'
|
||||
AllowAllArgumentsOnNextLine: 'false'
|
||||
AllowAllConstructorInitializersOnNextLine: 'true'
|
||||
AllowAllParametersOfDeclarationOnNextLine: 'true'
|
||||
AllowShortBlocksOnASingleLine: 'true'
|
||||
AllowShortCaseLabelsOnASingleLine: 'true'
|
||||
AllowShortFunctionsOnASingleLine: All
|
||||
AllowShortIfStatementsOnASingleLine: Always
|
||||
AllowShortLambdasOnASingleLine: All
|
||||
AllowShortLoopsOnASingleLine: 'true'
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: 'true'
|
||||
AlwaysBreakTemplateDeclarations: 'Yes'
|
||||
BinPackArguments: 'false'
|
||||
BinPackParameters: 'false'
|
||||
BreakAfterJavaFieldAnnotations: 'true'
|
||||
BreakBeforeBinaryOperators: NonAssignment
|
||||
BreakBeforeBraces: Allman
|
||||
BreakBeforeTernaryOperators: 'false'
|
||||
BreakConstructorInitializers: AfterColon
|
||||
BreakInheritanceList: AfterColon
|
||||
BreakStringLiterals: 'true'
|
||||
ColumnLimit: '80'
|
||||
CompactNamespaces: 'false'
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: 'true'
|
||||
Cpp11BracedListStyle: 'true'
|
||||
FixNamespaceComments: 'true'
|
||||
IncludeBlocks: Regroup
|
||||
IndentCaseLabels: 'false'
|
||||
IndentPPDirectives: AfterHash
|
||||
IndentWidth: '4'
|
||||
JavaScriptWrapImports: 'true'
|
||||
Language: Cpp
|
||||
MaxEmptyLinesToKeep: '1'
|
||||
NamespaceIndentation: All
|
||||
PointerAlignment: Left
|
||||
ReflowComments: 'true'
|
||||
SortIncludes: 'true'
|
||||
SortUsingDeclarations: 'true'
|
||||
SpaceAfterCStyleCast: 'true'
|
||||
SpaceAfterLogicalNot: 'true'
|
||||
SpaceAfterTemplateKeyword: 'true'
|
||||
SpaceBeforeAssignmentOperators: 'true'
|
||||
SpaceBeforeCpp11BracedList: 'true'
|
||||
SpaceBeforeCtorInitializerColon: 'true'
|
||||
SpaceBeforeInheritanceColon: 'true'
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceBeforeRangeBasedForLoopColon: 'true'
|
||||
SpaceInEmptyParentheses: 'false'
|
||||
SpacesInAngles: 'false'
|
||||
SpacesInCStyleCastParentheses: 'false'
|
||||
SpacesInContainerLiterals: 'false'
|
||||
SpacesInParentheses: 'false'
|
||||
SpacesInSquareBrackets: 'false'
|
||||
Standard: Cpp11
|
||||
TabWidth: '4'
|
||||
UseTab: Always
|
||||
|
||||
...
|
||||
@@ -1,73 +0,0 @@
|
||||
# This file is used to ignore files which are generated
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
*~
|
||||
*.autosave
|
||||
*.a
|
||||
*.core
|
||||
*.moc
|
||||
*.o
|
||||
*.obj
|
||||
*.orig
|
||||
*.rej
|
||||
*.so
|
||||
*.so.*
|
||||
*_pch.h.cpp
|
||||
*_resource.rc
|
||||
*.qm
|
||||
.#*
|
||||
*.*#
|
||||
core
|
||||
!core/
|
||||
tags
|
||||
.DS_Store
|
||||
.directory
|
||||
*.debug
|
||||
Makefile*
|
||||
*.prl
|
||||
*.app
|
||||
moc_*.cpp
|
||||
ui_*.h
|
||||
qrc_*.cpp
|
||||
Thumbs.db
|
||||
*.res
|
||||
*.rc
|
||||
/.qmake.cache
|
||||
/.qmake.stash
|
||||
|
||||
# qtcreator generated files
|
||||
*.pro.user*
|
||||
|
||||
# xemacs temporary files
|
||||
*.flc
|
||||
|
||||
# Vim temporary files
|
||||
.*.swp
|
||||
|
||||
# Visual Studio generated files
|
||||
*.ib_pdb_index
|
||||
*.idb
|
||||
*.ilk
|
||||
*.pdb
|
||||
*.sln
|
||||
*.suo
|
||||
*.vcproj
|
||||
*vcproj.*.*.user
|
||||
*.ncb
|
||||
*.sdf
|
||||
*.opensdf
|
||||
*.vcxproj
|
||||
*vcxproj.*
|
||||
|
||||
# MinGW generated files
|
||||
*.Debug
|
||||
*.Release
|
||||
|
||||
# Python byte code
|
||||
*.pyc
|
||||
|
||||
# Binaries
|
||||
# --------
|
||||
*.dll
|
||||
*.exe
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
# Copyright 2006 The QElectroTech Team
|
||||
# This file is part of QElectroTech.
|
||||
#
|
||||
# QElectroTech is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# QElectroTech is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
|
||||
message("..___________________________________________________________________")
|
||||
|
||||
project(G_unitmocktests LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
|
||||
SET(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
message(".. PROJECT_NAME :" ${PROJECT_NAME})
|
||||
message(".. PROJECT_SOURCE_DIR :" ${PROJECT_SOURCE_DIR})
|
||||
if(NOT DEFINED QET_DIR)
|
||||
set(QET_DIR "../..")
|
||||
message(".. QET_DIR is not set, assuming QET is ../..")
|
||||
endif()
|
||||
message(".. QET_DIR :" ${QET_DIR})
|
||||
if(NOT DEFINED QET_COMPONENTS)
|
||||
message(".. QET_COMPONENTS is not set !!! I set them up !!!")
|
||||
include(../../cmake/qet_compilation_vars.cmake)
|
||||
endif()
|
||||
|
||||
find_package(
|
||||
Qt6
|
||||
COMPONENTS
|
||||
${QET_COMPONENTS}
|
||||
REQUIRED)
|
||||
|
||||
Include(FetchContent)
|
||||
|
||||
FetchContent_Declare(
|
||||
GTest
|
||||
GIT_REPOSITORY https://github.com/google/googletest.git
|
||||
GIT_TAG release-1.10.0)
|
||||
set(INSTALL_GTEST OFF CACHE INTERNAL "")
|
||||
FetchContent_MakeAvailable(GTest)
|
||||
|
||||
include(../../cmake/fetch_kdeaddons.cmake)
|
||||
include(../../cmake/fetch_singleapplication.cmake)
|
||||
include(../../cmake/fetch_pugixml.cmake)
|
||||
|
||||
enable_testing()
|
||||
|
||||
add_executable(
|
||||
${PROJECT_NAME}
|
||||
tst_My_test.cpp
|
||||
main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
${PROJECT_NAME}
|
||||
PUBLIC
|
||||
gmock gmock_main
|
||||
PRIVATE
|
||||
${KF_PRIVATE_LIBRARIES}
|
||||
${QET_PRIVATE_LIBRARIES})
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#include <QtGui/QGuiApplication>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
QGuiApplication app(argc, argv);
|
||||
|
||||
// disable the whole debug output (we want only the output from gtest)
|
||||
// Debug::instance()->setDebugLevelLogFile(Debug::DebugLevel_t::Nothing);
|
||||
// Debug::instance()->setDebugLevelStderr(Debug::DebugLevel_t::Nothing);
|
||||
|
||||
// init gtest and run all tests
|
||||
::testing::InitGoogleMock(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
#include "../../sources/borderproperties.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
TEST(googlemocktest, sample_mock) { EXPECT_EQ(1, 1); }
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
BasedOnStyle: LLVM
|
||||
AlignAfterOpenBracket: AlwaysBreak
|
||||
AlignConsecutiveMacros: 'true'
|
||||
AlignConsecutiveAssignments: 'true'
|
||||
AlignConsecutiveDeclarations: 'true'
|
||||
AlignEscapedNewlines: Right
|
||||
AlignOperands: 'true'
|
||||
AlignTrailingComments: 'true'
|
||||
AllowAllArgumentsOnNextLine: 'false'
|
||||
AllowAllConstructorInitializersOnNextLine: 'true'
|
||||
AllowAllParametersOfDeclarationOnNextLine: 'true'
|
||||
AllowShortBlocksOnASingleLine: 'true'
|
||||
AllowShortCaseLabelsOnASingleLine: 'true'
|
||||
AllowShortFunctionsOnASingleLine: All
|
||||
AllowShortIfStatementsOnASingleLine: Always
|
||||
AllowShortLambdasOnASingleLine: All
|
||||
AllowShortLoopsOnASingleLine: 'true'
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: 'true'
|
||||
AlwaysBreakTemplateDeclarations: 'Yes'
|
||||
BinPackArguments: 'false'
|
||||
BinPackParameters: 'false'
|
||||
BreakAfterJavaFieldAnnotations: 'true'
|
||||
BreakBeforeBinaryOperators: NonAssignment
|
||||
BreakBeforeBraces: Allman
|
||||
BreakBeforeTernaryOperators: 'false'
|
||||
BreakConstructorInitializers: AfterColon
|
||||
BreakInheritanceList: AfterColon
|
||||
BreakStringLiterals: 'true'
|
||||
ColumnLimit: '80'
|
||||
CompactNamespaces: 'false'
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: 'true'
|
||||
Cpp11BracedListStyle: 'true'
|
||||
FixNamespaceComments: 'true'
|
||||
IncludeBlocks: Regroup
|
||||
IndentCaseLabels: 'false'
|
||||
IndentPPDirectives: AfterHash
|
||||
IndentWidth: '4'
|
||||
JavaScriptWrapImports: 'true'
|
||||
Language: Cpp
|
||||
MaxEmptyLinesToKeep: '1'
|
||||
NamespaceIndentation: All
|
||||
PointerAlignment: Left
|
||||
ReflowComments: 'true'
|
||||
SortIncludes: 'true'
|
||||
SortUsingDeclarations: 'true'
|
||||
SpaceAfterCStyleCast: 'true'
|
||||
SpaceAfterLogicalNot: 'true'
|
||||
SpaceAfterTemplateKeyword: 'true'
|
||||
SpaceBeforeAssignmentOperators: 'true'
|
||||
SpaceBeforeCpp11BracedList: 'true'
|
||||
SpaceBeforeCtorInitializerColon: 'true'
|
||||
SpaceBeforeInheritanceColon: 'true'
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceBeforeRangeBasedForLoopColon: 'true'
|
||||
SpaceInEmptyParentheses: 'false'
|
||||
SpacesInAngles: 'false'
|
||||
SpacesInCStyleCastParentheses: 'false'
|
||||
SpacesInContainerLiterals: 'false'
|
||||
SpacesInParentheses: 'false'
|
||||
SpacesInSquareBrackets: 'false'
|
||||
Standard: Cpp11
|
||||
TabWidth: '4'
|
||||
UseTab: Always
|
||||
|
||||
...
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user