Compare commits
78 Commits
2da516667e
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| d7052e396b | |||
| e123c55754 | |||
| 268aba60eb | |||
| 313f9533a8 | |||
| 3d1cb671c1 | |||
| c0fc093b4e | |||
| 46c35d2297 | |||
| 2adc58c1c0 | |||
| 82262c5980 | |||
| 6dcb6a2a8f | |||
| c740cdf1ac | |||
| aab0a18506 | |||
| ebab58e4b3 | |||
| 45ca53c7a8 | |||
| 2efce1752d | |||
| 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 | |||
| ef1fbd79b4 | |||
| eb7abdfbc6 | |||
| f574d28340 | |||
| 31f90e2c46 | |||
| 45bd8256b3 | |||
| 4e43cde063 | |||
| aef56fe41d | |||
| 0395f90f75 | |||
| a4d5b7a12a | |||
| ba29dc45a0 | |||
| d06c7be606 | |||
| 56cbd6e23d | |||
| 8bbc2da5c7 | |||
| 7fa1ff35b8 | |||
| 4b8db6be0e | |||
| a6b4c3c673 | |||
| 8f0ee06a4f | |||
| 7e3420b1a5 | |||
| 183035d5ac | |||
| eb4109bb11 | |||
| 717bf57677 | |||
| 861e5de25e | |||
| bf7bff595e | |||
| 70599c4ae1 | |||
| e42ebdc861 | |||
| 3c1fb2fe72 | |||
| 95d0e523fe | |||
| 81449faffd |
@@ -8,3 +8,11 @@ doc/*
|
|||||||
QElectroTech.tag
|
QElectroTech.tag
|
||||||
!doc/doc-utils
|
!doc/doc-utils
|
||||||
lang/*.qm
|
lang/*.qm
|
||||||
|
|
||||||
|
# -- IDE settings/intermediate files --
|
||||||
|
# zed
|
||||||
|
.zed
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode
|
||||||
|
|||||||
@@ -113,7 +113,9 @@ if(Backtrace_FOUND)
|
|||||||
include_directories(${Backtrace_INCLUDE_DIRS})
|
include_directories(${Backtrace_INCLUDE_DIRS})
|
||||||
add_compile_definitions(QET_CRASH_BACKTRACE)
|
add_compile_definitions(QET_CRASH_BACKTRACE)
|
||||||
else()
|
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()
|
endif()
|
||||||
|
|
||||||
find_package(SQLite3 REQUIRED)
|
find_package(SQLite3 REQUIRED)
|
||||||
|
|||||||
@@ -271,6 +271,22 @@ mkdir build && cd build
|
|||||||
cmake .. -G Ninja -DBUILD_WITH_KF=OFF -DCMAKE_BUILD_TYPE=Release
|
cmake .. -G Ninja -DBUILD_WITH_KF=OFF -DCMAKE_BUILD_TYPE=Release
|
||||||
cmake --build .
|
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.)
|
(KF6 isn't packaged in MSYS2 either, hence `-DBUILD_WITH_KF=OFF` again.)
|
||||||
|
|
||||||
Using the Qt Online Installer's bundled MinGW kit instead: point
|
Using the Qt Online Installer's bundled MinGW kit instead: point
|
||||||
|
|||||||
@@ -23,8 +23,31 @@ if(BUILD_WITH_KF)
|
|||||||
|
|
||||||
if(BUILD_KF)
|
if(BUILD_KF)
|
||||||
|
|
||||||
if(NOT DEFINED KF_GIT_TAG)
|
# v6.10.0 is a more or less random version, taken as an conservative
|
||||||
# this is a more or less random version, taken as an conservative approach
|
# 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)
|
set(KF_GIT_TAG v6.10.0)
|
||||||
endif()
|
endif()
|
||||||
# using a function in order to limit the scope of the variables
|
# using a function in order to limit the scope of the variables
|
||||||
@@ -53,19 +76,19 @@ if(BUILD_WITH_KF)
|
|||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
ecm
|
ecm
|
||||||
GIT_REPOSITORY https://invent.kde.org/frameworks/extra-cmake-modules.git
|
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_MakeAvailable(ecm)
|
||||||
|
|
||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
kcoreaddons
|
kcoreaddons
|
||||||
GIT_REPOSITORY https://invent.kde.org/frameworks/kcoreaddons.git
|
GIT_REPOSITORY https://invent.kde.org/frameworks/kcoreaddons.git
|
||||||
GIT_TAG ${KF_GIT_TAG})
|
GIT_TAG ${KF_KCOREADDONS_GIT_COMMIT})
|
||||||
FetchContent_MakeAvailable(kcoreaddons)
|
FetchContent_MakeAvailable(kcoreaddons)
|
||||||
|
|
||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
kwidgetsaddons
|
kwidgetsaddons
|
||||||
GIT_REPOSITORY https://invent.kde.org/frameworks/kwidgetsaddons.git
|
GIT_REPOSITORY https://invent.kde.org/frameworks/kwidgetsaddons.git
|
||||||
GIT_TAG ${KF_GIT_TAG})
|
GIT_TAG ${KF_KWIDGETSADDONS_GIT_COMMIT})
|
||||||
FetchContent_MakeAvailable(kwidgetsaddons)
|
FetchContent_MakeAvailable(kwidgetsaddons)
|
||||||
endfunction()
|
endfunction()
|
||||||
qet_make_kf_available()
|
qet_make_kf_available()
|
||||||
|
|||||||
@@ -22,10 +22,51 @@ option(BUILD_PUGIXML "Build pugixml library, use system one otherwise" YES)
|
|||||||
|
|
||||||
if(BUILD_PUGIXML)
|
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(
|
FetchContent_Declare(
|
||||||
pugixml
|
pugixml
|
||||||
GIT_REPOSITORY https://github.com/zeux/pugixml.git
|
GIT_REPOSITORY https://github.com/zeux/pugixml.git
|
||||||
GIT_TAG v1.15)
|
GIT_TAG ee86beb30e4973f5feffe3ce63bfa4fbadf72f38) # v1.15
|
||||||
set(PUGIXML_INSTALL OFF CACHE INTERNAL "")
|
set(PUGIXML_INSTALL OFF CACHE INTERNAL "")
|
||||||
FetchContent_MakeAvailable(pugixml)
|
FetchContent_MakeAvailable(pugixml)
|
||||||
else()
|
else()
|
||||||
|
|||||||
@@ -31,9 +31,15 @@ if(EXISTS "${CMAKE_SOURCE_DIR}/SingleApplication/CMakeLists.txt")
|
|||||||
set(FETCHCONTENT_SOURCE_DIR_SINGLEAPPLICATION "${CMAKE_SOURCE_DIR}/SingleApplication")
|
set(FETCHCONTENT_SOURCE_DIR_SINGLEAPPLICATION "${CMAKE_SOURCE_DIR}/SingleApplication")
|
||||||
endif()
|
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(
|
FetchContent_Declare(
|
||||||
SingleApplication
|
SingleApplication
|
||||||
GIT_REPOSITORY https://github.com/itay-grudev/SingleApplication.git
|
GIT_REPOSITORY https://github.com/itay-grudev/SingleApplication.git
|
||||||
GIT_TAG v3.2.0)
|
GIT_TAG aede311d28d20179216c5419b581087be2a8409f) # v3.2.0
|
||||||
set(QT_DEFAULT_MAJOR_VERSION 6)
|
set(QT_DEFAULT_MAJOR_VERSION 6)
|
||||||
FetchContent_MakeAvailable(SingleApplication)
|
FetchContent_MakeAvailable(SingleApplication)
|
||||||
|
|||||||
@@ -248,8 +248,12 @@ set(QET_SRC_FILES
|
|||||||
${QET_DIR}/sources/qet.h
|
${QET_DIR}/sources/qet.h
|
||||||
${QET_DIR}/sources/qeticons.cpp
|
${QET_DIR}/sources/qeticons.cpp
|
||||||
${QET_DIR}/sources/qeticons.h
|
${QET_DIR}/sources/qeticons.h
|
||||||
|
${QET_DIR}/sources/palettegraphicsview.cpp
|
||||||
|
${QET_DIR}/sources/palettegraphicsview.h
|
||||||
${QET_DIR}/sources/qetpalette.cpp
|
${QET_DIR}/sources/qetpalette.cpp
|
||||||
${QET_DIR}/sources/qetpalette.h
|
${QET_DIR}/sources/qetpalette.h
|
||||||
|
${QET_DIR}/sources/qetstyle.cpp
|
||||||
|
${QET_DIR}/sources/qetstyle.h
|
||||||
${QET_DIR}/sources/qetinformation.cpp
|
${QET_DIR}/sources/qetinformation.cpp
|
||||||
${QET_DIR}/sources/qetinformation.h
|
${QET_DIR}/sources/qetinformation.h
|
||||||
${QET_DIR}/sources/qetmainwindow.cpp
|
${QET_DIR}/sources/qetmainwindow.cpp
|
||||||
@@ -434,6 +438,8 @@ set(QET_SRC_FILES
|
|||||||
${QET_DIR}/sources/ElementsCollection/elementcollectionhandler.h
|
${QET_DIR}/sources/ElementsCollection/elementcollectionhandler.h
|
||||||
${QET_DIR}/sources/ElementsCollection/elementcollectionitem.cpp
|
${QET_DIR}/sources/ElementsCollection/elementcollectionitem.cpp
|
||||||
${QET_DIR}/sources/ElementsCollection/elementcollectionitem.h
|
${QET_DIR}/sources/ElementsCollection/elementcollectionitem.h
|
||||||
|
${QET_DIR}/sources/ElementsCollection/elementpreviewdelegate.cpp
|
||||||
|
${QET_DIR}/sources/ElementsCollection/elementpreviewdelegate.h
|
||||||
${QET_DIR}/sources/ElementsCollection/elementscollectionmodel.cpp
|
${QET_DIR}/sources/ElementsCollection/elementscollectionmodel.cpp
|
||||||
${QET_DIR}/sources/ElementsCollection/elementscollectionmodel.h
|
${QET_DIR}/sources/ElementsCollection/elementscollectionmodel.h
|
||||||
${QET_DIR}/sources/ElementsCollection/elementscollectionwidget.cpp
|
${QET_DIR}/sources/ElementsCollection/elementscollectionwidget.cpp
|
||||||
@@ -699,6 +705,10 @@ set(QET_SRC_FILES
|
|||||||
${QET_DIR}/sources/ui/contactgroupselectiondialog.h
|
${QET_DIR}/sources/ui/contactgroupselectiondialog.h
|
||||||
${QET_DIR}/sources/ui/conductorpropertiesdialog.cpp
|
${QET_DIR}/sources/ui/conductorpropertiesdialog.cpp
|
||||||
${QET_DIR}/sources/ui/conductorpropertiesdialog.h
|
${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.cpp
|
||||||
${QET_DIR}/sources/ui/conductorpropertieswidget.h
|
${QET_DIR}/sources/ui/conductorpropertieswidget.h
|
||||||
${QET_DIR}/sources/ui/configsaveloaderwidget.cpp
|
${QET_DIR}/sources/ui/configsaveloaderwidget.cpp
|
||||||
|
|||||||
|
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 |
|
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 |
|
Before Width: | Height: | Size: 216 B |
|
Before Width: | Height: | Size: 262 B |
|
Before Width: | Height: | Size: 247 B |
|
Before Width: | Height: | Size: 271 B |
|
Before Width: | Height: | Size: 233 B |
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -216,6 +216,9 @@ Clarification:
|
|||||||
ico/128x128/diagram.png by the QElectroTech team (License CC BY-ND 3.0)
|
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/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/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)
|
ico/256x256/* by Nuri from the QElectroTech team (License CC BY-ND 3.0)
|
||||||
ico/breeze-icons/* by Nuri from the QElectroTech team (License CC BY-ND 3.0)
|
ico/breeze-icons/* by Nuri from the QElectroTech team (License CC BY-ND 3.0)
|
||||||
ico/diagram.png by Nuri from the QElectroTech team (License CC BY-ND 3.0)
|
ico/diagram.png by Nuri from the QElectroTech team (License CC BY-ND 3.0)
|
||||||
|
|||||||
@@ -148,15 +148,8 @@
|
|||||||
<file alias="themes/qet/22x22/conductor-reset.png">22x22/conductor2.png</file>
|
<file alias="themes/qet/22x22/conductor-reset.png">22x22/conductor2.png</file>
|
||||||
<file alias="themes/qet/22x22/configure-toolbars.png">22x22/configure-toolbars.png</file>
|
<file alias="themes/qet/22x22/configure-toolbars.png">22x22/configure-toolbars.png</file>
|
||||||
<file alias="themes/qet/22x22/configure.png">22x22/configure.png</file>
|
<file alias="themes/qet/22x22/configure.png">22x22/configure.png</file>
|
||||||
<file alias="themes/qet/22x22/diagram.png">22x22/diagram.png</file>
|
|
||||||
<file alias="themes/qet/22x22/diagram_add.png">22x22/diagram_add.png</file>
|
|
||||||
<file alias="themes/qet/22x22/folio-new.png">22x22/diagram_add.png</file>
|
|
||||||
<file alias="themes/qet/22x22/diagram_bg.png">22x22/diagram_bg.png</file>
|
<file alias="themes/qet/22x22/diagram_bg.png">22x22/diagram_bg.png</file>
|
||||||
<file alias="themes/qet/22x22/diagram_del.png">22x22/diagram_del.png</file>
|
|
||||||
<file alias="themes/qet/22x22/folio-delete.png">22x22/diagram_del.png</file>
|
|
||||||
<file alias="themes/qet/22x22/dialog-cancel.png">22x22/dialog-cancel.png</file>
|
<file alias="themes/qet/22x22/dialog-cancel.png">22x22/dialog-cancel.png</file>
|
||||||
<file alias="themes/qet/22x22/dialog-information.png">22x22/dialog-information.png</file>
|
|
||||||
<file alias="themes/qet/22x22/folio-properties.png">22x22/dialog-information.png</file>
|
|
||||||
<file alias="themes/qet/22x22/dialog-ok.png">22x22/dialog-ok.png</file>
|
<file alias="themes/qet/22x22/dialog-ok.png">22x22/dialog-ok.png</file>
|
||||||
<file alias="themes/qet/22x22/document-close.png">22x22/document-close.png</file>
|
<file alias="themes/qet/22x22/document-close.png">22x22/document-close.png</file>
|
||||||
<file alias="themes/qet/22x22/document-export.png">22x22/document-export.png</file>
|
<file alias="themes/qet/22x22/document-export.png">22x22/document-export.png</file>
|
||||||
@@ -207,7 +200,6 @@
|
|||||||
<file alias="themes/qet/22x22/guides.png">22x22/guides.png</file>
|
<file alias="themes/qet/22x22/guides.png">22x22/guides.png</file>
|
||||||
<file alias="themes/qet/22x22/hotspot.png">22x22/hotspot.png</file>
|
<file alias="themes/qet/22x22/hotspot.png">22x22/hotspot.png</file>
|
||||||
<file alias="themes/qet/22x22/insert-image.png">22x22/insert-image.png</file>
|
<file alias="themes/qet/22x22/insert-image.png">22x22/insert-image.png</file>
|
||||||
<file alias="themes/qet/22x22/label.png">22x22/label.png</file>
|
|
||||||
<file alias="themes/qet/22x22/landscape.png">22x22/landscape.png</file>
|
<file alias="themes/qet/22x22/landscape.png">22x22/landscape.png</file>
|
||||||
<file alias="themes/qet/22x22/line.png">22x22/line.png</file>
|
<file alias="themes/qet/22x22/line.png">22x22/line.png</file>
|
||||||
<file alias="themes/qet/22x22/list-add.png">22x22/list-add.png</file>
|
<file alias="themes/qet/22x22/list-add.png">22x22/list-add.png</file>
|
||||||
@@ -218,7 +210,6 @@
|
|||||||
<file alias="themes/qet/22x22/object-locked.png">22x22/object-locked.png</file>
|
<file alias="themes/qet/22x22/object-locked.png">22x22/object-locked.png</file>
|
||||||
<file alias="themes/qet/22x22/object-rotate-right.png">22x22/object-rotate-right.png</file>
|
<file alias="themes/qet/22x22/object-rotate-right.png">22x22/object-rotate-right.png</file>
|
||||||
<file alias="themes/qet/22x22/object-unlocked.png">22x22/object-unlocked.png</file>
|
<file alias="themes/qet/22x22/object-unlocked.png">22x22/object-unlocked.png</file>
|
||||||
<file alias="themes/qet/22x22/pdf-import.png">22x22/pdf-import.png</file>
|
|
||||||
<file alias="themes/qet/22x22/polygon.png">22x22/polygon.png</file>
|
<file alias="themes/qet/22x22/polygon.png">22x22/polygon.png</file>
|
||||||
<file alias="themes/qet/22x22/portrait.png">22x22/portrait.png</file>
|
<file alias="themes/qet/22x22/portrait.png">22x22/portrait.png</file>
|
||||||
<file alias="themes/qet/22x22/preferences-desktop-user.png">22x22/preferences-desktop-user.png</file>
|
<file alias="themes/qet/22x22/preferences-desktop-user.png">22x22/preferences-desktop-user.png</file>
|
||||||
@@ -268,12 +259,20 @@
|
|||||||
<file alias="themes/qet/48x48/user-away-extended.png">48x48/user-away-extended.png</file>
|
<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/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/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/diagram.png">128x128/diagram.png</file>
|
||||||
<file alias="themes/qet/128x128/document-export.png">128x128/document-export.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/plasmagik.png">128x128/plasmagik.png</file>
|
||||||
<file alias="themes/qet/128x128/printer.png">128x128/printer.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/project.png">128x128/project.png</file>
|
||||||
<file alias="themes/qet/128x128/settings.png">128x128/settings.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>
|
||||||
|
<file alias="themes/qet/scalable/folio-properties.svg">scalable/folio-properties.svg</file>
|
||||||
|
<file alias="themes/qet/scalable/label.svg">scalable/label.svg</file>
|
||||||
|
<file alias="themes/qet/scalable/pdf-import.svg">scalable/pdf-import.svg</file>
|
||||||
<file alias="themes/qet/scalable/edit-opacity.svg">breeze-icons/scalable/apps/hidef/edit-opacity.svg</file>
|
<file alias="themes/qet/scalable/edit-opacity.svg">breeze-icons/scalable/apps/hidef/edit-opacity.svg</file>
|
||||||
<file alias="themes/qet/scalable/image-flip-horizontal.svg">breeze-icons/scalable/apps/hidef/image-flip-horizontal-symbolic.svg</file>
|
<file alias="themes/qet/scalable/image-flip-horizontal.svg">breeze-icons/scalable/apps/hidef/image-flip-horizontal-symbolic.svg</file>
|
||||||
<file alias="themes/qet/scalable/image-flip-vertical.svg">breeze-icons/scalable/apps/hidef/image-flip-vertical-symbolic.svg</file>
|
<file alias="themes/qet/scalable/image-flip-vertical.svg">breeze-icons/scalable/apps/hidef/image-flip-vertical-symbolic.svg</file>
|
||||||
@@ -290,7 +289,6 @@
|
|||||||
<file>themes/qet-dark/16x16/conductor-edit.png</file>
|
<file>themes/qet-dark/16x16/conductor-edit.png</file>
|
||||||
<file>themes/qet-dark/16x16/configure-toolbars.png</file>
|
<file>themes/qet-dark/16x16/configure-toolbars.png</file>
|
||||||
<file>themes/qet-dark/16x16/configure.png</file>
|
<file>themes/qet-dark/16x16/configure.png</file>
|
||||||
<file>themes/qet-dark/16x16/diagram.png</file>
|
|
||||||
<file>themes/qet-dark/16x16/dialog-cancel.png</file>
|
<file>themes/qet-dark/16x16/dialog-cancel.png</file>
|
||||||
<file>themes/qet-dark/16x16/dialog-ok.png</file>
|
<file>themes/qet-dark/16x16/dialog-ok.png</file>
|
||||||
<file>themes/qet-dark/16x16/document-export.png</file>
|
<file>themes/qet-dark/16x16/document-export.png</file>
|
||||||
@@ -331,9 +329,6 @@
|
|||||||
<file>themes/qet-dark/16x16/folder-open.png</file>
|
<file>themes/qet-dark/16x16/folder-open.png</file>
|
||||||
<file>themes/qet-dark/16x16/folder-show-all.png</file>
|
<file>themes/qet-dark/16x16/folder-show-all.png</file>
|
||||||
<file>themes/qet-dark/16x16/folder.png</file>
|
<file>themes/qet-dark/16x16/folder.png</file>
|
||||||
<file>themes/qet-dark/16x16/folio-delete.png</file>
|
|
||||||
<file>themes/qet-dark/16x16/folio-new.png</file>
|
|
||||||
<file>themes/qet-dark/16x16/folio-properties.png</file>
|
|
||||||
<file>themes/qet-dark/16x16/folio-ref-coming.png</file>
|
<file>themes/qet-dark/16x16/folio-ref-coming.png</file>
|
||||||
<file>themes/qet-dark/16x16/go-company.png</file>
|
<file>themes/qet-dark/16x16/go-company.png</file>
|
||||||
<file>themes/qet-dark/16x16/go-down-double.png</file>
|
<file>themes/qet-dark/16x16/go-down-double.png</file>
|
||||||
@@ -348,7 +343,6 @@
|
|||||||
<file>themes/qet-dark/16x16/item-copy.png</file>
|
<file>themes/qet-dark/16x16/item-copy.png</file>
|
||||||
<file>themes/qet-dark/16x16/item-move.png</file>
|
<file>themes/qet-dark/16x16/item-move.png</file>
|
||||||
<file>themes/qet-dark/16x16/kdenlive-show-video.png</file>
|
<file>themes/qet-dark/16x16/kdenlive-show-video.png</file>
|
||||||
<file>themes/qet-dark/16x16/label.png</file>
|
|
||||||
<file>themes/qet-dark/16x16/list-add.png</file>
|
<file>themes/qet-dark/16x16/list-add.png</file>
|
||||||
<file>themes/qet-dark/16x16/list-remove.png</file>
|
<file>themes/qet-dark/16x16/list-remove.png</file>
|
||||||
<file>themes/qet-dark/16x16/masquer.png</file>
|
<file>themes/qet-dark/16x16/masquer.png</file>
|
||||||
@@ -381,15 +375,7 @@
|
|||||||
<file>themes/qet-dark/22x22/arrow-right.png</file>
|
<file>themes/qet-dark/22x22/arrow-right.png</file>
|
||||||
<file>themes/qet-dark/22x22/configure-toolbars.png</file>
|
<file>themes/qet-dark/22x22/configure-toolbars.png</file>
|
||||||
<file>themes/qet-dark/22x22/configure.png</file>
|
<file>themes/qet-dark/22x22/configure.png</file>
|
||||||
<file>themes/qet-dark/22x22/diagram.png</file>
|
|
||||||
<file>themes/qet-dark/22x22/diagram_add.png</file>
|
|
||||||
<file>themes/qet-dark/22x22/folio-new.png</file>
|
|
||||||
<file>themes/qet-dark/22x22/diagram_bg.png</file>
|
|
||||||
<file>themes/qet-dark/22x22/diagram_del.png</file>
|
|
||||||
<file>themes/qet-dark/22x22/folio-delete.png</file>
|
|
||||||
<file>themes/qet-dark/22x22/dialog-cancel.png</file>
|
<file>themes/qet-dark/22x22/dialog-cancel.png</file>
|
||||||
<file>themes/qet-dark/22x22/dialog-information.png</file>
|
|
||||||
<file>themes/qet-dark/22x22/folio-properties.png</file>
|
|
||||||
<file>themes/qet-dark/22x22/dialog-ok.png</file>
|
<file>themes/qet-dark/22x22/dialog-ok.png</file>
|
||||||
<file>themes/qet-dark/22x22/document-export.png</file>
|
<file>themes/qet-dark/22x22/document-export.png</file>
|
||||||
<file>themes/qet-dark/22x22/document-import.png</file>
|
<file>themes/qet-dark/22x22/document-import.png</file>
|
||||||
@@ -425,7 +411,6 @@
|
|||||||
<file>themes/qet-dark/22x22/go-up.png</file>
|
<file>themes/qet-dark/22x22/go-up.png</file>
|
||||||
<file>themes/qet-dark/22x22/grid.png</file>
|
<file>themes/qet-dark/22x22/grid.png</file>
|
||||||
<file>themes/qet-dark/22x22/insert-image.png</file>
|
<file>themes/qet-dark/22x22/insert-image.png</file>
|
||||||
<file>themes/qet-dark/22x22/label.png</file>
|
|
||||||
<file>themes/qet-dark/22x22/landscape.png</file>
|
<file>themes/qet-dark/22x22/landscape.png</file>
|
||||||
<file>themes/qet-dark/22x22/line.png</file>
|
<file>themes/qet-dark/22x22/line.png</file>
|
||||||
<file>themes/qet-dark/22x22/list-add.png</file>
|
<file>themes/qet-dark/22x22/list-add.png</file>
|
||||||
@@ -436,7 +421,6 @@
|
|||||||
<file>themes/qet-dark/22x22/object-locked.png</file>
|
<file>themes/qet-dark/22x22/object-locked.png</file>
|
||||||
<file>themes/qet-dark/22x22/object-rotate-right.png</file>
|
<file>themes/qet-dark/22x22/object-rotate-right.png</file>
|
||||||
<file>themes/qet-dark/22x22/object-unlocked.png</file>
|
<file>themes/qet-dark/22x22/object-unlocked.png</file>
|
||||||
<file>themes/qet-dark/22x22/pdf-import.png</file>
|
|
||||||
<file>themes/qet-dark/22x22/polygon.png</file>
|
<file>themes/qet-dark/22x22/polygon.png</file>
|
||||||
<file>themes/qet-dark/22x22/portrait.png</file>
|
<file>themes/qet-dark/22x22/portrait.png</file>
|
||||||
<file>themes/qet-dark/22x22/preferences-desktop-user.png</file>
|
<file>themes/qet-dark/22x22/preferences-desktop-user.png</file>
|
||||||
@@ -472,8 +456,13 @@
|
|||||||
<file>themes/qet-dark/48x48/user-away.png</file>
|
<file>themes/qet-dark/48x48/user-away.png</file>
|
||||||
<file>themes/qet-dark/48x48/view-pim-journal.png</file>
|
<file>themes/qet-dark/48x48/view-pim-journal.png</file>
|
||||||
<file>themes/qet-dark/128x128/plasmagik.png</file>
|
<file>themes/qet-dark/128x128/plasmagik.png</file>
|
||||||
<file>themes/qet-dark/128x128/printer.png</file>
|
|
||||||
<file>themes/qet-dark/128x128/settings.png</file>
|
<file>themes/qet-dark/128x128/settings.png</file>
|
||||||
|
<file>themes/qet-dark/scalable/diagram.svg</file>
|
||||||
|
<file>themes/qet-dark/scalable/folio-delete.svg</file>
|
||||||
|
<file>themes/qet-dark/scalable/folio-new.svg</file>
|
||||||
|
<file>themes/qet-dark/scalable/folio-properties.svg</file>
|
||||||
|
<file>themes/qet-dark/scalable/label.svg</file>
|
||||||
|
<file>themes/qet-dark/scalable/pdf-import.svg</file>
|
||||||
<file>themes/qet-dark/scalable/edit-opacity.svg</file>
|
<file>themes/qet-dark/scalable/edit-opacity.svg</file>
|
||||||
<file>themes/qet-dark/scalable/image-flip-horizontal.svg</file>
|
<file>themes/qet-dark/scalable/image-flip-horizontal.svg</file>
|
||||||
<file>themes/qet-dark/scalable/image-flip-vertical.svg</file>
|
<file>themes/qet-dark/scalable/image-flip-vertical.svg</file>
|
||||||
@@ -482,5 +471,7 @@
|
|||||||
<file>themes/qet-dark/scalable/ellipse-to-bezier.svg</file>
|
<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-bezier.svg</file>
|
||||||
<file>themes/qet-dark/scalable/rect-to-polyline.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>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<defs id="defs3051">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#4d4d4d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(1,1)">
|
||||||
|
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 13 13 h 1 v 1 h -1 Z M 13 16 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 13 h 1 v 1 h -1 Z M 14 16 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 13 h 1 v 1 h -1 Z M 15 16 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 13 h 1 v 1 h -1 Z M 16 16 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 13 h 1 v 1 h -1 Z M 17 16 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 13 h 1 v 1 h -1 Z M 18 14 h 1 v 1 h -1 Z M 18 15 h 1 v 1 h -1 Z M 18 16 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<defs id="defs3051">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#4d4d4d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(1,1)">
|
||||||
|
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 17 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 17 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 17 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 17 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 17 h 1 v 1 h -1 Z M 19 17 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<defs id="defs3051">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#4d4d4d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(1,1)">
|
||||||
|
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 17 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 17 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 17 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 14 h 1 v 1 h -1 Z M 17 15 h 1 v 1 h -1 Z M 17 16 h 1 v 1 h -1 Z M 17 17 h 1 v 1 h -1 Z M 17 18 h 1 v 1 h -1 Z M 17 19 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 17 h 1 v 1 h -1 Z M 19 17 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<defs id="defs3051">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#4d4d4d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(1,1)">
|
||||||
|
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 8 h 1 v 1 h -1 Z M 6 10 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 8 h 1 v 1 h -1 Z M 7 10 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 8 h 1 v 1 h -1 Z M 8 10 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 8 h 1 v 1 h -1 Z M 9 10 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 8 h 1 v 1 h -1 Z M 10 10 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 8 h 1 v 1 h -1 Z M 11 10 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 8 h 1 v 1 h -1 Z M 12 10 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 13 8 h 1 v 1 h -1 Z M 13 10 h 1 v 1 h -1 Z M 13 13 h 1 v 1 h -1 Z M 13 16 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 8 h 1 v 1 h -1 Z M 14 10 h 1 v 1 h -1 Z M 14 13 h 1 v 1 h -1 Z M 14 16 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 8 h 1 v 1 h -1 Z M 15 10 h 1 v 1 h -1 Z M 15 13 h 1 v 1 h -1 Z M 15 16 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 13 h 1 v 1 h -1 Z M 16 16 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 13 h 1 v 1 h -1 Z M 17 16 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 13 h 1 v 1 h -1 Z M 18 14 h 1 v 1 h -1 Z M 18 15 h 1 v 1 h -1 Z M 18 16 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<defs id="defs3051">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#4d4d4d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(1,1)">
|
||||||
|
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 14 h 1 v 1 h -1 Z M 4 15 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 14 h 1 v 1 h -1 Z M 5 15 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 14 h 1 v 1 h -1 Z M 6 15 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 14 h 1 v 1 h -1 Z M 7 15 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 14 h 1 v 1 h -1 Z M 8 15 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 14 h 1 v 1 h -1 Z M 9 15 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 14 h 1 v 1 h -1 Z M 10 15 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 14 h 1 v 1 h -1 Z M 12 15 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 13 13 h 1 v 1 h -1 Z M 13 14 h 1 v 1 h -1 Z M 13 15 h 1 v 1 h -1 Z M 13 16 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 13 h 1 v 1 h -1 Z M 14 14 h 1 v 1 h -1 Z M 14 15 h 1 v 1 h -1 Z M 14 16 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 13 h 1 v 1 h -1 Z M 15 14 h 1 v 1 h -1 Z M 15 15 h 1 v 1 h -1 Z M 15 16 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 13 h 1 v 1 h -1 Z M 16 14 h 1 v 1 h -1 Z M 16 15 h 1 v 1 h -1 Z M 16 16 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 13 h 1 v 1 h -1 Z M 17 14 h 1 v 1 h -1 Z M 17 15 h 1 v 1 h -1 Z M 17 16 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 13 h 1 v 1 h -1 Z M 18 14 h 1 v 1 h -1 Z M 18 15 h 1 v 1 h -1 Z M 18 16 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<defs id="defs3051">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#4d4d4d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(1,1)">
|
||||||
|
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 3 3 h 16 v 1 h -16 Z M 3 4 h 1 v 15 h -1 Z M 4 18 h 10 v 1 h -10 Z M 18 4 h 1 v 8 h -1 Z M 16 14 h 1 v 5 h -1 Z M 14 16 h 5 v 1 h -5 Z" class="ColorScheme-Text"/>
|
||||||
|
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 5 7 h 1 v 1 h -1 Z M 6 7 h 1 v 1 h -1 Z M 7 7 h 1 v 1 h -1 Z M 5 8 h 1 v 1 h -1 Z M 7 8 h 1 v 1 h -1 Z M 5 9 h 1 v 1 h -1 Z M 6 9 h 1 v 1 h -1 Z M 7 9 h 1 v 1 h -1 Z M 5 10 h 1 v 1 h -1 Z M 5 11 h 1 v 1 h -1 Z M 9 7 h 1 v 1 h -1 Z M 10 7 h 1 v 1 h -1 Z M 9 8 h 1 v 1 h -1 Z M 11 8 h 1 v 1 h -1 Z M 9 9 h 1 v 1 h -1 Z M 11 9 h 1 v 1 h -1 Z M 9 10 h 1 v 1 h -1 Z M 11 10 h 1 v 1 h -1 Z M 9 11 h 1 v 1 h -1 Z M 10 11 h 1 v 1 h -1 Z M 13 7 h 1 v 1 h -1 Z M 14 7 h 1 v 1 h -1 Z M 15 7 h 1 v 1 h -1 Z M 13 8 h 1 v 1 h -1 Z M 13 9 h 1 v 1 h -1 Z M 14 9 h 1 v 1 h -1 Z M 13 10 h 1 v 1 h -1 Z M 13 11 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 154 B |
|
Before Width: | Height: | Size: 176 B |
|
Before Width: | Height: | Size: 163 B |
|
Before Width: | Height: | Size: 141 B |
|
Before Width: | Height: | Size: 146 B |
|
Before Width: | Height: | Size: 196 B |
|
Before Width: | Height: | Size: 702 B |
|
Before Width: | Height: | Size: 181 B |
|
Before Width: | Height: | Size: 189 B |
|
Before Width: | Height: | Size: 181 B |
|
Before Width: | Height: | Size: 196 B |
|
Before Width: | Height: | Size: 189 B |
|
Before Width: | Height: | Size: 165 B |
|
Before Width: | Height: | Size: 667 B |
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<defs id="defs3051">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#dcdcdc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(1,1)">
|
||||||
|
<path style="fill:#dcdcdc;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 13 13 h 1 v 1 h -1 Z M 13 16 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 13 h 1 v 1 h -1 Z M 14 16 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 13 h 1 v 1 h -1 Z M 15 16 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 13 h 1 v 1 h -1 Z M 16 16 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 13 h 1 v 1 h -1 Z M 17 16 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 13 h 1 v 1 h -1 Z M 18 14 h 1 v 1 h -1 Z M 18 15 h 1 v 1 h -1 Z M 18 16 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<defs id="defs3051">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#dcdcdc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(1,1)">
|
||||||
|
<path style="fill:#dcdcdc;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 17 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 17 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 17 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 17 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 17 h 1 v 1 h -1 Z M 19 17 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<defs id="defs3051">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#dcdcdc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(1,1)">
|
||||||
|
<path style="fill:#dcdcdc;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 17 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 17 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 17 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 14 h 1 v 1 h -1 Z M 17 15 h 1 v 1 h -1 Z M 17 16 h 1 v 1 h -1 Z M 17 17 h 1 v 1 h -1 Z M 17 18 h 1 v 1 h -1 Z M 17 19 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 17 h 1 v 1 h -1 Z M 19 17 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<defs id="defs3051">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#dcdcdc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(1,1)">
|
||||||
|
<path style="fill:#dcdcdc;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 8 h 1 v 1 h -1 Z M 6 10 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 8 h 1 v 1 h -1 Z M 7 10 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 8 h 1 v 1 h -1 Z M 8 10 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 8 h 1 v 1 h -1 Z M 9 10 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 8 h 1 v 1 h -1 Z M 10 10 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 8 h 1 v 1 h -1 Z M 11 10 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 8 h 1 v 1 h -1 Z M 12 10 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 13 8 h 1 v 1 h -1 Z M 13 10 h 1 v 1 h -1 Z M 13 13 h 1 v 1 h -1 Z M 13 16 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 8 h 1 v 1 h -1 Z M 14 10 h 1 v 1 h -1 Z M 14 13 h 1 v 1 h -1 Z M 14 16 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 8 h 1 v 1 h -1 Z M 15 10 h 1 v 1 h -1 Z M 15 13 h 1 v 1 h -1 Z M 15 16 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 13 h 1 v 1 h -1 Z M 16 16 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 13 h 1 v 1 h -1 Z M 17 16 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 13 h 1 v 1 h -1 Z M 18 14 h 1 v 1 h -1 Z M 18 15 h 1 v 1 h -1 Z M 18 16 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<defs id="defs3051">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#dcdcdc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(1,1)">
|
||||||
|
<path style="fill:#dcdcdc;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 14 h 1 v 1 h -1 Z M 4 15 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 14 h 1 v 1 h -1 Z M 5 15 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 14 h 1 v 1 h -1 Z M 6 15 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 14 h 1 v 1 h -1 Z M 7 15 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 14 h 1 v 1 h -1 Z M 8 15 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 14 h 1 v 1 h -1 Z M 9 15 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 14 h 1 v 1 h -1 Z M 10 15 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 14 h 1 v 1 h -1 Z M 12 15 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 13 13 h 1 v 1 h -1 Z M 13 14 h 1 v 1 h -1 Z M 13 15 h 1 v 1 h -1 Z M 13 16 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 13 h 1 v 1 h -1 Z M 14 14 h 1 v 1 h -1 Z M 14 15 h 1 v 1 h -1 Z M 14 16 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 13 h 1 v 1 h -1 Z M 15 14 h 1 v 1 h -1 Z M 15 15 h 1 v 1 h -1 Z M 15 16 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 13 h 1 v 1 h -1 Z M 16 14 h 1 v 1 h -1 Z M 16 15 h 1 v 1 h -1 Z M 16 16 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 13 h 1 v 1 h -1 Z M 17 14 h 1 v 1 h -1 Z M 17 15 h 1 v 1 h -1 Z M 17 16 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 13 h 1 v 1 h -1 Z M 18 14 h 1 v 1 h -1 Z M 18 15 h 1 v 1 h -1 Z M 18 16 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<defs id="defs3051">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#dcdcdc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(1,1)">
|
||||||
|
<path style="fill:#dcdcdc;fill-opacity:1;stroke:none" d="M 3 3 h 16 v 1 h -16 Z M 3 4 h 1 v 15 h -1 Z M 4 18 h 10 v 1 h -10 Z M 18 4 h 1 v 8 h -1 Z M 16 14 h 1 v 5 h -1 Z M 14 16 h 5 v 1 h -5 Z" class="ColorScheme-Text"/>
|
||||||
|
<path style="fill:#dcdcdc;fill-opacity:1;stroke:none" d="M 5 7 h 1 v 1 h -1 Z M 6 7 h 1 v 1 h -1 Z M 7 7 h 1 v 1 h -1 Z M 5 8 h 1 v 1 h -1 Z M 7 8 h 1 v 1 h -1 Z M 5 9 h 1 v 1 h -1 Z M 6 9 h 1 v 1 h -1 Z M 7 9 h 1 v 1 h -1 Z M 5 10 h 1 v 1 h -1 Z M 5 11 h 1 v 1 h -1 Z M 9 7 h 1 v 1 h -1 Z M 10 7 h 1 v 1 h -1 Z M 9 8 h 1 v 1 h -1 Z M 11 8 h 1 v 1 h -1 Z M 9 9 h 1 v 1 h -1 Z M 11 9 h 1 v 1 h -1 Z M 9 10 h 1 v 1 h -1 Z M 11 10 h 1 v 1 h -1 Z M 9 11 h 1 v 1 h -1 Z M 10 11 h 1 v 1 h -1 Z M 13 7 h 1 v 1 h -1 Z M 14 7 h 1 v 1 h -1 Z M 15 7 h 1 v 1 h -1 Z M 13 8 h 1 v 1 h -1 Z M 13 9 h 1 v 1 h -1 Z M 14 9 h 1 v 1 h -1 Z M 13 10 h 1 v 1 h -1 Z M 13 11 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -34,10 +34,27 @@ 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
|
the icons that need a dark variant: black line art. Colored icons are not
|
||||||
touched; the dark theme inherits them from the light one.
|
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
|
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
|
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.
|
so pure black becomes (220, 220, 220), the dark palette's text color.
|
||||||
|
|
||||||
|
An icon drawn as a light object is left alone even when it is not
|
||||||
|
saturated: a page sheet, a printer body, the white half of the background
|
||||||
|
swatch. Such art already reads on a dark toolbar, and inverting it would
|
||||||
|
turn the white body black (the PDF import icon, GitHub #919). The test is
|
||||||
|
the share of visible pixels that are near white: 30% or more and the icon
|
||||||
|
inherits from the light theme untouched.
|
||||||
|
|
||||||
Requires Pillow. Idempotent: running it twice changes nothing.
|
Requires Pillow. Idempotent: running it twice changes nothing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -64,16 +81,21 @@ SIZES = ["16x16", "22x22", "32x32", "48x48", "128x128"]
|
|||||||
|
|
||||||
# Table entries in sources/qeticons.cpp that pair a 16 pixel file with a
|
# Table entries in sources/qeticons.cpp that pair a 16 pixel file with a
|
||||||
# 22 pixel file of another name. The theme needs one name per icon, so
|
# 22 pixel file of another name. The theme needs one name per icon, so
|
||||||
# the 22 pixel file is exposed under the 16 pixel name as well.
|
# the 22 pixel file is exposed under the 16 pixel name as well. The folio
|
||||||
|
# icons that used to be listed here are SVGs now (ico/scalable/).
|
||||||
ALIASES = {
|
ALIASES = {
|
||||||
"22x22/conductor2.png": "conductor-reset",
|
"22x22/conductor2.png": "conductor-reset",
|
||||||
"22x22/diagram_add.png": "folio-new",
|
|
||||||
"22x22/diagram_del.png": "folio-delete",
|
|
||||||
"22x22/dialog-information.png": "folio-properties",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# SVG icons referenced from sources/qeticons.cpp.
|
# SVG icons referenced from sources/qeticons.cpp. ico/scalable/ holds the
|
||||||
|
# ones drawn for QET as vectors; one file serves every size.
|
||||||
SVGS = [
|
SVGS = [
|
||||||
|
"scalable/diagram.svg",
|
||||||
|
"scalable/folio-delete.svg",
|
||||||
|
"scalable/folio-new.svg",
|
||||||
|
"scalable/folio-properties.svg",
|
||||||
|
"scalable/label.svg",
|
||||||
|
"scalable/pdf-import.svg",
|
||||||
"breeze-icons/scalable/apps/hidef/edit-opacity.svg",
|
"breeze-icons/scalable/apps/hidef/edit-opacity.svg",
|
||||||
"breeze-icons/scalable/apps/hidef/image-flip-horizontal-symbolic.svg",
|
"breeze-icons/scalable/apps/hidef/image-flip-horizontal-symbolic.svg",
|
||||||
"breeze-icons/scalable/apps/hidef/image-flip-vertical-symbolic.svg",
|
"breeze-icons/scalable/apps/hidef/image-flip-vertical-symbolic.svg",
|
||||||
@@ -85,8 +107,12 @@ SVGS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
SATURATED_FRACTION = 0.20 # at or above this an icon is "colored"
|
SATURATED_FRACTION = 0.20 # at or above this an icon is "colored"
|
||||||
|
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
|
INK = 220 / 255.0 # lightness of pure black after inversion
|
||||||
SVG_INK = "#dcdcdc"
|
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):
|
def visible_pixels(image):
|
||||||
@@ -107,6 +133,38 @@ def is_line_art(image):
|
|||||||
return saturated / len(pixels) < SATURATED_FRACTION
|
return saturated / len(pixels) < SATURATED_FRACTION
|
||||||
|
|
||||||
|
|
||||||
|
def has_light_fill(image):
|
||||||
|
"""True when the icon is mostly white: a page, a sheet, a light body."""
|
||||||
|
pixels = visible_pixels(image)
|
||||||
|
if not pixels:
|
||||||
|
return False
|
||||||
|
white = sum(1 for r, g, b, _ in pixels
|
||||||
|
if colorsys.rgb_to_hls(r / 255, g / 255, b / 255)[1] > WHITE_LIGHTNESS)
|
||||||
|
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):
|
def invert_lightness(image):
|
||||||
"""Invert lightness, keeping hue, saturation and alpha.
|
"""Invert lightness, keeping hue, saturation and alpha.
|
||||||
|
|
||||||
@@ -177,7 +235,7 @@ def main():
|
|||||||
light = [] # (alias, source) pairs, paths relative to ico/
|
light = [] # (alias, source) pairs, paths relative to ico/
|
||||||
dark = [] # paths relative to ico/, files exist on disk
|
dark = [] # paths relative to ico/, files exist on disk
|
||||||
changed = 0
|
changed = 0
|
||||||
line_art = colored = 0
|
line_art = colored = light_art = 0
|
||||||
|
|
||||||
for size in SIZES:
|
for size in SIZES:
|
||||||
folder = ICO / size
|
folder = ICO / size
|
||||||
@@ -188,7 +246,10 @@ def main():
|
|||||||
names.append(ALIASES[rel])
|
names.append(ALIASES[rel])
|
||||||
image = Image.open(png).convert("RGBA")
|
image = Image.open(png).convert("RGBA")
|
||||||
art = is_line_art(image)
|
art = is_line_art(image)
|
||||||
if art:
|
if art and has_light_fill(image):
|
||||||
|
art = False
|
||||||
|
light_art += 1
|
||||||
|
elif art:
|
||||||
line_art += 1
|
line_art += 1
|
||||||
dark_image = None
|
dark_image = None
|
||||||
else:
|
else:
|
||||||
@@ -213,6 +274,29 @@ def main():
|
|||||||
changed += write_if_changed(target, text)
|
changed += write_if_changed(target, text)
|
||||||
dark.append(f"themes/qet-dark/scalable/{name}")
|
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.
|
||||||
|
for stale in sorted((THEMES / "qet-dark").glob("*/*")):
|
||||||
|
if stale.is_file() and stale.name != "index.theme" \
|
||||||
|
and str(stale.relative_to(ICO)) not in dark:
|
||||||
|
stale.unlink()
|
||||||
|
changed += 1
|
||||||
|
|
||||||
dirs = SIZES + ["scalable"]
|
dirs = SIZES + ["scalable"]
|
||||||
changed += write_if_changed(THEMES / "qet" / "index.theme",
|
changed += write_if_changed(THEMES / "qet" / "index.theme",
|
||||||
index_theme("qet", "QElectroTech icons", dirs))
|
index_theme("qet", "QElectroTech icons", dirs))
|
||||||
@@ -228,11 +312,13 @@ def main():
|
|||||||
qrc.append(f' <file alias="{alias}">{source}</file>')
|
qrc.append(f' <file alias="{alias}">{source}</file>')
|
||||||
for path in dark:
|
for path in dark:
|
||||||
qrc.append(f" <file>{path}</file>")
|
qrc.append(f" <file>{path}</file>")
|
||||||
|
for alias, source in dark_aliases:
|
||||||
|
qrc.append(f' <file alias="{alias}">{source}</file>')
|
||||||
qrc += [" </qresource>", "</RCC>", ""]
|
qrc += [" </qresource>", "</RCC>", ""]
|
||||||
changed += write_if_changed(QRC, "\n".join(qrc))
|
changed += write_if_changed(QRC, "\n".join(qrc))
|
||||||
|
|
||||||
print(f"{line_art} line-art icons, {colored} colored icons, {len(SVGS)} SVGs; "
|
print(f"{line_art} line-art icons, {light_art} light icons, {colored} colored icons, "
|
||||||
f"{changed} files written")
|
f"{len(SVGS)} SVGs; {changed} files written or removed")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# 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** — moves with deltas, adds, removes, relabels, conductor field changes |
|
||||||
|
| `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) |
|
||||||
|
|
||||||
|
Only `qet_export` launches 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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
## Notes and limits
|
||||||
|
|
||||||
|
- **The project database is not reachable from outside the application.**
|
||||||
|
`projectDataBase::newQuery()` and `isReadOnlySelect()` are C++-internal
|
||||||
|
and the JavaScript scripting API exposes no SQL binding, so structural
|
||||||
|
queries here are done over the XML. A `--query` CLI verb, or a scripting
|
||||||
|
binding, would let this server expose the guarded read-only SQL surface
|
||||||
|
instead, and would be a better foundation.
|
||||||
|
- **`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 made all 47 conductors of
|
||||||
|
an untouched `ArduinoLCD.qet` read as 29 removed and 29 re-added the
|
||||||
|
moment the other side had been through QElectroTech. Ends 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 re-saved but otherwise untouched project.
|
||||||
|
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.
|
||||||
|
- **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 rather
|
||||||
|
than as a move.
|
||||||
|
- Read-only by design. Nothing here writes to a project.
|
||||||
@@ -0,0 +1,721 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# 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/>.
|
||||||
|
"""
|
||||||
|
qet-mcp — a Model Context Protocol server over QElectroTech projects.
|
||||||
|
|
||||||
|
WHY THIS EXISTS
|
||||||
|
|
||||||
|
Verifying a QET change by screenshot is unreliable. Twice in one review
|
||||||
|
session a screenshot was read as showing a defect that the saved file
|
||||||
|
proved had not happened: once "dragging a multi-selection leaves the
|
||||||
|
symbols behind and detaches their labels" (the XML showed all four
|
||||||
|
elements moved and no label moved), and once "Apply does nothing" (Apply
|
||||||
|
was disabled because a required field was empty). Both times the pixels
|
||||||
|
misled and the model told the truth.
|
||||||
|
|
||||||
|
So the primary tools here read the *model*, not the screen, and the
|
||||||
|
primary tool is qet_diff: do the thing, then ask what actually changed.
|
||||||
|
|
||||||
|
DESIGN
|
||||||
|
|
||||||
|
Most tools parse the .qet XML directly and never launch QElectroTech.
|
||||||
|
That is deliberate: it is fast, deterministic, needs no display, and
|
||||||
|
cannot be confused by a dialog. Only qet_export shells out to the
|
||||||
|
binary, and it carries the launch traps with it (see _run_qet).
|
||||||
|
|
||||||
|
The project database would be a better query surface than XML, but it is
|
||||||
|
not reachable from outside the application: projectDataBase::newQuery()
|
||||||
|
and isReadOnlySelect() are C++-internal and the JavaScript scripting API
|
||||||
|
exposes no SQL binding. Until it does, structure lives here.
|
||||||
|
|
||||||
|
PROTOCOL
|
||||||
|
|
||||||
|
Line-delimited JSON-RPC 2.0 on stdin/stdout, per MCP's stdio transport.
|
||||||
|
Nothing but protocol goes to stdout; diagnostics go to stderr.
|
||||||
|
No third-party dependencies — the MCP SDK is not assumed to be present.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SERVER_NAME = "qet-mcp"
|
||||||
|
SERVER_VERSION = "0.1.0"
|
||||||
|
DEFAULT_PROTOCOL = "2025-06-18"
|
||||||
|
|
||||||
|
EXPORT_FORMATS = {
|
||||||
|
"pdf": "--export-pdf",
|
||||||
|
"png": "--export-png",
|
||||||
|
"svg": "--export-svg",
|
||||||
|
"bom": "--export-bom",
|
||||||
|
"cables": "--export-cables",
|
||||||
|
"wires": "--export-wires",
|
||||||
|
"wiring": "--export-wiring",
|
||||||
|
"nets": "--export-nets",
|
||||||
|
"links": "--export-links",
|
||||||
|
"info": "--info",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# model reading
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _root(path: str) -> ET.Element:
|
||||||
|
p = Path(path).expanduser()
|
||||||
|
if not p.is_file():
|
||||||
|
raise ValueError(f"no such file: {p}")
|
||||||
|
try:
|
||||||
|
return ET.parse(p).getroot()
|
||||||
|
except ET.ParseError as exc:
|
||||||
|
raise ValueError(f"{p.name} is not parseable XML: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _element_info(el: ET.Element) -> dict:
|
||||||
|
"""The <elementInformations> bag, as a plain dict."""
|
||||||
|
out = {}
|
||||||
|
bag = el.find("elementInformations")
|
||||||
|
if bag is not None:
|
||||||
|
for info in bag.findall("elementInformation"):
|
||||||
|
name = info.get("name")
|
||||||
|
if name:
|
||||||
|
out[name] = (info.text or "").strip()
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _folios(root: ET.Element):
|
||||||
|
"""Yield (index, diagram) for each folio, 1-based as the UI numbers them."""
|
||||||
|
for i, d in enumerate(root.iter("diagram"), start=1):
|
||||||
|
yield i, d
|
||||||
|
|
||||||
|
|
||||||
|
def _elements(root: ET.Element):
|
||||||
|
for i, d in _folios(root):
|
||||||
|
for el in d.iter("element"):
|
||||||
|
yield i, el
|
||||||
|
|
||||||
|
|
||||||
|
def _conductors(root: ET.Element):
|
||||||
|
for i, d in _folios(root):
|
||||||
|
index = _terminal_index(d)
|
||||||
|
for c in d.iter("conductor"):
|
||||||
|
yield i, c, index
|
||||||
|
|
||||||
|
|
||||||
|
def _element_row(folio: int, el: ET.Element) -> dict:
|
||||||
|
info = _element_info(el)
|
||||||
|
etype = el.get("type", "")
|
||||||
|
return {
|
||||||
|
"folio": folio,
|
||||||
|
"uuid": el.get("uuid", ""),
|
||||||
|
"type": etype,
|
||||||
|
"name": etype.rsplit("/", 1)[-1].removesuffix(".elmt"),
|
||||||
|
"x": el.get("x"),
|
||||||
|
"y": el.get("y"),
|
||||||
|
"label": info.get("label", ""),
|
||||||
|
"info": info,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _terminal_index(diagram: ET.Element) -> dict:
|
||||||
|
"""Map a folio's terminal ids to an identity that survives a save.
|
||||||
|
|
||||||
|
A conductor names its ends with terminal1/terminal2, which are plain
|
||||||
|
integers scoped to the folio -- and QElectroTech reassigns them on every
|
||||||
|
write, in whatever order it happens to serialise the elements. The same
|
||||||
|
untouched conductor comes back as terminal1="1" terminal2="16" before a
|
||||||
|
save and terminal1="34" terminal2="15" after one. Keying a conductor on
|
||||||
|
that pair, which this tool used to do, made every conductor in the file
|
||||||
|
read as removed-and-re-added whenever the "after" side had been through
|
||||||
|
QElectroTech -- which is the common case for "what did that edit change",
|
||||||
|
so the conductor half of the diff was noise precisely when it was needed.
|
||||||
|
|
||||||
|
So resolve each id to (owning element uuid, terminal position and
|
||||||
|
orientation inside that element). Element uuids are persisted and
|
||||||
|
stable; the terminal's local geometry comes from the element definition
|
||||||
|
and does not move when the element moves. That pair is the same basis
|
||||||
|
QET's own Terminal::stableUuid() uses for terminals with no uuid of
|
||||||
|
their own, and it is stable for the same reasons.
|
||||||
|
|
||||||
|
Conductors in the corpus carry no element1/element2 attribute -- 0 of
|
||||||
|
47 in ArduinoLCD.qet, 0 of 67 in 741.qet -- so this mapping has to be
|
||||||
|
built from the elements rather than read off the conductor.
|
||||||
|
"""
|
||||||
|
index = {}
|
||||||
|
for el in diagram.iter("element"):
|
||||||
|
uuid = el.get("uuid", "")
|
||||||
|
if not uuid:
|
||||||
|
# Old enough to predate persisted element uuids. Leaving these
|
||||||
|
# ids unresolved is deliberate: keyed on terminal geometry
|
||||||
|
# alone, every element of the same type collapses together --
|
||||||
|
# in schema_indus.qet that merged nine distinct conductors onto
|
||||||
|
# one key, which is worse than the instability it was meant to
|
||||||
|
# fix. An unresolved end keeps them apart and stays visibly
|
||||||
|
# marked with a "#" so the caller can see the diff is on the
|
||||||
|
# unstable footing that file forces.
|
||||||
|
continue
|
||||||
|
for t in el.iter("terminal"):
|
||||||
|
tid = t.get("id")
|
||||||
|
if tid is None:
|
||||||
|
continue
|
||||||
|
index[tid] = (f"{uuid}@{t.get('x','?')},{t.get('y','?')}"
|
||||||
|
f",{t.get('orientation','?')}")
|
||||||
|
return index
|
||||||
|
|
||||||
|
|
||||||
|
def _conductor_key(folio: int, c: ET.Element, index: dict) -> str:
|
||||||
|
"""Identify a conductor by its two ends, in whichever scheme it uses.
|
||||||
|
|
||||||
|
The project format has two, and a file can hold both at once -- the
|
||||||
|
same folio, after an edit, carries legacy conductors and new ones:
|
||||||
|
|
||||||
|
- legacy: terminal1/terminal2 are the folio-scoped integer ids, and
|
||||||
|
there is no element1/element2. Resolve them through index.
|
||||||
|
- current: terminal1/terminal2 are terminal uuids from the element
|
||||||
|
*definition*, with element1/element2 naming the placed instances.
|
||||||
|
The terminal uuid alone is not an identity -- two coils of the same
|
||||||
|
type have the same one on both ends, so a conductor between them
|
||||||
|
would key as a self-loop -- so it is the (instance, terminal) pair
|
||||||
|
that identifies an end.
|
||||||
|
"""
|
||||||
|
ends = []
|
||||||
|
for elem_attr, term_attr, name_attr in (("element1", "terminal1", "terminalname1"),
|
||||||
|
("element2", "terminal2", "terminalname2")):
|
||||||
|
tid = c.get(term_attr, "?")
|
||||||
|
owner = c.get(elem_attr)
|
||||||
|
if owner:
|
||||||
|
ends.append(f"{owner}/{tid or c.get(name_attr, '?')}")
|
||||||
|
else:
|
||||||
|
# An id with no element behind it stays visible as itself
|
||||||
|
# rather than silently collapsing conductors onto one key.
|
||||||
|
ends.append(index.get(tid, f"#{tid}"))
|
||||||
|
# A conductor is undirected: whichever end QET happens to write first,
|
||||||
|
# it is the same connection.
|
||||||
|
return f"{folio}:" + "--".join(sorted(ends))
|
||||||
|
|
||||||
|
|
||||||
|
def _conductor_row(folio: int, c: ET.Element, index: dict | None = None) -> dict:
|
||||||
|
return {
|
||||||
|
"folio": folio,
|
||||||
|
"uuid": c.get("uuid", ""),
|
||||||
|
"key": _conductor_key(folio, c, index or {}),
|
||||||
|
"num": c.get("num", ""),
|
||||||
|
"formula": c.get("formula", ""),
|
||||||
|
"cable": c.get("cable", ""),
|
||||||
|
"bus": c.get("bus", ""),
|
||||||
|
"function": c.get("function", ""),
|
||||||
|
"color": c.get("conductor_color", ""),
|
||||||
|
"section": c.get("conductor_section", ""),
|
||||||
|
"type": c.get("type", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# tools
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def tool_project_info(path: str) -> dict:
|
||||||
|
root = _root(path)
|
||||||
|
folios = []
|
||||||
|
for i, d in _folios(root):
|
||||||
|
folios.append({
|
||||||
|
"index": i,
|
||||||
|
"title": d.get("title", ""),
|
||||||
|
"elements": sum(1 for _ in d.iter("element")),
|
||||||
|
"conductors": sum(1 for _ in d.iter("conductor")),
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"file": str(Path(path).expanduser()),
|
||||||
|
"title": root.get("title", ""),
|
||||||
|
"version": root.get("version", ""),
|
||||||
|
"folio_count": len(folios),
|
||||||
|
"element_count": sum(f["elements"] for f in folios),
|
||||||
|
"conductor_count": sum(f["conductors"] for f in folios),
|
||||||
|
"folios": folios,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def tool_elements(path: str, folio: int | None = None,
|
||||||
|
name_contains: str | None = None, limit: int = 200) -> dict:
|
||||||
|
rows = []
|
||||||
|
for i, el in _elements(_root(path)):
|
||||||
|
if folio is not None and i != folio:
|
||||||
|
continue
|
||||||
|
row = _element_row(i, el)
|
||||||
|
if name_contains and name_contains.lower() not in row["name"].lower():
|
||||||
|
continue
|
||||||
|
rows.append(row)
|
||||||
|
return {"count": len(rows), "truncated": len(rows) > limit,
|
||||||
|
"elements": rows[:limit]}
|
||||||
|
|
||||||
|
|
||||||
|
def tool_conductors(path: str, folio: int | None = None,
|
||||||
|
attribute: str | None = None,
|
||||||
|
non_empty: bool = False, limit: int = 200) -> dict:
|
||||||
|
rows = []
|
||||||
|
for i, c, ix in _conductors(_root(path)):
|
||||||
|
if folio is not None and i != folio:
|
||||||
|
continue
|
||||||
|
row = _conductor_row(i, c, ix)
|
||||||
|
if attribute is not None:
|
||||||
|
value = c.get(attribute, "")
|
||||||
|
if non_empty and not value.strip():
|
||||||
|
continue
|
||||||
|
row["value"] = value
|
||||||
|
rows.append(row)
|
||||||
|
return {"count": len(rows), "truncated": len(rows) > limit,
|
||||||
|
"conductors": rows[:limit]}
|
||||||
|
|
||||||
|
|
||||||
|
def tool_diff(before: str, after: str) -> dict:
|
||||||
|
"""Structural diff of two .qet files.
|
||||||
|
|
||||||
|
This is the tool that answers "what did that edit actually change",
|
||||||
|
which is the question a screenshot answers badly.
|
||||||
|
"""
|
||||||
|
# Key on uuid where there is one. Files written before conductors and
|
||||||
|
# elements carried persisted uuids fall back to a positional key, which
|
||||||
|
# is why a move in such a file reads as remove+add rather than a move.
|
||||||
|
a_el, b_el = {}, {}
|
||||||
|
for i, e in _elements(_root(before)):
|
||||||
|
r = _element_row(i, e)
|
||||||
|
a_el[r["uuid"] or f"{i}:{r['x']},{r['y']}:{r['name']}"] = r
|
||||||
|
for i, e in _elements(_root(after)):
|
||||||
|
r = _element_row(i, e)
|
||||||
|
b_el[r["uuid"] or f"{i}:{r['x']},{r['y']}:{r['name']}"] = r
|
||||||
|
|
||||||
|
moved, relabelled, changed_info = [], [], []
|
||||||
|
for k, a in a_el.items():
|
||||||
|
b = b_el.get(k)
|
||||||
|
if b is None:
|
||||||
|
continue
|
||||||
|
if (a["x"], a["y"]) != (b["x"], b["y"]):
|
||||||
|
moved.append({
|
||||||
|
"uuid": k, "name": a["name"], "folio": a["folio"],
|
||||||
|
"from": [a["x"], a["y"]], "to": [b["x"], b["y"]],
|
||||||
|
"delta": [_num(b["x"]) - _num(a["x"]),
|
||||||
|
_num(b["y"]) - _num(a["y"])],
|
||||||
|
})
|
||||||
|
if a["label"] != b["label"]:
|
||||||
|
relabelled.append({"uuid": k, "name": a["name"],
|
||||||
|
"from": a["label"], "to": b["label"]})
|
||||||
|
if a["info"] != b["info"]:
|
||||||
|
changed_info.append({"uuid": k, "name": a["name"],
|
||||||
|
"from": a["info"], "to": b["info"]})
|
||||||
|
|
||||||
|
a_co = {r["key"]: r for i, c, ix in _conductors(_root(before))
|
||||||
|
for r in [_conductor_row(i, c, ix)]}
|
||||||
|
b_co = {r["key"]: r for i, c, ix in _conductors(_root(after))
|
||||||
|
for r in [_conductor_row(i, c, ix)]}
|
||||||
|
# An end that could not be resolved to an element is keyed on the
|
||||||
|
# folio-scoped integer id, which QElectroTech reassigns on every write.
|
||||||
|
# Say so rather than presenting the result as if it were comparable:
|
||||||
|
# in such a file an untouched conductor can read as removed and re-added.
|
||||||
|
shaky = sum(1 for k in set(a_co) | set(b_co) if "#" in k)
|
||||||
|
unstable = {} if not shaky else {
|
||||||
|
"unstable_keys": shaky,
|
||||||
|
"warning": "some conductors sit on elements with no persisted uuid, so "
|
||||||
|
"they are keyed on folio-scoped terminal ids that "
|
||||||
|
"QElectroTech renumbers on save; added/removed entries "
|
||||||
|
"marked with # may be the same conductor, not a change",
|
||||||
|
}
|
||||||
|
conductor_changes = []
|
||||||
|
for k, a in a_co.items():
|
||||||
|
b = b_co.get(k)
|
||||||
|
if b is None:
|
||||||
|
continue
|
||||||
|
fields = {f: [a[f], b[f]] for f in
|
||||||
|
("num", "formula", "cable", "bus", "color", "section",
|
||||||
|
"function", "type")
|
||||||
|
if a[f] != b[f]}
|
||||||
|
if fields:
|
||||||
|
conductor_changes.append({"key": k, "changed": fields})
|
||||||
|
|
||||||
|
deltas = sorted({tuple(m["delta"]) for m in moved})
|
||||||
|
return {
|
||||||
|
"elements": {
|
||||||
|
"before": len(a_el), "after": len(b_el),
|
||||||
|
"added": sorted(set(b_el) - set(a_el))[:50],
|
||||||
|
"removed": sorted(set(a_el) - set(b_el))[:50],
|
||||||
|
"moved": moved[:100],
|
||||||
|
"moved_count": len(moved),
|
||||||
|
"distinct_move_deltas": [list(d) for d in deltas],
|
||||||
|
"relabelled": relabelled[:50],
|
||||||
|
"info_changed": changed_info[:50],
|
||||||
|
},
|
||||||
|
"conductors": {
|
||||||
|
"before": len(a_co), "after": len(b_co),
|
||||||
|
"added": sorted(set(b_co) - set(a_co))[:50],
|
||||||
|
"removed": sorted(set(a_co) - set(b_co))[:50],
|
||||||
|
"changed": conductor_changes[:100],
|
||||||
|
"changed_count": len(conductor_changes),
|
||||||
|
**unstable,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _num(v) -> float:
|
||||||
|
try:
|
||||||
|
return float(v)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def tool_scan(directory: str, tag: str = "conductor",
|
||||||
|
attribute: str = "cable", recursive: bool = True) -> dict:
|
||||||
|
"""Sweep every .qet in a directory, counting how many <tag> carry a
|
||||||
|
non-empty `attribute`.
|
||||||
|
|
||||||
|
This is the corpus question: "3190 conductors, 0 cable values across
|
||||||
|
25 projects" is exactly one call to this tool.
|
||||||
|
"""
|
||||||
|
d = Path(directory).expanduser()
|
||||||
|
if not d.is_dir():
|
||||||
|
raise ValueError(f"not a directory: {d}")
|
||||||
|
files = sorted(d.rglob("*.qet") if recursive else d.glob("*.qet"))
|
||||||
|
total = non_empty = 0
|
||||||
|
per_file, values, unreadable = [], {}, []
|
||||||
|
for f in files:
|
||||||
|
try:
|
||||||
|
root = ET.parse(f).getroot()
|
||||||
|
except ET.ParseError as exc:
|
||||||
|
unreadable.append({"file": f.name, "error": str(exc)})
|
||||||
|
continue
|
||||||
|
n = ne = 0
|
||||||
|
for node in root.iter(tag):
|
||||||
|
n += 1
|
||||||
|
v = (node.get(attribute) or "").strip()
|
||||||
|
if v:
|
||||||
|
ne += 1
|
||||||
|
values[v] = values.get(v, 0) + 1
|
||||||
|
total += n
|
||||||
|
non_empty += ne
|
||||||
|
per_file.append({"file": f.name, tag: n, "non_empty": ne})
|
||||||
|
return {
|
||||||
|
"directory": str(d), "files": len(files), "unreadable": unreadable,
|
||||||
|
"tag": tag, "attribute": attribute,
|
||||||
|
"total": total, "non_empty": non_empty,
|
||||||
|
"distinct_values": sorted(values.items(), key=lambda kv: -kv[1])[:25],
|
||||||
|
"per_file": per_file,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def tool_element_info(path: str) -> dict:
|
||||||
|
"""Introspect a .elmt: names, terminals, and which info fields it carries."""
|
||||||
|
root = _root(path)
|
||||||
|
names = {n.get("lang"): (n.text or "") for n in root.iter("name")}
|
||||||
|
terminals = [{"x": t.get("x"), "y": t.get("y"),
|
||||||
|
"orientation": t.get("orientation"),
|
||||||
|
"name": t.get("name", ""), "type": t.get("type", "")}
|
||||||
|
for t in root.iter("terminal")]
|
||||||
|
info_fields = sorted({(i.text or "").strip()
|
||||||
|
for i in root.iter("info_name") if (i.text or "").strip()})
|
||||||
|
parts = {}
|
||||||
|
desc = root.find("description")
|
||||||
|
for child in (desc if desc is not None else []):
|
||||||
|
parts[child.tag] = parts.get(child.tag, 0) + 1
|
||||||
|
return {
|
||||||
|
"file": str(Path(path).expanduser()),
|
||||||
|
"type": root.get("type", ""), "link_type": root.get("link_type", ""),
|
||||||
|
"width": root.get("width"), "height": root.get("height"),
|
||||||
|
"names": names,
|
||||||
|
"terminal_count": len(terminals), "terminals": terminals,
|
||||||
|
"info_fields": info_fields,
|
||||||
|
"parts": parts,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _run_qet(binary: str, args: list[str], timeout: int = 180) -> dict:
|
||||||
|
"""Launch QElectroTech headlessly, carrying the known launch traps.
|
||||||
|
|
||||||
|
SingleApplication keys its socket on applicationFilePath(), so a second
|
||||||
|
launch of the same path forwards its request to an already-running
|
||||||
|
instance and returns THAT process's answer with no error. Copying the
|
||||||
|
binary to a unique path gives this run its own socket. A symlink will
|
||||||
|
not do: applicationFilePath() resolves it back.
|
||||||
|
"""
|
||||||
|
src = Path(binary).expanduser()
|
||||||
|
if not src.is_file() or not os.access(src, os.X_OK):
|
||||||
|
raise ValueError(f"not an executable: {src}")
|
||||||
|
with tempfile.TemporaryDirectory(prefix="qet-mcp-") as tmp:
|
||||||
|
sandbox = Path(tmp)
|
||||||
|
exe = sandbox / f"qet-mcp-{os.getpid()}"
|
||||||
|
shutil.copy2(src, exe)
|
||||||
|
home = sandbox / "home"
|
||||||
|
(home / ".config").mkdir(parents=True)
|
||||||
|
(home / ".local" / "share").mkdir(parents=True)
|
||||||
|
env = dict(os.environ,
|
||||||
|
HOME=str(home),
|
||||||
|
XDG_CONFIG_HOME=str(home / ".config"),
|
||||||
|
XDG_DATA_HOME=str(home / ".local" / "share"),
|
||||||
|
QT_QPA_PLATFORM="offscreen")
|
||||||
|
try:
|
||||||
|
p = subprocess.run([str(exe), *args], env=env, timeout=timeout,
|
||||||
|
capture_output=True, text=True)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return {"ok": False, "timed_out": True, "timeout_s": timeout,
|
||||||
|
"hint": "a modal dialog during load will hang a headless "
|
||||||
|
"run; check the project's format version"}
|
||||||
|
return {"ok": p.returncode == 0, "exit_code": p.returncode,
|
||||||
|
"stdout": p.stdout[-4000:], "stderr": p.stderr[-4000:]}
|
||||||
|
|
||||||
|
|
||||||
|
def tool_export(binary: str, project: str, format: str, output: str,
|
||||||
|
timeout: int = 180) -> dict:
|
||||||
|
if format not in EXPORT_FORMATS:
|
||||||
|
raise ValueError(f"unknown format {format!r}; "
|
||||||
|
f"expected one of {', '.join(sorted(EXPORT_FORMATS))}")
|
||||||
|
proj = Path(project).expanduser()
|
||||||
|
if not proj.is_file():
|
||||||
|
raise ValueError(f"no such project: {proj}")
|
||||||
|
# The CLI matches its flags by exact string (cli_export.cpp:828) and takes
|
||||||
|
# the project and output as the two positional arguments after the flag
|
||||||
|
# (:862, :882). A "--export-bom=out.csv" form is NOT recognised: it fails
|
||||||
|
# the flag test, so the run is not treated as an export at all and the
|
||||||
|
# application starts its GUI instead, which then hangs on an offscreen
|
||||||
|
# platform. Order matters here.
|
||||||
|
flag = EXPORT_FORMATS[format]
|
||||||
|
result = _run_qet(binary, [flag, str(proj), output], timeout)
|
||||||
|
out = Path(output).expanduser()
|
||||||
|
result["output"] = str(out)
|
||||||
|
result["output_exists"] = out.exists()
|
||||||
|
if out.exists() and out.is_file():
|
||||||
|
result["output_bytes"] = out.stat().st_size
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
TOOLS = [
|
||||||
|
{
|
||||||
|
"name": "qet_project_info",
|
||||||
|
"description": "Summarise a .qet project: title, format version, folios, "
|
||||||
|
"and element/conductor counts per folio. Reads the file "
|
||||||
|
"directly; does not launch QElectroTech.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"path": {"type": "string", "description": "path to a .qet file"}},
|
||||||
|
"required": ["path"],
|
||||||
|
},
|
||||||
|
"handler": lambda a: tool_project_info(a["path"]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "qet_elements",
|
||||||
|
"description": "List placed elements with uuid, type, position, label and "
|
||||||
|
"their elementInformations bag. Optionally filter by folio "
|
||||||
|
"or by element name substring.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {"type": "string"},
|
||||||
|
"folio": {"type": "integer", "description": "1-based folio number"},
|
||||||
|
"name_contains": {"type": "string"},
|
||||||
|
"limit": {"type": "integer", "default": 200},
|
||||||
|
},
|
||||||
|
"required": ["path"],
|
||||||
|
},
|
||||||
|
"handler": lambda a: tool_elements(a["path"], a.get("folio"),
|
||||||
|
a.get("name_contains"),
|
||||||
|
a.get("limit", 200)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "qet_conductors",
|
||||||
|
"description": "List conductors with their documentation fields (num, "
|
||||||
|
"formula, cable, bus, function, colour, section). Set "
|
||||||
|
"attribute+non_empty to find only conductors that carry a "
|
||||||
|
"value for one attribute.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {"type": "string"},
|
||||||
|
"folio": {"type": "integer"},
|
||||||
|
"attribute": {"type": "string",
|
||||||
|
"description": "an XML attribute of <conductor>, e.g. cable"},
|
||||||
|
"non_empty": {"type": "boolean", "default": False},
|
||||||
|
"limit": {"type": "integer", "default": 200},
|
||||||
|
},
|
||||||
|
"required": ["path"],
|
||||||
|
},
|
||||||
|
"handler": lambda a: tool_conductors(a["path"], a.get("folio"),
|
||||||
|
a.get("attribute"),
|
||||||
|
a.get("non_empty", False),
|
||||||
|
a.get("limit", 200)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "qet_diff",
|
||||||
|
"description": "Structurally diff two .qet files: which elements moved and "
|
||||||
|
"by what delta, which were added, removed or relabelled, and "
|
||||||
|
"which conductor fields changed. Use this to verify what an "
|
||||||
|
"edit actually did, rather than reading a screenshot.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"before": {"type": "string"},
|
||||||
|
"after": {"type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["before", "after"],
|
||||||
|
},
|
||||||
|
"handler": lambda a: tool_diff(a["before"], a["after"]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "qet_scan",
|
||||||
|
"description": "Sweep every .qet in a directory and count how many nodes of "
|
||||||
|
"a given tag carry a non-empty attribute, with the distinct "
|
||||||
|
"values found. For corpus questions such as how many "
|
||||||
|
"conductors in the shipped examples have a cable value.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"directory": {"type": "string"},
|
||||||
|
"tag": {"type": "string", "default": "conductor"},
|
||||||
|
"attribute": {"type": "string", "default": "cable"},
|
||||||
|
"recursive": {"type": "boolean", "default": True},
|
||||||
|
},
|
||||||
|
"required": ["directory"],
|
||||||
|
},
|
||||||
|
"handler": lambda a: tool_scan(a["directory"], a.get("tag", "conductor"),
|
||||||
|
a.get("attribute", "cable"),
|
||||||
|
a.get("recursive", True)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "qet_element_info",
|
||||||
|
"description": "Introspect a .elmt element definition: translated names, "
|
||||||
|
"terminals, which dynamic-text info fields it carries, and a "
|
||||||
|
"count of its drawing parts.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"path": {"type": "string", "description": "path to a .elmt file"}},
|
||||||
|
"required": ["path"],
|
||||||
|
},
|
||||||
|
"handler": lambda a: tool_element_info(a["path"]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "qet_export",
|
||||||
|
"description": "Run a QElectroTech export headlessly (pdf, png, svg, bom, "
|
||||||
|
"cables, wires, wiring, nets, links, info). Launches the "
|
||||||
|
"binary in an isolated sandbox so it cannot be captured by, "
|
||||||
|
"or capture, a running QElectroTech.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"binary": {"type": "string", "description": "path to the qelectrotech executable"},
|
||||||
|
"project": {"type": "string"},
|
||||||
|
"format": {"type": "string", "enum": sorted(EXPORT_FORMATS)},
|
||||||
|
"output": {"type": "string"},
|
||||||
|
"timeout": {"type": "integer", "default": 180},
|
||||||
|
},
|
||||||
|
"required": ["binary", "project", "format", "output"],
|
||||||
|
},
|
||||||
|
"handler": lambda a: tool_export(a["binary"], a["project"], a["format"],
|
||||||
|
a["output"], a.get("timeout", 180)),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
_BY_NAME = {t["name"]: t for t in TOOLS}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# JSON-RPC / MCP plumbing
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _public(tool: dict) -> dict:
|
||||||
|
return {k: v for k, v in tool.items() if k != "handler"}
|
||||||
|
|
||||||
|
|
||||||
|
def handle(msg: dict) -> dict | None:
|
||||||
|
method = msg.get("method")
|
||||||
|
mid = msg.get("id")
|
||||||
|
|
||||||
|
if method == "initialize":
|
||||||
|
want = (msg.get("params") or {}).get("protocolVersion")
|
||||||
|
return _ok(mid, {
|
||||||
|
"protocolVersion": want or DEFAULT_PROTOCOL,
|
||||||
|
"capabilities": {"tools": {}},
|
||||||
|
"serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
|
||||||
|
})
|
||||||
|
|
||||||
|
if method in ("notifications/initialized", "initialized"):
|
||||||
|
return None # notification: no reply
|
||||||
|
|
||||||
|
if method == "ping":
|
||||||
|
return _ok(mid, {})
|
||||||
|
|
||||||
|
if method == "tools/list":
|
||||||
|
return _ok(mid, {"tools": [_public(t) for t in TOOLS]})
|
||||||
|
|
||||||
|
if method == "tools/call":
|
||||||
|
params = msg.get("params") or {}
|
||||||
|
name = params.get("name")
|
||||||
|
tool = _BY_NAME.get(name)
|
||||||
|
if tool is None:
|
||||||
|
return _err(mid, -32602, f"unknown tool: {name}")
|
||||||
|
try:
|
||||||
|
result = tool["handler"](params.get("arguments") or {})
|
||||||
|
text = json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
return _ok(mid, {"content": [{"type": "text", "text": text}]})
|
||||||
|
except Exception as exc: # surfaced to the model, not the transport
|
||||||
|
return _ok(mid, {
|
||||||
|
"isError": True,
|
||||||
|
"content": [{"type": "text",
|
||||||
|
"text": f"{type(exc).__name__}: {exc}"}],
|
||||||
|
})
|
||||||
|
|
||||||
|
if mid is None:
|
||||||
|
return None
|
||||||
|
return _err(mid, -32601, f"method not found: {method}")
|
||||||
|
|
||||||
|
|
||||||
|
def _ok(mid, result):
|
||||||
|
return {"jsonrpc": "2.0", "id": mid, "result": result}
|
||||||
|
|
||||||
|
|
||||||
|
def _err(mid, code, message):
|
||||||
|
return {"jsonrpc": "2.0", "id": mid, "error": {"code": code, "message": message}}
|
||||||
|
|
||||||
|
|
||||||
|
def serve(stdin=sys.stdin, stdout=sys.stdout) -> None:
|
||||||
|
for line in stdin:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
msg = json.loads(line)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
print(json.dumps(_err(None, -32700, f"parse error: {exc}")),
|
||||||
|
file=stdout, flush=True)
|
||||||
|
continue
|
||||||
|
reply = handle(msg)
|
||||||
|
if reply is not None:
|
||||||
|
print(json.dumps(reply, ensure_ascii=False), file=stdout, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] in ("--list", "-l"):
|
||||||
|
for t in TOOLS:
|
||||||
|
print(f"{t['name']}\n {t['description']}\n")
|
||||||
|
return 0
|
||||||
|
serve()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -93,10 +93,7 @@
|
|||||||
<file>ico/22x22/conductor2.png</file>
|
<file>ico/22x22/conductor2.png</file>
|
||||||
<file>ico/22x22/configure.png</file>
|
<file>ico/22x22/configure.png</file>
|
||||||
<file>ico/22x22/configure-toolbars.png</file>
|
<file>ico/22x22/configure-toolbars.png</file>
|
||||||
<file>ico/22x22/diagram_add.png</file>
|
|
||||||
<file>ico/22x22/diagram_del.png</file>
|
|
||||||
<file>ico/22x22/dialog-cancel.png</file>
|
<file>ico/22x22/dialog-cancel.png</file>
|
||||||
<file>ico/22x22/dialog-information.png</file>
|
|
||||||
<file>ico/22x22/dialog-ok.png</file>
|
<file>ico/22x22/dialog-ok.png</file>
|
||||||
<file>ico/22x22/document-close.png</file>
|
<file>ico/22x22/document-close.png</file>
|
||||||
<file>ico/22x22/document-export.png</file>
|
<file>ico/22x22/document-export.png</file>
|
||||||
@@ -140,8 +137,6 @@
|
|||||||
<file>ico/22x22/go-up.png</file>
|
<file>ico/22x22/go-up.png</file>
|
||||||
<file>ico/22x22/hotspot.png</file>
|
<file>ico/22x22/hotspot.png</file>
|
||||||
<file>ico/22x22/insert-image.png</file>
|
<file>ico/22x22/insert-image.png</file>
|
||||||
<file>ico/22x22/pdf-import.png</file>
|
|
||||||
<file>ico/22x22/label.png</file>
|
|
||||||
<file>ico/22x22/landscape.png</file>
|
<file>ico/22x22/landscape.png</file>
|
||||||
<file>ico/22x22/line.png</file>
|
<file>ico/22x22/line.png</file>
|
||||||
<file>ico/22x22/list-add.png</file>
|
<file>ico/22x22/list-add.png</file>
|
||||||
@@ -242,7 +237,6 @@
|
|||||||
<file>ico/16x16/project.png</file>
|
<file>ico/16x16/project.png</file>
|
||||||
<file>ico/16x16/qt.png</file>
|
<file>ico/16x16/qt.png</file>
|
||||||
<file>ico/16x16/terminalstrip.png</file>
|
<file>ico/16x16/terminalstrip.png</file>
|
||||||
<file>ico/22x22/diagram.png</file>
|
|
||||||
<file>ico/22x22/edit-clear-locationbar-rtl.png</file>
|
<file>ico/22x22/edit-clear-locationbar-rtl.png</file>
|
||||||
<file>ico/22x22/export-csv.png</file>
|
<file>ico/22x22/export-csv.png</file>
|
||||||
<file>ico/22x22/format-text-subscript.png</file>
|
<file>ico/22x22/format-text-subscript.png</file>
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
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 "elementpreviewdelegate.h"
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "../qetpalette.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief ElementPreviewDelegate::initStyleOption
|
||||||
|
After the base class has filled the option from the model, replace
|
||||||
|
the icon with one that reads on a dark palette. The adapted icon is
|
||||||
|
built from the pixmap the view is about to draw, at the view's
|
||||||
|
decoration size and device pixel ratio.
|
||||||
|
*/
|
||||||
|
void ElementPreviewDelegate::initStyleOption(QStyleOptionViewItem *option,
|
||||||
|
const QModelIndex &index) const
|
||||||
|
{
|
||||||
|
QStyledItemDelegate::initStyleOption(option, index);
|
||||||
|
if (option->icon.isNull() || !QET::Palette::isDark(option->palette))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const qint64 key = option->icon.cacheKey();
|
||||||
|
const auto it = m_dark_icons.constFind(key);
|
||||||
|
if (it != m_dark_icons.constEnd())
|
||||||
|
{
|
||||||
|
option->icon = *it;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qreal dpr = option->widget ? option->widget->devicePixelRatio() : 1.0;
|
||||||
|
const QPixmap source = option->icon.pixmap(option->decorationSize, dpr);
|
||||||
|
const QPixmap adapted = QET::Palette::forPalette(source, option->palette);
|
||||||
|
QIcon icon = option->icon;
|
||||||
|
if (adapted.cacheKey() != source.cacheKey())
|
||||||
|
icon = QIcon(adapted);
|
||||||
|
m_dark_icons.insert(key, icon);
|
||||||
|
option->icon = icon;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
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 ELEMENTPREVIEWDELEGATE_H
|
||||||
|
#define ELEMENTPREVIEWDELEGATE_H
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QStyledItemDelegate>
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief The ElementPreviewDelegate class
|
||||||
|
Draws the items of an element collection tree with icons that read on
|
||||||
|
the current palette. Element previews are black line art drawn for a
|
||||||
|
white sheet; on a dark palette this delegate hands the view the same
|
||||||
|
picture with its lightness inverted (QET::Palette::forPalette), so the
|
||||||
|
ink is light on the dark row. Colored icons, such as folders, are left
|
||||||
|
alone, and nothing changes on a light palette. Adapted icons are kept
|
||||||
|
per source icon, so a repaint costs a hash lookup.
|
||||||
|
*/
|
||||||
|
class ElementPreviewDelegate : public QStyledItemDelegate
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
using QStyledItemDelegate::QStyledItemDelegate;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void initStyleOption(QStyleOptionViewItem *option,
|
||||||
|
const QModelIndex &index) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable QHash<qint64, QIcon> m_dark_icons;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -725,7 +725,10 @@ void ElementsCollectionWidget::showThisDir()
|
|||||||
ElementCollectionItem *eci =
|
ElementCollectionItem *eci =
|
||||||
elementCollectionItemForIndex(m_showed_index);
|
elementCollectionItemForIndex(m_showed_index);
|
||||||
if (eci)
|
if (eci)
|
||||||
|
{
|
||||||
eci->setBackground(QBrush());
|
eci->setBackground(QBrush());
|
||||||
|
eci->setForeground(QBrush());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_showed_index = m_index_at_context_menu;
|
m_showed_index = m_index_at_context_menu;
|
||||||
@@ -736,7 +739,11 @@ void ElementsCollectionWidget::showThisDir()
|
|||||||
ElementCollectionItem *eci =
|
ElementCollectionItem *eci =
|
||||||
elementCollectionItemForIndex(m_showed_index);
|
elementCollectionItemForIndex(m_showed_index);
|
||||||
if (eci)
|
if (eci)
|
||||||
|
{
|
||||||
|
// Amber under black, whatever the palette's text color.
|
||||||
eci->setBackground(QBrush(QColor(255, 204, 0, 255)));
|
eci->setBackground(QBrush(QColor(255, 204, 0, 255)));
|
||||||
|
eci->setForeground(QBrush(Qt::black));
|
||||||
|
}
|
||||||
search();
|
search();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -755,7 +762,10 @@ void ElementsCollectionWidget::resetShowThisDir()
|
|||||||
ElementCollectionItem *eci = elementCollectionItemForIndex(
|
ElementCollectionItem *eci = elementCollectionItemForIndex(
|
||||||
m_showed_index);
|
m_showed_index);
|
||||||
if (eci)
|
if (eci)
|
||||||
|
{
|
||||||
eci->setBackground(QBrush());
|
eci->setBackground(QBrush());
|
||||||
|
eci->setForeground(QBrush());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_showed_index = QModelIndex();
|
m_showed_index = QModelIndex();
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
#include "elementstreeview.h"
|
#include "elementstreeview.h"
|
||||||
|
#include "elementpreviewdelegate.h"
|
||||||
|
#include "../qetpalette.h"
|
||||||
|
|
||||||
#include "../factory/elementfactory.h"
|
#include "../factory/elementfactory.h"
|
||||||
#include "../qetgraphicsitem/element.h"
|
#include "../qetgraphicsitem/element.h"
|
||||||
@@ -41,24 +43,11 @@ static int MAX_DND_PIXMAP_HEIGHT = 375;
|
|||||||
ElementsTreeView::ElementsTreeView(QWidget *parent) :
|
ElementsTreeView::ElementsTreeView(QWidget *parent) :
|
||||||
QTreeView(parent)
|
QTreeView(parent)
|
||||||
{
|
{
|
||||||
// force du noir sur une alternance de blanc (comme le schema) et de gris
|
// Rows follow the application palette. Element previews are black
|
||||||
// clair, avec du blanc sur bleu pas trop fonce pour la selection
|
// line art drawn for a white sheet; ElementPreviewDelegate adapts them
|
||||||
//
|
// to a dark palette, so this view no longer has to force a light one
|
||||||
// Element icons are rendered with colors read directly from each .elmt
|
// (bugtracker #335).
|
||||||
// file (almost always black linework, matching printed-schematic
|
setItemDelegate(new ElementPreviewDelegate(this));
|
||||||
// convention) onto a transparent background -- so this view must keep
|
|
||||||
// a light background regardless of the OS/desktop theme, or the icons
|
|
||||||
// become invisible on dark themes. QAbstractItemView paints its rows
|
|
||||||
// using the viewport's palette, not the view's own, so the palette
|
|
||||||
// must be applied to both to actually take effect under every style.
|
|
||||||
QPalette qp = palette();
|
|
||||||
qp.setColor(QPalette::Text, Qt::black);
|
|
||||||
qp.setColor(QPalette::Base, Qt::white);
|
|
||||||
qp.setColor(QPalette::AlternateBase, QColor("#e8e8e8"));
|
|
||||||
qp.setColor(QPalette::Highlight, QColor("#678db2"));
|
|
||||||
qp.setColor(QPalette::HighlightedText, Qt::black);
|
|
||||||
setPalette(qp);
|
|
||||||
viewport()->setPalette(qp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -220,7 +209,7 @@ void ElementsTreeView::startElementDrag(const ElementsLocation &location)
|
|||||||
&elmt_creation_state));
|
&elmt_creation_state));
|
||||||
if (elmt_creation_state) { return; }
|
if (elmt_creation_state) { return; }
|
||||||
|
|
||||||
QPixmap elmt_pixmap(temp_elmt->pixmap());
|
QPixmap elmt_pixmap(QET::Palette::forPalette(temp_elmt->pixmap(), palette()));
|
||||||
QPoint elmt_hotspot(temp_elmt->hotspot());
|
QPoint elmt_hotspot(temp_elmt->hotspot());
|
||||||
|
|
||||||
//Adjust the size of the pixmap if he is too big
|
//Adjust the size of the pixmap if he is too big
|
||||||
|
|||||||
@@ -35,13 +35,17 @@ AddTerminalToStripCommand::AddTerminalToStripCommand(QSharedPointer<RealTerminal
|
|||||||
const auto t_label = terminal->label();
|
const auto t_label = terminal->label();
|
||||||
const auto ts_name = strip->name();
|
const auto ts_name = strip->name();
|
||||||
|
|
||||||
const auto str_1 = t_label.isEmpty() ? QObject::tr("Ajouter une borne") :
|
QString text;
|
||||||
QObject::tr("Ajouter la borne %1").arg(t_label);
|
if (ts_name.isEmpty()) {
|
||||||
|
text = t_label.isEmpty()
|
||||||
const auto str_2 = ts_name.isEmpty() ? QObject::tr("à un groupe de bornes") :
|
? QObject::tr("Ajouter une borne à un groupe de bornes")
|
||||||
QObject::tr("au groupe de bornes %1").arg(ts_name);
|
: QObject::tr("Ajouter la borne %1 à un groupe de bornes").arg(t_label);
|
||||||
|
} else {
|
||||||
setText(str_1 % " " % str_2);
|
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) :
|
AddTerminalToStripCommand::AddTerminalToStripCommand(QVector<QSharedPointer<RealTerminal>> terminals, TerminalStrip *strip, QUndoCommand *parent) :
|
||||||
@@ -50,14 +54,11 @@ AddTerminalToStripCommand::AddTerminalToStripCommand(QVector<QSharedPointer<Real
|
|||||||
m_new_strip{strip}
|
m_new_strip{strip}
|
||||||
{
|
{
|
||||||
const auto ts_name = strip->name();
|
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()) :
|
setText(ts_name.isEmpty()
|
||||||
QObject::tr("Ajouter une borne");
|
? 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));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -137,13 +138,11 @@ void RemoveTerminalFromStripCommand::redo()
|
|||||||
void RemoveTerminalFromStripCommand::setCommandTitle()
|
void RemoveTerminalFromStripCommand::setCommandTitle()
|
||||||
{
|
{
|
||||||
const auto strip_name = m_strip->name();
|
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()):
|
setText(strip_name.isEmpty()
|
||||||
QObject::tr("Enlever une borne");
|
? 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));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,19 +165,30 @@ MoveTerminalCommand::MoveTerminalCommand(QSharedPointer<PhysicalTerminal> termin
|
|||||||
t_label.append(", ");
|
t_label.append(", ");
|
||||||
t_label.append(real_t->label());
|
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();
|
QString text;
|
||||||
auto new_strip_name = new_strip->name();
|
if (t_label.isEmpty()) {
|
||||||
|
if (strip_name.isEmpty() && new_strip_name.isEmpty())
|
||||||
auto str_1 = t_label.isEmpty() ? QObject::tr("Déplacer une borne") :
|
text = QObject::tr("Déplacer une borne d'un groupe de bornes vers un groupe de bornes");
|
||||||
QObject::tr("Déplacer la borne %1").arg(t_label);
|
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);
|
||||||
auto str_2 = strip_name.isEmpty() ? QObject::tr(" d'un groupe de bornes") :
|
else if (new_strip_name.isEmpty())
|
||||||
QObject::tr(" du groupe de bornes %1").arg(strip_name);
|
text = QObject::tr("Déplacer une borne du groupe de bornes %1 vers un groupe de bornes").arg(strip_name);
|
||||||
|
else
|
||||||
auto str_3 = new_strip_name.isEmpty() ? QObject::tr("vers un groupe de bornes") :
|
text = QObject::tr("Déplacer une borne du groupe de bornes %1 vers le groupe de bornes %2").arg(strip_name, new_strip_name);
|
||||||
QObject::tr("vers le groupe de bornes %1").arg(new_strip_name);
|
} else {
|
||||||
setText(str_1 % " " % str_2 % " " % str_3);
|
if (strip_name.isEmpty() && new_strip_name.isEmpty())
|
||||||
|
text = QObject::tr("Déplacer la borne %1 d'un groupe de bornes vers un 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 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,
|
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 strip_name = old_strip->name();
|
||||||
const auto new_strip_name = new_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") :
|
QString text;
|
||||||
QObject::tr("Déplacer une borne");
|
if (strip_name.isEmpty() && new_strip_name.isEmpty())
|
||||||
|
text = QObject::tr("Déplacer %n borne(s) d'un groupe de bornes vers un groupe de bornes", "", count);
|
||||||
const auto str_2 = strip_name.isEmpty() ? QObject::tr(" d'un groupe de bornes") :
|
else if (strip_name.isEmpty())
|
||||||
QObject::tr(" du groupe de bornes %1").arg(strip_name);
|
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())
|
||||||
const auto str_3 = new_strip_name.isEmpty() ? QObject::tr("vers un groupe de bornes") :
|
text = QObject::tr("Déplacer %n borne(s) du groupe de bornes %1 vers un groupe de bornes", "", count).arg(strip_name);
|
||||||
QObject::tr("vers le groupe de bornes %1").arg(new_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(str_1 % " " % str_2 % " " % str_3);
|
setText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MoveTerminalCommand::undo()
|
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("%1").arg(value.toInt(), 3, 10, QChar('0'));
|
||||||
return QString::number(value.toInt());
|
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 <QStringList>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QDomElement>
|
#include <QDomElement>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QSettings>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This class represents a numerotation context, i.e. the data (type, value, increase)
|
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.
|
/// UI preview of a part's value matches what actually gets rendered.
|
||||||
static QString formatValue(const QStringList &item);
|
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:
|
private:
|
||||||
QStringList content_;
|
QStringList content_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -505,7 +505,10 @@ void BorderTitleBlock::draw(QPainter *painter)
|
|||||||
{
|
{
|
||||||
//Set the QPainter
|
//Set the QPainter
|
||||||
painter -> save();
|
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 -> setPen(pen);
|
||||||
painter -> setBrush(Qt::NoBrush);
|
painter -> setBrush(Qt::NoBrush);
|
||||||
|
|
||||||
|
|||||||
@@ -499,6 +499,7 @@ void ConductorProperties::applyForEqualAttributes(QList<ConductorProperties> lis
|
|||||||
horiz_rotate_text = cp.horiz_rotate_text;
|
horiz_rotate_text = cp.horiz_rotate_text;
|
||||||
m_vertical_alignment = cp.m_vertical_alignment;
|
m_vertical_alignment = cp.m_vertical_alignment;
|
||||||
m_horizontal_alignment = cp.m_horizontal_alignment;
|
m_horizontal_alignment = cp.m_horizontal_alignment;
|
||||||
|
style = cp.style;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -555,6 +556,18 @@ void ConductorProperties::applyForEqualAttributes(QList<ConductorProperties> lis
|
|||||||
m_dash_size = i_value;
|
m_dash_size = i_value;
|
||||||
equal = true;
|
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
|
//text
|
||||||
s_value = clist.first().text;
|
s_value = clist.first().text;
|
||||||
for(ConductorProperties cp : clist)
|
for(ConductorProperties cp : clist)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
#include "diagramposition.h"
|
#include "diagramposition.h"
|
||||||
#include "factory/elementfactory.h"
|
#include "factory/elementfactory.h"
|
||||||
#include "qetapp.h"
|
#include "qetapp.h"
|
||||||
|
#include "qetpalette.h"
|
||||||
#include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
|
#include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
|
||||||
#include "qetgraphicsitem/conductor.h"
|
#include "qetgraphicsitem/conductor.h"
|
||||||
#include "qetgraphicsitem/conductortextitem.h"
|
#include "qetgraphicsitem/conductortextitem.h"
|
||||||
@@ -283,10 +284,11 @@ void Diagram::drawBackground(QPainter *p, const QRectF &r) {
|
|||||||
* if background color is black,
|
* if background color is black,
|
||||||
* then grid spots shall be white,
|
* then grid spots shall be white,
|
||||||
* else they shall be black in color.
|
* 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;
|
QPen pen;
|
||||||
Diagram::background_color == Qt::black? pen.setColor(Qt::white)
|
pen.setColor(QET::Palette::gridDotColor(Diagram::background_color, m_inverted_lightness));
|
||||||
: pen.setColor(Qt::black);
|
|
||||||
pen.setCosmetic(true);
|
pen.setCosmetic(true);
|
||||||
p->setPen(pen);
|
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
|
//Get the top left corner of the rectangle that contain all added items
|
||||||
QRectF items_rect;
|
QRectF items_rect;
|
||||||
for (auto item : added_items) {
|
for (auto item : added_items) {
|
||||||
items_rect = items_rect.united(
|
items_rect = items_rect.united(item->mapToScene(item->boundingRect()).boundingRect());
|
||||||
item->mapToScene(
|
|
||||||
item->boundingRect()
|
|
||||||
).boundingRect());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QPointF point_ = items_rect.topLeft();
|
QPointF point_ = items_rect.topLeft();
|
||||||
@@ -1718,8 +1717,12 @@ bool Diagram::fromXml(QDomElement &document,
|
|||||||
position.y() - point_.y()));
|
position.y() - point_.y()));
|
||||||
|
|
||||||
//Translate all added items
|
//Translate all added items
|
||||||
for (auto qgi : added_items)
|
for (auto qgi : added_items) {
|
||||||
qgi->setPos(qgi->pos() += pos_);
|
qgi->setPos(qgi->pos() += pos_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load conductor
|
// Load conductor
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ class Diagram : public QGraphicsScene
|
|||||||
qreal diagram_qet_version_;
|
qreal diagram_qet_version_;
|
||||||
|
|
||||||
bool draw_grid_;
|
bool draw_grid_;
|
||||||
|
bool m_inverted_lightness = false;
|
||||||
bool use_border_;
|
bool use_border_;
|
||||||
bool draw_guides_;
|
bool draw_guides_;
|
||||||
QList<Diagram::Guide> m_guides_list;
|
QList<Diagram::Guide> m_guides_list;
|
||||||
@@ -222,6 +223,7 @@ class Diagram : public QGraphicsScene
|
|||||||
ExportProperties applyProperties(const ExportProperties &);
|
ExportProperties applyProperties(const ExportProperties &);
|
||||||
void setDisplayGrid(bool);
|
void setDisplayGrid(bool);
|
||||||
bool displayGrid();
|
bool displayGrid();
|
||||||
|
void setInvertedLightness(bool);
|
||||||
void setDisplayGuides(bool);
|
void setDisplayGuides(bool);
|
||||||
bool displayGuides();
|
bool displayGuides();
|
||||||
void updateProjectGuides(const QList<GuideProperties> &guides);
|
void updateProjectGuides(const QList<GuideProperties> &guides);
|
||||||
@@ -355,6 +357,18 @@ inline void Diagram::setDisplayGrid(bool dg) {
|
|||||||
draw_grid_ = 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
|
@brief Diagram::displayGrid
|
||||||
@return draw_grid_ true if the grid is drawn, false otherwise.
|
@return draw_grid_ true if the grid is drawn, false otherwise.
|
||||||
|
|||||||
@@ -67,7 +67,19 @@ m_preview_item(nullptr)
|
|||||||
dummy_diagram->setDisplayGrid(false);
|
dummy_diagram->setDisplayGrid(false);
|
||||||
dummy_diagram->fromXml(diagram_node, QPointF(0, 0), false, nullptr);
|
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();
|
QRectF scene_rect = dummy_diagram->itemsBoundingRect();
|
||||||
|
|
||||||
if (!scene_rect.isEmpty()) {
|
if (!scene_rect.isEmpty()) {
|
||||||
QPixmap pixmap(scene_rect.toAlignedRect().size());
|
QPixmap pixmap(scene_rect.toAlignedRect().size());
|
||||||
pixmap.fill(Qt::transparent);
|
pixmap.fill(Qt::transparent);
|
||||||
@@ -80,10 +92,11 @@ m_preview_item(nullptr)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_preview_item) {
|
if (m_preview_item) {
|
||||||
m_preview_item->setPos(Diagram::snapToGrid(pos));
|
QPointF snapped = Diagram::snapToGrid(pos);
|
||||||
m_preview_item->setOpacity(0.6);
|
m_preview_item->setPos(snapped);
|
||||||
m_diagram->addItem(m_preview_item);
|
m_preview_item->setOpacity(0.6);
|
||||||
|
m_diagram->addItem(m_preview_item);
|
||||||
m_running = true;
|
m_running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +130,7 @@ void DiagramEventAddMacro::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
|||||||
{
|
{
|
||||||
if (m_preview_item) {
|
if (m_preview_item) {
|
||||||
const auto pos_{Diagram::snapToGrid(event->scenePos())};
|
const auto pos_{Diagram::snapToGrid(event->scenePos())};
|
||||||
|
|
||||||
m_preview_item->setPos(pos_);
|
m_preview_item->setPos(pos_);
|
||||||
|
|
||||||
if (m_status_bar) {
|
if (m_status_bar) {
|
||||||
@@ -141,7 +155,8 @@ void DiagramEventAddMacro::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
|||||||
emit finish();
|
emit finish();
|
||||||
}
|
}
|
||||||
else if (event->button() == Qt::LeftButton) {
|
else if (event->button() == Qt::LeftButton) {
|
||||||
addMacro(Diagram::snapToGrid(event->scenePos()));
|
QPointF snapped = Diagram::snapToGrid(event->scenePos());
|
||||||
|
addMacro(snapped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
event->setAccepted(true);
|
event->setAccepted(true);
|
||||||
@@ -238,10 +253,9 @@ void DiagramEventAddMacro::addMacro(QPointF final_pos)
|
|||||||
|
|
||||||
if (!diagram_node.isNull()) {
|
if (!diagram_node.isNull()) {
|
||||||
QDomElement cloned_node = diagram_node.cloneNode(true).toElement();
|
QDomElement cloned_node = diagram_node.cloneNode(true).toElement();
|
||||||
|
|
||||||
DiagramContent pasted_content;
|
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();
|
m_diagram->refreshContents();
|
||||||
|
|
||||||
// Prevent PasteDiagramCommand from erasing labels (BMK)
|
// Prevent PasteDiagramCommand from erasing labels (BMK)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ private:
|
|||||||
QDomDocument m_macro_doc;
|
QDomDocument m_macro_doc;
|
||||||
QGraphicsPixmapItem *m_preview_item;
|
QGraphicsPixmapItem *m_preview_item;
|
||||||
QPointer<QStatusBar> m_status_bar;
|
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
|
#endif // DIAGRAMEVENTADDMACRO_H
|
||||||
|
|||||||
@@ -29,7 +29,14 @@
|
|||||||
*/
|
*/
|
||||||
DiagramEventAddText::DiagramEventAddText(Diagram *diagram) :
|
DiagramEventAddText::DiagramEventAddText(Diagram *diagram) :
|
||||||
DiagramEventInterface(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
|
@brief DiagramEventAddText::~DiagramEventAddText
|
||||||
@@ -52,6 +59,8 @@ void DiagramEventAddText::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
|||||||
event->scenePos()));
|
event->scenePos()));
|
||||||
text->setTextInteractionFlags(Qt::TextEditorInteraction);
|
text->setTextInteractionFlags(Qt::TextEditorInteraction);
|
||||||
text->setFocus(Qt::MouseFocusReason);
|
text->setFocus(Qt::MouseFocusReason);
|
||||||
|
//Placed: the tool is done before it announces it.
|
||||||
|
m_running = false;
|
||||||
emit finish();
|
emit finish();
|
||||||
event->setAccepted(true);
|
event->setAccepted(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
#include "ElementsCollection/xmlelementcollection.h"
|
#include "ElementsCollection/xmlelementcollection.h"
|
||||||
#include "NameList/nameslist.h"
|
#include "NameList/nameslist.h"
|
||||||
#include "elementdialog.h"
|
#include "elementdialog.h"
|
||||||
|
#include <QApplication>
|
||||||
#include <QDropEvent>
|
#include <QDropEvent>
|
||||||
|
#include <QPainter>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,7 +50,7 @@
|
|||||||
@param parent Le QWidget parent de cette vue de schema
|
@param parent Le QWidget parent de cette vue de schema
|
||||||
*/
|
*/
|
||||||
DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
|
DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
|
||||||
QGraphicsView (parent),
|
PaletteGraphicsView (parent),
|
||||||
m_diagram (diagram)
|
m_diagram (diagram)
|
||||||
{
|
{
|
||||||
grabGesture(Qt::PinchGesture);
|
grabGesture(Qt::PinchGesture);
|
||||||
@@ -1081,6 +1083,16 @@ bool DiagramView::event(QEvent *e) {
|
|||||||
return(QGraphicsView::event(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
|
@brief DiagramView::paintEvent
|
||||||
Reimplemented from QGraphicsView
|
Reimplemented from QGraphicsView
|
||||||
@@ -1088,7 +1100,7 @@ bool DiagramView::event(QEvent *e) {
|
|||||||
*/
|
*/
|
||||||
void DiagramView::paintEvent(QPaintEvent *event)
|
void DiagramView::paintEvent(QPaintEvent *event)
|
||||||
{
|
{
|
||||||
QGraphicsView::paintEvent(event);
|
PaletteGraphicsView::paintEvent(event);
|
||||||
|
|
||||||
if (m_free_rubberbanding && m_free_rubberband.count() >= 3)
|
if (m_free_rubberbanding && m_free_rubberband.count() >= 3)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
#include "titleblock/templatelocation.h"
|
#include "titleblock/templatelocation.h"
|
||||||
|
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
#include <QGraphicsView>
|
#include "palettegraphicsview.h"
|
||||||
|
|
||||||
class Conductor;
|
class Conductor;
|
||||||
class Diagram;
|
class Diagram;
|
||||||
@@ -35,7 +35,7 @@ class QGestureEvent;
|
|||||||
This class provides a widget to render an electric diagram in an editable,
|
This class provides a widget to render an electric diagram in an editable,
|
||||||
interactive way.
|
interactive way.
|
||||||
*/
|
*/
|
||||||
class DiagramView : public QGraphicsView
|
class DiagramView : public PaletteGraphicsView
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
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.
|
///Set for one call only, by the Escape handler, to let focus leave the view.
|
||||||
bool m_releasing_focus = false;
|
bool m_releasing_focus = false;
|
||||||
void paintEvent(QPaintEvent *event) override;
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
void paintingInverted(bool inverted) override;
|
||||||
void mousePressEvent(QMouseEvent *) override;
|
void mousePressEvent(QMouseEvent *) override;
|
||||||
void mouseMoveEvent(QMouseEvent *) override;
|
void mouseMoveEvent(QMouseEvent *) override;
|
||||||
void mouseReleaseEvent(QMouseEvent *) override;
|
void mouseReleaseEvent(QMouseEvent *) override;
|
||||||
|
|||||||
@@ -52,6 +52,18 @@ ElementsCollectionCache::ElementsCollectionCache(const QString &database_path, Q
|
|||||||
QSqlQuery(cache_db_).exec("PRAGMA locking_mode = EXCLUSIVE");
|
QSqlQuery(cache_db_).exec("PRAGMA locking_mode = EXCLUSIVE");
|
||||||
QSqlQuery(cache_db_).exec("PRAGMA synchronous = OFF");
|
QSqlQuery(cache_db_).exec("PRAGMA synchronous = OFF");
|
||||||
|
|
||||||
|
// Previews used to be stored on an opaque white sheet; they are
|
||||||
|
// transparent now, so the collection tree can adapt them to a
|
||||||
|
// dark palette. A cache written before that is dropped once.
|
||||||
|
QSqlQuery(cache_db_).exec("CREATE TABLE IF NOT EXISTS meta"
|
||||||
|
"(key VARCHAR(32) NOT NULL PRIMARY KEY, value VARCHAR(64));");
|
||||||
|
QSqlQuery meta(cache_db_);
|
||||||
|
meta.exec("SELECT value FROM meta WHERE key = 'pixmaps'");
|
||||||
|
if (!meta.next() || meta.value(0).toString() != QLatin1String("transparent")) {
|
||||||
|
QSqlQuery(cache_db_).exec("DROP TABLE IF EXISTS pixmaps");
|
||||||
|
QSqlQuery(cache_db_).exec("DROP TABLE IF EXISTS names");
|
||||||
|
QSqlQuery(cache_db_).exec("REPLACE INTO meta (key, value) VALUES ('pixmaps', 'transparent')");
|
||||||
|
}
|
||||||
#if TODO_LIST
|
#if TODO_LIST
|
||||||
#pragma message("@TODO the tables could already exist, handle that case.")
|
#pragma message("@TODO the tables could already exist, handle that case.")
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -54,24 +54,8 @@ ElementsPanel::ElementsPanel(QWidget *parent) :
|
|||||||
setDropIndicatorShown(true);
|
setDropIndicatorShown(true);
|
||||||
setAutoExpandDelay(1000);
|
setAutoExpandDelay(1000);
|
||||||
|
|
||||||
// force du noir sur une alternance de blanc (comme le schema) et de gris
|
// Rows follow the application palette; the icons shown here come from
|
||||||
// clair, avec du blanc sur bleu pas trop fonce pour la selection
|
// the icon theme, which has a dark variant.
|
||||||
//
|
|
||||||
// Element icons are rendered with colors read directly from each .elmt
|
|
||||||
// file (almost always black linework, matching printed-schematic
|
|
||||||
// convention) onto a transparent background -- so this view must keep
|
|
||||||
// a light background regardless of the OS/desktop theme, or the icons
|
|
||||||
// become invisible on dark themes. QAbstractItemView paints its rows
|
|
||||||
// using the viewport's palette, not the view's own, so the palette
|
|
||||||
// must be applied to both to actually take effect under every style.
|
|
||||||
QPalette qp = palette();
|
|
||||||
qp.setColor(QPalette::Text, Qt::black);
|
|
||||||
qp.setColor(QPalette::Base, Qt::white);
|
|
||||||
qp.setColor(QPalette::AlternateBase, QColor("#e8e8e8"));
|
|
||||||
qp.setColor(QPalette::Highlight, QColor("#678db2"));
|
|
||||||
qp.setColor(QPalette::HighlightedText, Qt::black);
|
|
||||||
setPalette(qp);
|
|
||||||
viewport()->setPalette(qp);
|
|
||||||
|
|
||||||
// we handle double click on items ourselves
|
// we handle double click on items ourselves
|
||||||
connect(this, &ElementsPanel::itemDoubleClicked, this, &ElementsPanel::slot_doubleClick);
|
connect(this, &ElementsPanel::itemDoubleClicked, this, &ElementsPanel::slot_doubleClick);
|
||||||
|
|||||||
@@ -137,25 +137,14 @@ void ElementTextsMover::endMovement()
|
|||||||
|
|
||||||
QString ElementTextsMover::undoText() const
|
QString ElementTextsMover::undoText() const
|
||||||
{
|
{
|
||||||
QString 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(m_text_count == 1)
|
if (parts.isEmpty())
|
||||||
undo_text.append(QObject::tr("Déplacer un texte d'élément"));
|
return QString(); // should never occur
|
||||||
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)
|
return QObject::tr("Déplacer %1").arg(QLocale().createSeparatedList(parts));
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,12 +146,13 @@ QPixmap ElementPictureFactory::pixmap(const ElementsLocation &location)
|
|||||||
QPixmap pix(w, h);
|
QPixmap pix(w, h);
|
||||||
//Element definitions almost always draw with a hardcoded black
|
//Element definitions almost always draw with a hardcoded black
|
||||||
//stroke color, on the assumption of the white diagram sheet they
|
//stroke color, on the assumption of the white diagram sheet they
|
||||||
//are normally placed on. A transparent background here makes
|
//are normally placed on. The pixmap is kept as drawn, on a
|
||||||
//that stroke disappear against a dark widget/tree-view background
|
//transparent background: the places that show it (the
|
||||||
//(bugtracker #335). Give it an opaque white background instead -
|
//collection tree through ElementPreviewDelegate, the drag icon)
|
||||||
//exactly what the element already assumes visually, in every
|
//adapt it to the palette with QET::Palette::forPalette(), so a
|
||||||
//context this pixmap is used (tree icons, drag icon, previews).
|
//dark palette gets light ink instead of black on black
|
||||||
pix.fill(Qt::white);
|
//(bugtracker #335).
|
||||||
|
pix.fill(Qt::transparent);
|
||||||
|
|
||||||
QPainter painter(&pix);
|
QPainter painter(&pix);
|
||||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -271,84 +271,88 @@ QString QET::ElementsAndConductorsSentence(
|
|||||||
int tables_count,
|
int tables_count,
|
||||||
int terminal_strip_count)
|
int terminal_strip_count)
|
||||||
{
|
{
|
||||||
QString text;
|
QStringList parts;
|
||||||
if (elements_count) {
|
if (elements_count) {
|
||||||
text += QObject::tr(
|
parts.append(
|
||||||
"%n élément(s)",
|
QObject::tr(
|
||||||
"part of a sentence listing the content of a diagram",
|
"%n élément(s)",
|
||||||
elements_count
|
"part of a enumerative partial sentence listing the content of a diagram",
|
||||||
|
elements_count
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conductors_count) {
|
if (conductors_count) {
|
||||||
if (!text.isEmpty()) text += ", ";
|
parts.append(
|
||||||
text += QObject::tr(
|
QObject::tr(
|
||||||
"%n conducteur(s)",
|
"%n conducteur(s)",
|
||||||
"part of a sentence listing the content of a diagram",
|
"part of a enumerative partial sentence listing the content of a diagram",
|
||||||
conductors_count
|
conductors_count
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (texts_count) {
|
if (texts_count) {
|
||||||
if (!text.isEmpty()) text += ", ";
|
parts.append(
|
||||||
text += QObject::tr(
|
QObject::tr(
|
||||||
"%n champ(s) de texte",
|
"%n champ(s) de texte",
|
||||||
"part of a sentence listing the content of a diagram",
|
"part of a enumerative partial sentence listing the content of a diagram",
|
||||||
texts_count
|
texts_count
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (images_count) {
|
if (images_count) {
|
||||||
if (!text.isEmpty()) text += ", ";
|
parts.append(
|
||||||
// Qt's %n only selects a grammatical singular/plural form (the
|
QObject::tr(
|
||||||
// "(s)" convention used by every other count here) -- it never
|
"%n image(s)",
|
||||||
// spells the number out as a word, so getting "une image"
|
"part of a enumerative partial sentence listing the content of a diagram",
|
||||||
// instead of the literal "1 image" for the single-item case
|
images_count
|
||||||
// means handling that count outside %n entirely, with its own
|
)
|
||||||
// fixed string.
|
);
|
||||||
text += images_count == 1
|
|
||||||
? QObject::tr("une image", "part of a sentence listing the content of a diagram")
|
|
||||||
: QObject::tr(
|
|
||||||
"%n images",
|
|
||||||
"part of a sentence listing the content of a diagram",
|
|
||||||
images_count
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shapes_count) {
|
if (shapes_count) {
|
||||||
if (!text.isEmpty()) text += ", ";
|
parts.append(
|
||||||
text += QObject::tr(
|
QObject::tr(
|
||||||
"%n forme(s)",
|
"%n forme(s)",
|
||||||
"part of a sentence listing the content of a diagram",
|
"part of a enumerative partial sentence listing the content of a diagram",
|
||||||
shapes_count
|
shapes_count
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element_text_count) {
|
if (element_text_count) {
|
||||||
if (!text.isEmpty()) text += ", ";
|
parts.append(
|
||||||
text += QObject::tr(
|
QObject::tr(
|
||||||
"%n texte(s) d'élément",
|
"%n texte(s) d'élément",
|
||||||
"part of a sentence listing the content of a diagram",
|
"part of a enumerative partial sentence listing the content of a diagram",
|
||||||
element_text_count);
|
element_text_count
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tables_count) {
|
if (tables_count) {
|
||||||
if (!text.isEmpty()) text += ", ";
|
parts.append(
|
||||||
text += QObject::tr(
|
QObject::tr(
|
||||||
"%n tableau(s)",
|
"%n tableau(s)",
|
||||||
"part of a sentence listing the content of diagram",
|
"part of a enumerative partial sentence listing the content of diagram",
|
||||||
tables_count);
|
tables_count
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (terminal_strip_count) {
|
if (terminal_strip_count) {
|
||||||
if (!text.isEmpty()) text += ", ";
|
parts.append(
|
||||||
text += QObject::tr(
|
QObject::tr(
|
||||||
"%n plan de bornes",
|
"%n plan(s) de bornes",
|
||||||
"part of a sentence listing the content of a diagram",
|
"part of a enumerative partial sentence listing the content of a diagram",
|
||||||
terminal_strip_count);
|
terminal_strip_count
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return(text);
|
return QLocale().createSeparatedList(parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#include "qetdiagrameditor.h"
|
#include "qetdiagrameditor.h"
|
||||||
#include "qeticons.h"
|
#include "qeticons.h"
|
||||||
#include "qetpalette.h"
|
#include "qetpalette.h"
|
||||||
|
#include "qetstyle.h"
|
||||||
#include "utils/qetutils.h"
|
#include "utils/qetutils.h"
|
||||||
#include "qetmessagebox.h"
|
#include "qetmessagebox.h"
|
||||||
#include "qetproject.h"
|
#include "qetproject.h"
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
#include <QFontDatabase>
|
#include <QFontDatabase>
|
||||||
#include <QProcessEnvironment>
|
#include <QProcessEnvironment>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
|
#include <QStyleFactory>
|
||||||
#include <QStyleHints>
|
#include <QStyleHints>
|
||||||
#ifdef BUILD_WITHOUT_KF
|
#ifdef BUILD_WITHOUT_KF
|
||||||
# include "ui/nokde/kautosavefile.h"
|
# include "ui/nokde/kautosavefile.h"
|
||||||
@@ -234,6 +236,8 @@ QString QETApp::loadedQtTranslationFile()
|
|||||||
void QETApp::setLanguage(const QString &desired_language) {
|
void QETApp::setLanguage(const QString &desired_language) {
|
||||||
QString languages_path = languagesPath();
|
QString languages_path = languagesPath();
|
||||||
|
|
||||||
|
QLocale::setDefault(QLocale(desired_language));
|
||||||
|
|
||||||
// load Qt library translations
|
// load Qt library translations
|
||||||
QString qt_l10n_path = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
|
QString qt_l10n_path = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
|
||||||
if (!qtTranslator.load("qt_" + desired_language, qt_l10n_path))
|
if (!qtTranslator.load("qt_" + desired_language, qt_l10n_path))
|
||||||
@@ -1806,6 +1810,10 @@ void QETApp::useSystemPalette(bool use) {
|
|||||||
file.close();
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2368,6 +2376,12 @@ void QETApp::applyIconTheme(const QPalette &palette)
|
|||||||
*/
|
*/
|
||||||
void QETApp::initStyle()
|
void QETApp::initStyle()
|
||||||
{
|
{
|
||||||
|
// Wrap the running style so icons get a hover state (see qetstyle.h).
|
||||||
|
// The proxy keeps the base style's object name, so the Fusion checks
|
||||||
|
// below still see "fusion".
|
||||||
|
if (!qobject_cast<QETStyle *>(qApp->style()))
|
||||||
|
qApp->setStyle(new QETStyle(QStyleFactory::create(qApp->style()->objectName())));
|
||||||
|
|
||||||
initial_palette_ = qApp->palette();
|
initial_palette_ = qApp->palette();
|
||||||
|
|
||||||
#ifdef Q_OS_MACOS
|
#ifdef Q_OS_MACOS
|
||||||
|
|||||||
@@ -50,6 +50,8 @@
|
|||||||
#include "recentfiles.h"
|
#include "recentfiles.h"
|
||||||
#include "shortcutmanager.h"
|
#include "shortcutmanager.h"
|
||||||
#include "ui/bomexportdialog.h"
|
#include "ui/bomexportdialog.h"
|
||||||
|
#include "ui/conductorcolortoolbutton.h"
|
||||||
|
#include "ui/diagrambgcolorbutton.h"
|
||||||
#include "ui/jumptoelementdialog.h"
|
#include "ui/jumptoelementdialog.h"
|
||||||
#include "ui/diagrampropertieseditordockwidget.h"
|
#include "ui/diagrampropertieseditordockwidget.h"
|
||||||
#include "ui/backupdialog.h"
|
#include "ui/backupdialog.h"
|
||||||
@@ -389,6 +391,13 @@ void QETDiagramEditor::setUpActions()
|
|||||||
if (ProjectView *pv = currentProjectView())
|
if (ProjectView *pv = currentProjectView())
|
||||||
pv->project()->setAutoConductor(ac);
|
pv->project()->setAutoConductor(ac);
|
||||||
});
|
});
|
||||||
|
//Registered with no default sequence on purpose. This is a
|
||||||
|
//setting some people toggle constantly and others never touch,
|
||||||
|
//so it earns a place in the Shortcuts page rather than a key of
|
||||||
|
//its own taken from the ones still free. Asked for on the forum
|
||||||
|
//(viewtopic.php?pid=23296): "est il possible dans les raccourcis
|
||||||
|
//d'ajouter un pour création automatique de conducteur ?"
|
||||||
|
ShortcutManager::instance().registerAction(m_auto_conductor, "diagrameditor.auto_conductor", tr("Éditeur de schémas"), QKeySequence());
|
||||||
|
|
||||||
//AutoBreakConductor
|
//AutoBreakConductor
|
||||||
m_auto_break_conductor = new QAction (QET::Icons::Conductor, tr("Coupure automatique de conducteur(s)","Tool tip of auto break conductor"), this);
|
m_auto_break_conductor = new QAction (QET::Icons::Conductor, tr("Coupure automatique de conducteur(s)","Tool tip of auto break conductor"), this);
|
||||||
@@ -405,15 +414,8 @@ void QETDiagramEditor::setUpActions()
|
|||||||
pv->project()->setAutoBreakConductor(abc);
|
pv->project()->setAutoBreakConductor(abc);
|
||||||
});
|
});
|
||||||
|
|
||||||
//Switch background color
|
//Diagram background color picker
|
||||||
m_grey_background = new QAction (QET::Icons::DiagramBg, tr("Couleur de fond blanc/gris","Tool tip of white/grey background button"), this);
|
m_background_color_button = new DiagramBgColorToolButton(this, 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();
|
|
||||||
});
|
|
||||||
|
|
||||||
//Draw or not the background grid
|
//Draw or not the background grid
|
||||||
m_draw_grid = new QAction ( QET::Icons::Grid, tr("Afficher la grille"), this);
|
m_draw_grid = new QAction ( QET::Icons::Grid, tr("Afficher la grille"), this);
|
||||||
@@ -904,7 +906,7 @@ void QETDiagramEditor::setUpToolBar()
|
|||||||
view_tool_bar -> addSeparator();
|
view_tool_bar -> addSeparator();
|
||||||
view_tool_bar -> addAction(m_draw_grid);
|
view_tool_bar -> addAction(m_draw_grid);
|
||||||
view_tool_bar -> addAction(m_draw_guides);
|
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 -> addSeparator();
|
||||||
view_tool_bar -> addActions(m_zoom_action_toolBar);
|
view_tool_bar -> addActions(m_zoom_action_toolBar);
|
||||||
|
|
||||||
@@ -912,6 +914,10 @@ void QETDiagramEditor::setUpToolBar()
|
|||||||
diagram_tool_bar -> addAction (m_conductor_reset);
|
diagram_tool_bar -> addAction (m_conductor_reset);
|
||||||
diagram_tool_bar -> addAction (m_auto_conductor);
|
diagram_tool_bar -> addAction (m_auto_conductor);
|
||||||
diagram_tool_bar -> addAction (m_auto_break_conductor);
|
diagram_tool_bar -> addAction (m_auto_break_conductor);
|
||||||
|
//Sits with the conductor actions it works alongside: it colours
|
||||||
|
//the selected conductors and sets the colour of the next one drawn.
|
||||||
|
m_conductor_color_button = new ConductorColorToolButton(this, this);
|
||||||
|
diagram_tool_bar -> addWidget (m_conductor_color_button);
|
||||||
|
|
||||||
m_add_item_tool_bar = new QToolBar(tr("Ajouter"), this);
|
m_add_item_tool_bar = new QToolBar(tr("Ajouter"), this);
|
||||||
m_add_item_tool_bar->setObjectName("adding");
|
m_add_item_tool_bar->setObjectName("adding");
|
||||||
@@ -1051,7 +1057,7 @@ void QETDiagramEditor::setUpMenu()
|
|||||||
menu_affichage -> addSeparator();
|
menu_affichage -> addSeparator();
|
||||||
menu_affichage -> addAction(m_draw_grid);
|
menu_affichage -> addAction(m_draw_grid);
|
||||||
menu_affichage -> addAction(m_draw_guides);
|
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 -> addSeparator();
|
||||||
menu_affichage -> addActions(m_zoom_actions_group.actions());
|
menu_affichage -> addActions(m_zoom_actions_group.actions());
|
||||||
|
|
||||||
@@ -1868,7 +1874,7 @@ void QETDiagramEditor::slot_updateActions()
|
|||||||
m_select_actions_group. setEnabled(opened_diagram);
|
m_select_actions_group. setEnabled(opened_diagram);
|
||||||
m_add_item_actions_group. setEnabled(editable_project);
|
m_add_item_actions_group. setEnabled(editable_project);
|
||||||
m_row_column_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_grid-> setEnabled(opened_diagram);
|
||||||
m_draw_guides-> setEnabled(opened_diagram);
|
m_draw_guides-> setEnabled(opened_diagram);
|
||||||
|
|
||||||
@@ -2110,6 +2116,10 @@ void QETDiagramEditor::slot_updateModeActions()
|
|||||||
m_auto_conductor -> setDisabled(true);
|
m_auto_conductor -> setDisabled(true);
|
||||||
m_auto_break_conductor -> setDisabled(true);
|
m_auto_break_conductor -> setDisabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_conductor_color_button) {
|
||||||
|
m_conductor_color_button->updateEnabledState();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ class QMdiSubWindow;
|
|||||||
class QETProject;
|
class QETProject;
|
||||||
class QETResult;
|
class QETResult;
|
||||||
class ProjectView;
|
class ProjectView;
|
||||||
|
class ConductorColorToolButton;
|
||||||
class CustomElement;
|
class CustomElement;
|
||||||
|
class DiagramBgColorToolButton;
|
||||||
class Diagram;
|
class Diagram;
|
||||||
class DiagramView;
|
class DiagramView;
|
||||||
class Element;
|
class Element;
|
||||||
@@ -199,7 +201,6 @@ class QETDiagramEditor : public QETMainWindow
|
|||||||
*m_paste, ///< Paste clipboard content on the current diagram
|
*m_paste, ///< Paste clipboard content on the current diagram
|
||||||
*m_auto_conductor, ///< Enable/Disable the use of auto conductor
|
*m_auto_conductor, ///< Enable/Disable the use of auto conductor
|
||||||
*m_auto_break_conductor, ///< Enable/Disable the use of auto break 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_grid, ///< Switch the background grid display or not
|
||||||
*m_draw_guides = nullptr, ///< Switch the custom guides 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.
|
*m_project_edit_properties, ///< Edit the properties of the current project.
|
||||||
@@ -237,6 +238,11 @@ class QETDiagramEditor : public QETMainWindow
|
|||||||
*m_find = nullptr,
|
*m_find = nullptr,
|
||||||
*m_jump_to_element = nullptr; ///< Open the "jump to element" quick-open popup
|
*m_jump_to_element = nullptr; ///< Open the "jump to element" quick-open popup
|
||||||
|
|
||||||
|
///< 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
|
QList <QAction *> m_zoom_action_toolBar; ///Only zoom action must displayed in the toolbar
|
||||||
|
|
||||||
void removeDiagramSilent(Diagram *diagram);
|
void removeDiagramSilent(Diagram *diagram);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include "../qetgraphicsitem/terminal.h"
|
#include "../qetgraphicsitem/terminal.h"
|
||||||
#include "../qetinformation.h"
|
#include "../qetinformation.h"
|
||||||
#include "../utils/qetutils.h"
|
#include "../utils/qetutils.h"
|
||||||
|
#include "../QetGraphicsItemModeler/qetgraphicshandleritem.h"
|
||||||
#include "crossrefitem.h"
|
#include "crossrefitem.h"
|
||||||
#include "element.h"
|
#include "element.h"
|
||||||
#include "elementtextitemgroup.h"
|
#include "elementtextitemgroup.h"
|
||||||
@@ -67,7 +68,9 @@ DynamicElementTextItem::DynamicElementTextItem(Element *parent_element) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
DynamicElementTextItem::~DynamicElementTextItem()
|
DynamicElementTextItem::~DynamicElementTextItem()
|
||||||
{}
|
{
|
||||||
|
removeResizeHandles();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief DynamicElementTextItem::textFromMetaEnum
|
@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 diffx = qRound(current_parent_pos.x() - button_down_parent_pos.x());
|
||||||
int diffy = qRound(current_parent_pos.y() - button_down_parent_pos.y());
|
int diffy = qRound(current_parent_pos.y() - button_down_parent_pos.y());
|
||||||
QPointF new_pos = m_initial_position + QPointF(diffx, diffy);
|
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())
|
if(diagram())
|
||||||
diagram()->elementTextsMover().continueMovement(event);
|
diagram()->elementTextsMover().continueMovement(event);
|
||||||
@@ -723,6 +732,9 @@ void DynamicElementTextItem::paint(QPainter *painter, const QStyleOptionGraphics
|
|||||||
{
|
{
|
||||||
DiagramTextItem::paint(painter, option, widget);
|
DiagramTextItem::paint(painter, option, widget);
|
||||||
|
|
||||||
|
if (m_left_resize_handle || m_right_resize_handle)
|
||||||
|
updateResizeHandlesPos();
|
||||||
|
|
||||||
if (m_frame)
|
if (m_frame)
|
||||||
{
|
{
|
||||||
painter->save();
|
painter->save();
|
||||||
@@ -812,12 +824,41 @@ QVariant DynamicElementTextItem::itemChange(QGraphicsItem::GraphicsItemChange ch
|
|||||||
updateXref();
|
updateXref();
|
||||||
updateXref();
|
updateXref();
|
||||||
}
|
}
|
||||||
|
else if (change == QGraphicsItem::ItemSelectedHasChanged)
|
||||||
|
{
|
||||||
|
if (value.toBool())
|
||||||
|
addResizeHandles();
|
||||||
|
else
|
||||||
|
removeResizeHandles();
|
||||||
|
}
|
||||||
|
else if (change == QGraphicsItem::ItemSceneHasChanged && !scene())
|
||||||
|
{
|
||||||
|
removeResizeHandles();
|
||||||
|
}
|
||||||
|
|
||||||
return QGraphicsObject::itemChange(change, value);
|
return QGraphicsObject::itemChange(change, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DynamicElementTextItem::sceneEventFilter(QGraphicsItem *watched, QEvent *event)
|
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)
|
if(watched != m_slave_Xref_item)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -837,6 +878,125 @@ bool DynamicElementTextItem::sceneEventFilter(QGraphicsItem *watched, QEvent *ev
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@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 frameRect()'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.
|
||||||
|
*/
|
||||||
|
void DynamicElementTextItem::updateResizeHandlesPos()
|
||||||
|
{
|
||||||
|
if (!m_left_resize_handle || !m_right_resize_handle)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QRectF fr = frameRect();
|
||||||
|
m_left_resize_handle->setPos(mapToScene(QPointF(fr.left(), fr.center().y())));
|
||||||
|
m_right_resize_handle->setPos(mapToScene(QPointF(fr.right(), fr.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()
|
void DynamicElementTextItem::elementInfoChanged()
|
||||||
{
|
{
|
||||||
DiagramContext dc;
|
DiagramContext dc;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class Element;
|
|||||||
class Conductor;
|
class Conductor;
|
||||||
class ElementTextItemGroup;
|
class ElementTextItemGroup;
|
||||||
class CrossRefItem;
|
class CrossRefItem;
|
||||||
|
class QetGraphicsHandlerItem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief The DynamicElementTextItem class
|
@brief The DynamicElementTextItem class
|
||||||
@@ -151,6 +152,12 @@ class DynamicElementTextItem : public DiagramTextItem
|
|||||||
void zoomToLinkedElement();
|
void zoomToLinkedElement();
|
||||||
void parentElementRotationChanged();
|
void parentElementRotationChanged();
|
||||||
void thisRotationChanged();
|
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:
|
private:
|
||||||
QPointer <Element>
|
QPointer <Element>
|
||||||
@@ -182,6 +189,11 @@ class DynamicElementTextItem : public DiagramTextItem
|
|||||||
bool m_rotation_point_center = false;
|
bool m_rotation_point_center = false;
|
||||||
qreal m_visual_rotation_ref = 0;
|
qreal m_visual_rotation_ref = 0;
|
||||||
bool m_move_parent = true;
|
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
|
#endif // DYNAMICELEMENTTEXTITEM_H
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ namespace QET {
|
|||||||
QIcon ConductorEdit;
|
QIcon ConductorEdit;
|
||||||
QIcon ConductorSettings;
|
QIcon ConductorSettings;
|
||||||
QIcon Configure;
|
QIcon Configure;
|
||||||
|
QIcon ConfigureShortcuts;
|
||||||
QIcon ConfigureToolbars;
|
QIcon ConfigureToolbars;
|
||||||
QIcon IC_CopyFile;
|
QIcon IC_CopyFile;
|
||||||
QIcon DefaultConductor;
|
QIcon DefaultConductor;
|
||||||
@@ -408,6 +409,7 @@ void QET::Icons::initIcons()
|
|||||||
ConductorEdit = QIcon::fromTheme("conductor-edit");
|
ConductorEdit = QIcon::fromTheme("conductor-edit");
|
||||||
ConductorSettings = QIcon::fromTheme("conductor-reset");
|
ConductorSettings = QIcon::fromTheme("conductor-reset");
|
||||||
Configure = QIcon::fromTheme("configure");
|
Configure = QIcon::fromTheme("configure");
|
||||||
|
ConfigureShortcuts = QIcon::fromTheme("configure-shortcuts");
|
||||||
ConfigureToolbars = QIcon::fromTheme("configure-toolbars");
|
ConfigureToolbars = QIcon::fromTheme("configure-toolbars");
|
||||||
IC_CopyFile = QIcon::fromTheme("item-copy");
|
IC_CopyFile = QIcon::fromTheme("item-copy");
|
||||||
DiagramAdd = QIcon::fromTheme("folio-new");
|
DiagramAdd = QIcon::fromTheme("folio-new");
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ namespace QET {
|
|||||||
extern QIcon ConductorEdit;
|
extern QIcon ConductorEdit;
|
||||||
extern QIcon ConductorSettings;
|
extern QIcon ConductorSettings;
|
||||||
extern QIcon Configure;
|
extern QIcon Configure;
|
||||||
|
extern QIcon ConfigureShortcuts;
|
||||||
extern QIcon ConfigureToolbars;
|
extern QIcon ConfigureToolbars;
|
||||||
extern QIcon IC_CopyFile;
|
extern QIcon IC_CopyFile;
|
||||||
extern QIcon DefaultConductor;
|
extern QIcon DefaultConductor;
|
||||||
|
|||||||
@@ -17,7 +17,11 @@
|
|||||||
*/
|
*/
|
||||||
#include "qetpalette.h"
|
#include "qetpalette.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QImage>
|
||||||
#include <QStyle>
|
#include <QStyle>
|
||||||
|
#include <QWidget>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -74,6 +78,57 @@ bool QET::Palette::isDark(const QPalette &palette)
|
|||||||
return palette.color(QPalette::Active, QPalette::Window).lightness() < 128;
|
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 QET::Palette::contrastRatio(const QColor &a, const QColor &b)
|
||||||
{
|
{
|
||||||
double lighter = relativeLuminance(a);
|
double lighter = relativeLuminance(a);
|
||||||
@@ -177,3 +232,79 @@ QPalette QET::Palette::forFusion(const QPalette &platform)
|
|||||||
return withPlatformAccent(isDark(platform) ? fusionDark() : fusionLight(),
|
return withPlatformAccent(isDark(platform) ? fusionDark() : fusionLight(),
|
||||||
platform);
|
platform);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool QET::Palette::isLineArt(const QImage &image)
|
||||||
|
{
|
||||||
|
const QImage source = image.convertToFormat(QImage::Format_ARGB32);
|
||||||
|
int visible = 0;
|
||||||
|
int saturated = 0;
|
||||||
|
for (int y = 0; y < source.height(); ++y)
|
||||||
|
{
|
||||||
|
const QRgb *line = reinterpret_cast<const QRgb *>(source.constScanLine(y));
|
||||||
|
for (int x = 0; x < source.width(); ++x)
|
||||||
|
{
|
||||||
|
if (qAlpha(line[x]) <= 64)
|
||||||
|
continue;
|
||||||
|
++visible;
|
||||||
|
const QColor color(line[x]);
|
||||||
|
if (color.hslSaturationF() > 0.25 && color.value() > 60)
|
||||||
|
++saturated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return visible > 0 && saturated < visible * 0.20;
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage QET::Palette::invertedLightness(const QImage &image)
|
||||||
|
{
|
||||||
|
// Lightness of pure black after inversion: the dark palette's text.
|
||||||
|
const qreal ink = 220.0 / 255.0;
|
||||||
|
QImage result = image.convertToFormat(QImage::Format_ARGB32);
|
||||||
|
qreal darkest = 1.0;
|
||||||
|
for (int y = 0; y < result.height(); ++y)
|
||||||
|
{
|
||||||
|
const QRgb *line = reinterpret_cast<const QRgb *>(result.constScanLine(y));
|
||||||
|
for (int x = 0; x < result.width(); ++x)
|
||||||
|
if (qAlpha(line[x]) > 64)
|
||||||
|
darkest = qMin(darkest, QColor(line[x]).lightnessF());
|
||||||
|
}
|
||||||
|
const qreal span = qMax(1.0 - darkest, 1e-6);
|
||||||
|
for (int y = 0; y < result.height(); ++y)
|
||||||
|
{
|
||||||
|
QRgb *line = reinterpret_cast<QRgb *>(result.scanLine(y));
|
||||||
|
for (int x = 0; x < result.width(); ++x)
|
||||||
|
{
|
||||||
|
const int alpha = qAlpha(line[x]);
|
||||||
|
if (alpha == 0)
|
||||||
|
continue;
|
||||||
|
const QColor color(line[x]);
|
||||||
|
const qreal lightness = qBound(0.0, ink * (1.0 - (color.lightnessF() - darkest) / span), 1.0);
|
||||||
|
QColor out = QColor::fromHslF(qMax(color.hslHueF(), 0.0), color.hslSaturationF(), lightness);
|
||||||
|
out.setAlpha(alpha);
|
||||||
|
line[x] = out.rgba();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap QET::Palette::forPalette(const QPixmap &pixmap, const QPalette &palette)
|
||||||
|
{
|
||||||
|
if (pixmap.isNull() || !isDark(palette))
|
||||||
|
return pixmap;
|
||||||
|
const QImage image = pixmap.toImage();
|
||||||
|
if (!isLineArt(image))
|
||||||
|
return pixmap;
|
||||||
|
QPixmap result = QPixmap::fromImage(invertedLightness(image));
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,8 +18,11 @@
|
|||||||
#ifndef QET_PALETTE_H
|
#ifndef QET_PALETTE_H
|
||||||
#define QET_PALETTE_H
|
#define QET_PALETTE_H
|
||||||
|
|
||||||
|
#include <QImage>
|
||||||
#include <QPalette>
|
#include <QPalette>
|
||||||
|
#include <QPixmap>
|
||||||
|
|
||||||
|
class QImage;
|
||||||
class QStyle;
|
class QStyle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,6 +55,28 @@ namespace QET {
|
|||||||
*/
|
*/
|
||||||
bool isDark(const QPalette &palette);
|
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)
|
WCAG 2 contrast ratio between two opaque colors, from 1 (equal)
|
||||||
to 21 (black on white). Normal text needs at least 4.5, large
|
to 21 (black on white). Normal text needs at least 4.5, large
|
||||||
@@ -87,6 +112,41 @@ namespace QET {
|
|||||||
platform's accent color kept when it is readable.
|
platform's accent color kept when it is readable.
|
||||||
*/
|
*/
|
||||||
QPalette forFusion(const QPalette &platform);
|
QPalette forFusion(const QPalette &platform);
|
||||||
|
|
||||||
|
/**
|
||||||
|
True when fewer than a fifth of the visible pixels are
|
||||||
|
saturated: black or gray line art, which is what element
|
||||||
|
previews and most of QET's own icons are.
|
||||||
|
*/
|
||||||
|
bool isLineArt(const QImage &image);
|
||||||
|
|
||||||
|
/**
|
||||||
|
The image with its lightness inverted and hue, saturation and
|
||||||
|
alpha kept: the darkest ink becomes light gray (220), white
|
||||||
|
becomes black. The rule misc/make_icon_themes.py applies when
|
||||||
|
it builds the dark icon theme.
|
||||||
|
*/
|
||||||
|
QImage invertedLightness(const QImage &image);
|
||||||
|
|
||||||
|
/**
|
||||||
|
A picture drawn for a white sheet, made to read on palette:
|
||||||
|
returned as is on a light palette, and with its lightness
|
||||||
|
inverted on a dark one when it is line art. Colored art is
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,30 @@ m_project_properties_handler{this}
|
|||||||
m_default_guides.append(g);
|
m_default_guides.append(g);
|
||||||
}
|
}
|
||||||
settings.endArray();
|
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()
|
ProjectPropertiesHandler &QETProject::projectPropertiesHandler()
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
/*
|
||||||
|
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 "qetstyle.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QStyleOption>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "qetpalette.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QETStyle::QETStyle
|
||||||
|
@param base the style to wrap; this object takes ownership of it.
|
||||||
|
*/
|
||||||
|
QETStyle::QETStyle(QStyle *base) :
|
||||||
|
QProxyStyle(base)
|
||||||
|
{
|
||||||
|
setObjectName(base ? base->objectName() : QString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QETStyle::isLineArt
|
||||||
|
The same rule misc/make_icon_themes.py uses to pick the icons that
|
||||||
|
get a dark variant: an icon is line art when fewer than 20% of its
|
||||||
|
visible pixels are saturated.
|
||||||
|
*/
|
||||||
|
bool QETStyle::isLineArt(const QImage &image)
|
||||||
|
{
|
||||||
|
const QImage source = image.convertToFormat(QImage::Format_ARGB32);
|
||||||
|
int visible = 0;
|
||||||
|
int saturated = 0;
|
||||||
|
for (int y = 0; y < source.height(); ++y)
|
||||||
|
{
|
||||||
|
const QRgb *line = reinterpret_cast<const QRgb *>(source.constScanLine(y));
|
||||||
|
for (int x = 0; x < source.width(); ++x)
|
||||||
|
{
|
||||||
|
const QRgb pixel = line[x];
|
||||||
|
if (qAlpha(pixel) <= 64)
|
||||||
|
continue;
|
||||||
|
++visible;
|
||||||
|
const QColor color(pixel);
|
||||||
|
if (color.hslSaturationF() > 0.25 && color.value() > 60)
|
||||||
|
++saturated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return visible > 0 && saturated < visible * 0.20;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QETStyle::hoverColor
|
||||||
|
The palette's highlight color is the accent users already know from
|
||||||
|
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. 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 = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QETStyle::tinted
|
||||||
|
@return pixmap with every pixel set to color, alpha kept.
|
||||||
|
*/
|
||||||
|
QPixmap QETStyle::tinted(const QPixmap &pixmap, const QColor &color)
|
||||||
|
{
|
||||||
|
QImage image = pixmap.toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||||
|
QPainter painter(&image);
|
||||||
|
painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
|
||||||
|
painter.fillRect(image.rect(), color);
|
||||||
|
painter.end();
|
||||||
|
|
||||||
|
QPixmap result = QPixmap::fromImage(image);
|
||||||
|
result.setDevicePixelRatio(pixmap.devicePixelRatio());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QETStyle::menuIcon
|
||||||
|
An icon for a highlighted menu item: Normal as the original, Active
|
||||||
|
as the original in the HighlightedText color when it is line art.
|
||||||
|
Cached per source icon, size and device pixel ratio, since menus
|
||||||
|
repaint on every pointer move.
|
||||||
|
*/
|
||||||
|
QIcon QETStyle::menuIcon(const QIcon &icon, const QPalette &palette, int size, qreal dpr) const
|
||||||
|
{
|
||||||
|
const QString key = QString("%1/%2/%3/%4")
|
||||||
|
.arg(icon.cacheKey()).arg(size).arg(dpr)
|
||||||
|
.arg(palette.color(QPalette::Active, QPalette::HighlightedText).name());
|
||||||
|
auto it = menu_icons_.constFind(key);
|
||||||
|
if (it != menu_icons_.constEnd())
|
||||||
|
return *it;
|
||||||
|
|
||||||
|
const QPixmap normal = icon.pixmap(QSize(size, size), dpr, QIcon::Normal);
|
||||||
|
QIcon result;
|
||||||
|
result.addPixmap(normal, QIcon::Normal);
|
||||||
|
result.addPixmap(isLineArt(normal.toImage())
|
||||||
|
? tinted(normal, palette.color(QPalette::Active, QPalette::HighlightedText))
|
||||||
|
: normal,
|
||||||
|
QIcon::Active);
|
||||||
|
result.addPixmap(icon.pixmap(QSize(size, size), dpr, QIcon::Disabled), QIcon::Disabled);
|
||||||
|
menu_icons_.insert(key, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QETStyle::drawControl
|
||||||
|
CE_MenuItem with an icon and a highlight: swap the icon for menuIcon()
|
||||||
|
so the base style's QIcon::Active request gets an icon that reads on
|
||||||
|
the highlight bar. Everything else goes to the base style.
|
||||||
|
*/
|
||||||
|
void QETStyle::drawControl(ControlElement element,
|
||||||
|
const QStyleOption *option,
|
||||||
|
QPainter *painter,
|
||||||
|
const QWidget *widget) const
|
||||||
|
{
|
||||||
|
if (element == CE_MenuItem)
|
||||||
|
{
|
||||||
|
const auto *item = qstyleoption_cast<const QStyleOptionMenuItem *>(option);
|
||||||
|
if (item && !item->icon.isNull()
|
||||||
|
&& (item->state & State_Selected) && (item->state & State_Enabled))
|
||||||
|
{
|
||||||
|
QStyleOptionMenuItem copy = *item;
|
||||||
|
const int size = pixelMetric(PM_SmallIconSize, option, widget);
|
||||||
|
const qreal dpr = widget ? widget->devicePixelRatio()
|
||||||
|
: QApplication::instance() ? qApp->devicePixelRatio() : 1.0;
|
||||||
|
copy.icon = menuIcon(item->icon, item->palette, size, dpr);
|
||||||
|
QProxyStyle::drawControl(element, ©, painter, widget);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QProxyStyle::drawControl(element, option, painter, widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QETStyle::generatedIconPixmap
|
||||||
|
QIcon::Active: a line-art icon comes back tinted in hoverColor(),
|
||||||
|
alpha kept, so the hovered icon differs from the normal one on both
|
||||||
|
light and dark palettes. Colored icons and every
|
||||||
|
other mode go to the base style.
|
||||||
|
*/
|
||||||
|
QPixmap QETStyle::generatedIconPixmap(QIcon::Mode mode,
|
||||||
|
const QPixmap &pixmap,
|
||||||
|
const QStyleOption *option) const
|
||||||
|
{
|
||||||
|
if (mode != QIcon::Active || pixmap.isNull())
|
||||||
|
return QProxyStyle::generatedIconPixmap(mode, pixmap, option);
|
||||||
|
|
||||||
|
if (!isLineArt(pixmap.toImage()))
|
||||||
|
return QProxyStyle::generatedIconPixmap(mode, pixmap, option);
|
||||||
|
|
||||||
|
const QPalette palette = option ? option->palette : QApplication::palette();
|
||||||
|
return tinted(pixmap, hoverColor(palette));
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
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 QET_STYLE_H
|
||||||
|
#define QET_STYLE_H
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QProxyStyle>
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief The QETStyle class
|
||||||
|
A proxy over the running widget style that gives icons a hover state.
|
||||||
|
|
||||||
|
QToolButton, QTabBar and item views ask the application style for the
|
||||||
|
QIcon::Active pixmap of an icon while the mouse is over it. Every
|
||||||
|
built-in style returns the icon unchanged for that mode, so only the
|
||||||
|
button frame changes on hover, and on a dark palette Fusion's frame is
|
||||||
|
too faint to see (GitHub #870). This proxy answers QIcon::Active for
|
||||||
|
line-art icons with the icon tinted in the palette's highlight color,
|
||||||
|
lightened when needed to reach 3:1 on a button face, so the icon
|
||||||
|
itself changes on both light and dark palettes. Colored icons keep
|
||||||
|
their colors. Every other mode is left to the base style.
|
||||||
|
|
||||||
|
Fusion also asks for QIcon::Active for the icon of a highlighted menu
|
||||||
|
item and paints it on the highlight bar, where the hover tint would
|
||||||
|
vanish. The generated pixmap is cached per icon and cannot tell a
|
||||||
|
menu from a toolbar, so drawControl(CE_MenuItem) hands the base style
|
||||||
|
an icon whose Active pixmap is the icon in the highlighted-text color.
|
||||||
|
*/
|
||||||
|
class QETStyle : public QProxyStyle
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit QETStyle(QStyle *base);
|
||||||
|
~QETStyle() override = default;
|
||||||
|
|
||||||
|
QPixmap generatedIconPixmap(QIcon::Mode mode,
|
||||||
|
const QPixmap &pixmap,
|
||||||
|
const QStyleOption *option) const override;
|
||||||
|
void drawControl(ControlElement element,
|
||||||
|
const QStyleOption *option,
|
||||||
|
QPainter *painter,
|
||||||
|
const QWidget *widget = nullptr) const override;
|
||||||
|
|
||||||
|
/// True when fewer than a fifth of the visible pixels are saturated.
|
||||||
|
static bool isLineArt(const QImage &image);
|
||||||
|
/// The highlight color, lightened until it reads at 3:1 on a button.
|
||||||
|
static QColor hoverColor(const QPalette &palette);
|
||||||
|
/// pixmap with every pixel set to color, alpha kept.
|
||||||
|
static QPixmap tinted(const QPixmap &pixmap, const QColor &color);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QIcon menuIcon(const QIcon &icon, const QPalette &palette, int size, qreal dpr) const;
|
||||||
|
mutable QHash<QString, QIcon> menu_icons_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -27,11 +27,25 @@
|
|||||||
#include "../qet.h"
|
#include "../qet.h"
|
||||||
#include "../qetgraphicsitem/element.h"
|
#include "../qetgraphicsitem/element.h"
|
||||||
#include "../qetmessagebox.h"
|
#include "../qetmessagebox.h"
|
||||||
|
#include "../dataBase/projectdatabase.h"
|
||||||
#include "../qetproject.h"
|
#include "../qetproject.h"
|
||||||
#include "../qetresult.h"
|
#include "../qetresult.h"
|
||||||
|
#include "../qetgraphicsitem/conductor.h"
|
||||||
|
#include "../qetgraphicsitem/independenttextitem.h"
|
||||||
|
#include "../qetgraphicsitem/qetshapeitem.h"
|
||||||
|
#include "../qetgraphicsitem/terminal.h"
|
||||||
|
#include "../qetinformation.h"
|
||||||
|
#include "../titleblockproperties.h"
|
||||||
#include "../undocommand/addgraphicsobjectcommand.h"
|
#include "../undocommand/addgraphicsobjectcommand.h"
|
||||||
|
#include "../undocommand/changeelementinformationcommand.h"
|
||||||
|
#include "../undocommand/changetitleblockcommand.h"
|
||||||
#include "../undocommand/deleteqgraphicsitemcommand.h"
|
#include "../undocommand/deleteqgraphicsitemcommand.h"
|
||||||
|
#include "../undocommand/linkelementcommand.h"
|
||||||
|
#include "../utils/conductorcreator.h"
|
||||||
|
|
||||||
|
#include <QSqlError>
|
||||||
|
#include <QSqlQuery>
|
||||||
|
#include <QSqlRecord>
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
#include <QUndoCommand>
|
#include <QUndoCommand>
|
||||||
|
|
||||||
@@ -268,6 +282,137 @@ Element *QetScriptApi::findElement(int folioIndex, const QString &elementUuid) c
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Terminal *QetScriptApi::findTerminal(int folioIndex, const QString &elementUuid,
|
||||||
|
int terminalIndex, const QString &caller)
|
||||||
|
{
|
||||||
|
Element *element = findElement(folioIndex, elementUuid);
|
||||||
|
if (!element) {
|
||||||
|
log(QStringLiteral("qet.%1: no element %2 on folio %3").arg(caller, elementUuid).arg(folioIndex));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const QList<Terminal *> terminals = element->terminals();
|
||||||
|
if (terminalIndex < 0 || terminalIndex >= terminals.count()) {
|
||||||
|
log(QStringLiteral("qet.%1: %2 has %3 terminal(s), no index %4")
|
||||||
|
.arg(caller, element->name()).arg(terminals.count()).arg(terminalIndex));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return terminals.at(terminalIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QetScriptApi::findConductor
|
||||||
|
The single conductor attached to a terminal, or nullptr.
|
||||||
|
|
||||||
|
Conductors carry no persisted uuid, and the terminal1/terminal2 ids the
|
||||||
|
file uses for their ends are folio-scoped integers QElectroTech
|
||||||
|
renumbers on every save, so a conductor has no name that survives a
|
||||||
|
save/load cycle. Naming one by a terminal it is attached to does, and
|
||||||
|
it reads the way the question is usually asked ("the wire on A1 of
|
||||||
|
KM1"). A terminal with several conductors on it does not name one, so
|
||||||
|
refuse rather than silently take the first.
|
||||||
|
*/
|
||||||
|
Conductor *QetScriptApi::findConductor(int folioIndex, const QString &elementUuid,
|
||||||
|
int terminalIndex, const QString &caller)
|
||||||
|
{
|
||||||
|
Terminal *terminal = findTerminal(folioIndex, elementUuid, terminalIndex, caller);
|
||||||
|
if (!terminal) return nullptr;
|
||||||
|
const QList<Conductor *> conductors = terminal->conductors();
|
||||||
|
if (conductors.isEmpty()) {
|
||||||
|
log(QStringLiteral("qet.%1: terminal %2 of %3 has no conductor on it")
|
||||||
|
.arg(caller).arg(terminalIndex).arg(elementUuid));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (conductors.count() > 1) {
|
||||||
|
log(QStringLiteral("qet.%1: terminal %2 of %3 carries %4 conductors, so it does "
|
||||||
|
"not name one -- use a terminal with a single conductor")
|
||||||
|
.arg(caller).arg(terminalIndex).arg(elementUuid).arg(conductors.count()));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return conductors.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/**
|
||||||
|
Read or write one named conductor property. The names are the ones the
|
||||||
|
project file uses for the same fields (ConductorProperties::toXml), so
|
||||||
|
that what a script sets is what a reader of the .qet sees, rather than
|
||||||
|
a third spelling invented here.
|
||||||
|
*/
|
||||||
|
QString conductorPropertyValue(const ConductorProperties &p, const QString &name)
|
||||||
|
{
|
||||||
|
if (name == QLatin1String("num")) return p.text;
|
||||||
|
if (name == QLatin1String("formula")) return p.m_formula;
|
||||||
|
if (name == QLatin1String("function")) return p.m_function;
|
||||||
|
if (name == QLatin1String("bus")) return p.m_bus;
|
||||||
|
if (name == QLatin1String("cable")) return p.m_cable;
|
||||||
|
if (name == QLatin1String("tension_protocol")) return p.m_tension_protocol;
|
||||||
|
if (name == QLatin1String("conductor_color")) return p.m_wire_color;
|
||||||
|
if (name == QLatin1String("conductor_section")) return p.m_wire_section;
|
||||||
|
if (name == QLatin1String("color")) return p.color.name();
|
||||||
|
if (name == QLatin1String("text_color")) return p.text_color.name();
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool setConductorPropertyValue(ConductorProperties &p, const QString &name, const QString &value)
|
||||||
|
{
|
||||||
|
if (name == QLatin1String("num")) { p.text = value; return true; }
|
||||||
|
if (name == QLatin1String("formula")) { p.m_formula = value; return true; }
|
||||||
|
if (name == QLatin1String("function")) { p.m_function = value; return true; }
|
||||||
|
if (name == QLatin1String("bus")) { p.m_bus = value; return true; }
|
||||||
|
if (name == QLatin1String("cable")) { p.m_cable = value; return true; }
|
||||||
|
if (name == QLatin1String("tension_protocol")) { p.m_tension_protocol = value; return true; }
|
||||||
|
if (name == QLatin1String("conductor_color")) { p.m_wire_color = value; return true; }
|
||||||
|
if (name == QLatin1String("conductor_section")) { p.m_wire_section = value; return true; }
|
||||||
|
// The two real colours are QColor, not free text: an unparseable name
|
||||||
|
// would otherwise be stored as an invalid colour and drawn as black.
|
||||||
|
if (name == QLatin1String("color") || name == QLatin1String("text_color"))
|
||||||
|
{
|
||||||
|
const QColor c(value);
|
||||||
|
if (!c.isValid()) return false;
|
||||||
|
if (name == QLatin1String("color")) p.color = c; else p.text_color = c;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList &conductorPropertyNames()
|
||||||
|
{
|
||||||
|
static const QStringList names {
|
||||||
|
QStringLiteral("num"), QStringLiteral("formula"), QStringLiteral("function"),
|
||||||
|
QStringLiteral("bus"), QStringLiteral("cable"), QStringLiteral("tension_protocol"),
|
||||||
|
QStringLiteral("conductor_color"), QStringLiteral("conductor_section"),
|
||||||
|
QStringLiteral("color"), QStringLiteral("text_color")};
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool QetScriptApi::setInfoKey(int folioIndex, const QString &elementUuid,
|
||||||
|
const QString &key, const QString &value, const QString &caller)
|
||||||
|
{
|
||||||
|
if (!m_project) return false;
|
||||||
|
if (m_project->isReadOnly()) {
|
||||||
|
log(QStringLiteral("qet.%1: project is read-only").arg(caller));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (key.isEmpty()) {
|
||||||
|
log(QStringLiteral("qet.%1: empty information key").arg(caller));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Element *element = findElement(folioIndex, elementUuid);
|
||||||
|
if (!element) return false;
|
||||||
|
|
||||||
|
const DiagramContext old_info = element->elementInformations();
|
||||||
|
if (old_info.value(key).toString() == value) return true; // nothing to push
|
||||||
|
DiagramContext new_info = old_info;
|
||||||
|
new_info.addValue(key, value);
|
||||||
|
|
||||||
|
auto *cmd = new ChangeElementInformationCommand(element, old_info, new_info);
|
||||||
|
m_project->undoStack()->push(cmd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief QetScriptApi::addElement
|
@brief QetScriptApi::addElement
|
||||||
Place a new element on a folio, through the same AddGraphicsObjectCommand
|
Place a new element on a folio, through the same AddGraphicsObjectCommand
|
||||||
@@ -417,6 +562,737 @@ bool QetScriptApi::deleteElement(int folioIndex, const QString &elementUuid)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool QetScriptApi::rotateElement(int folioIndex, const QString &elementUuid, double angle)
|
||||||
|
{
|
||||||
|
if (m_project && m_project->isReadOnly()) {
|
||||||
|
log(QStringLiteral("qet.rotateElement: project is read-only"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Element *element = findElement(folioIndex, elementUuid);
|
||||||
|
if (!element) return false;
|
||||||
|
|
||||||
|
// The same property command RotateSelectionCommand pushes for an
|
||||||
|
// Element -- deliberately not RotateSelectionCommand itself, which
|
||||||
|
// works on diagram->selectedItems() and would mean quietly rewriting
|
||||||
|
// the user's selection to rotate one element by uuid. For a single
|
||||||
|
// element the two are mechanically identical: that class special-cases
|
||||||
|
// Element::Type to exactly this one command, and only adds a second,
|
||||||
|
// positional one when rotating a multi-item selection as a group.
|
||||||
|
auto *cmd = new QPropertyUndoCommand(element, "rotation",
|
||||||
|
QVariant(element->rotation()),
|
||||||
|
QVariant(element->rotation() + angle));
|
||||||
|
cmd->setText(QObject::tr("Pivoter %1").arg(element->name()));
|
||||||
|
m_project->undoStack()->push(cmd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList QetScriptApi::elementUuids(int folioIndex) const
|
||||||
|
{
|
||||||
|
QStringList uuids;
|
||||||
|
if (!m_project) return uuids;
|
||||||
|
const QList<Diagram *> diagrams = m_project->diagrams();
|
||||||
|
if (folioIndex < 0 || folioIndex >= diagrams.count()) return uuids;
|
||||||
|
DiagramContent content(diagrams.at(folioIndex), false);
|
||||||
|
for (Element *elmt : std::as_const(content.m_elements)) {
|
||||||
|
uuids << elmt->uuid().toString();
|
||||||
|
}
|
||||||
|
return uuids;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString QetScriptApi::elementName(int folioIndex, const QString &elementUuid) const
|
||||||
|
{
|
||||||
|
Element *element = findElement(folioIndex, elementUuid);
|
||||||
|
return element ? element->name() : QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QetScriptApi::elementTerminals
|
||||||
|
The element's terminals, in the order addConductor() indexes them: one
|
||||||
|
entry per terminal, "<index>: <name> (<n> conductor(s))". Descriptive
|
||||||
|
rather than structured because its only job is to let a script -- or a
|
||||||
|
human reading a script's output -- see which index is which before
|
||||||
|
wiring anything to it.
|
||||||
|
|
||||||
|
Indexes, not uuids, because a terminal uuid does not address a terminal
|
||||||
|
on a folio. Terminal::uuid() comes from the catalog .elmt definition
|
||||||
|
(see Terminal::stableUuid()), so it is empty for most of the installed
|
||||||
|
base, and where it is not, every instance of that same element carries
|
||||||
|
the same one -- two coils of one type placed side by side have
|
||||||
|
byte-identical terminal uuids, which is plainly visible in the saved
|
||||||
|
file of any project written through this API. The order of
|
||||||
|
Element::terminals() also comes from the definition, but it is at least
|
||||||
|
unambiguous within the element the caller has already named by uuid.
|
||||||
|
*/
|
||||||
|
QStringList QetScriptApi::elementTerminals(int folioIndex, const QString &elementUuid) const
|
||||||
|
{
|
||||||
|
QStringList list;
|
||||||
|
Element *element = findElement(folioIndex, elementUuid);
|
||||||
|
if (!element) return list;
|
||||||
|
const QList<Terminal *> terminals = element->terminals();
|
||||||
|
for (int i = 0 ; i < terminals.count() ; ++i)
|
||||||
|
{
|
||||||
|
Terminal *t = terminals.at(i);
|
||||||
|
list << QStringLiteral("%1: %2 (%3 conductor(s))")
|
||||||
|
.arg(i)
|
||||||
|
.arg(t->name().isEmpty() ? QStringLiteral("-") : t->name())
|
||||||
|
.arg(t->conductorsCount());
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString QetScriptApi::elementInfo(int folioIndex, const QString &elementUuid, const QString &key) const
|
||||||
|
{
|
||||||
|
Element *element = findElement(folioIndex, elementUuid);
|
||||||
|
if (!element) return QString();
|
||||||
|
return element->elementInformations().value(key).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QetScriptApi::setElementInfo(int folioIndex, const QString &elementUuid,
|
||||||
|
const QString &key, const QString &value)
|
||||||
|
{
|
||||||
|
return setInfoKey(folioIndex, elementUuid, key, value, QStringLiteral("setElementInfo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString QetScriptApi::elementLabel(int folioIndex, const QString &elementUuid) const
|
||||||
|
{
|
||||||
|
return elementInfo(folioIndex, elementUuid, QETInformation::ELMT_LABEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QetScriptApi::setElementLabel(int folioIndex, const QString &elementUuid, const QString &label)
|
||||||
|
{
|
||||||
|
return setInfoKey(folioIndex, elementUuid, QETInformation::ELMT_LABEL, label,
|
||||||
|
QStringLiteral("setElementLabel"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QetScriptApi::addConductor
|
||||||
|
Wire terminal terminalIndexA of one element to terminalIndexB of
|
||||||
|
another, on the same folio, through ConductorCreator -- the same class
|
||||||
|
the "draw a selection rectangle over terminals" GUI path uses. Going
|
||||||
|
through it rather than constructing a Conductor directly is what makes
|
||||||
|
the new conductor inherit an existing potential's properties and take
|
||||||
|
part in conductor auto-numbering; a hand-built one would be silently
|
||||||
|
outside both.
|
||||||
|
|
||||||
|
Refuses, rather than creating anything, when the two terminals sit on
|
||||||
|
two different existing potentials: ConductorCreator then has to ask
|
||||||
|
which one's properties the new conductor should inherit, and it asks
|
||||||
|
with a plain modal QDialog that QET::QetMessageBox's non-interactive
|
||||||
|
mode does not cover -- so under headless --run there would be nobody to
|
||||||
|
answer it and the script would hang forever. Same reasoning, and the
|
||||||
|
same choice, as addElement() makes about the import-conflict dialog.
|
||||||
|
@return true if a conductor was created
|
||||||
|
*/
|
||||||
|
bool QetScriptApi::addConductor(int folioIndex,
|
||||||
|
const QString &elementUuidA, int terminalIndexA,
|
||||||
|
const QString &elementUuidB, int terminalIndexB)
|
||||||
|
{
|
||||||
|
if (!m_project) return false;
|
||||||
|
if (m_project->isReadOnly()) {
|
||||||
|
log(QStringLiteral("qet.addConductor: project is read-only"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QString caller = QStringLiteral("addConductor");
|
||||||
|
Terminal *t1 = findTerminal(folioIndex, elementUuidA, terminalIndexA, caller);
|
||||||
|
Terminal *t2 = findTerminal(folioIndex, elementUuidB, terminalIndexB, caller);
|
||||||
|
if (!t1 || !t2) return false;
|
||||||
|
|
||||||
|
if (t1 == t2) {
|
||||||
|
log(QStringLiteral("qet.addConductor: both ends are the same terminal"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (t1->isLinkedTo(t2)) {
|
||||||
|
log(QStringLiteral("qet.addConductor: those two terminals are already wired together"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!t1->canBeLinkedTo(t2)) {
|
||||||
|
log(QStringLiteral("qet.addConductor: those two terminals cannot be linked"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QList<Terminal *> terminals {t1, t2};
|
||||||
|
if (ConductorCreator::needsPotentialChoice(terminals)) {
|
||||||
|
log(QStringLiteral("qet.addConductor: those terminals are on two different existing "
|
||||||
|
"potentials, so creating a conductor would ask which one to inherit "
|
||||||
|
"-- refusing rather than open a dialog no script can answer"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Diagram *diagram = m_project->diagrams().at(folioIndex);
|
||||||
|
ConductorCreator creator(diagram, terminals);
|
||||||
|
Q_UNUSED(creator)
|
||||||
|
|
||||||
|
// ConductorCreator has no return value and several ways to decline
|
||||||
|
// quietly, so report what actually happened rather than that it ran.
|
||||||
|
return t1->isLinkedTo(t2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QetScriptApi::conductors
|
||||||
|
One line per conductor on the folio: which terminals it joins and its
|
||||||
|
number, in the form setConductorProperty() addresses them. Descriptive
|
||||||
|
rather than structured for the same reason elementTerminals() is -- it
|
||||||
|
exists so a script, or a person reading its output, can see what is
|
||||||
|
there before changing it.
|
||||||
|
*/
|
||||||
|
QStringList QetScriptApi::conductors(int folioIndex) const
|
||||||
|
{
|
||||||
|
QStringList list;
|
||||||
|
if (!m_project) return list;
|
||||||
|
const QList<Diagram *> diagrams = m_project->diagrams();
|
||||||
|
if (folioIndex < 0 || folioIndex >= diagrams.count()) return list;
|
||||||
|
|
||||||
|
auto describe = [](Terminal *t) -> QString {
|
||||||
|
if (!t || !t->parentElement()) return QStringLiteral("?");
|
||||||
|
return QStringLiteral("%1 terminal %2")
|
||||||
|
.arg(t->parentElement()->uuid().toString())
|
||||||
|
.arg(t->parentElement()->terminals().indexOf(t));
|
||||||
|
};
|
||||||
|
|
||||||
|
DiagramContent content(diagrams.at(folioIndex), false);
|
||||||
|
const QList<Conductor *> all = content.conductors(DiagramContent::AnyConductor);
|
||||||
|
for (Conductor *c : all)
|
||||||
|
{
|
||||||
|
list << QStringLiteral("%1 -- %2 : num='%3'")
|
||||||
|
.arg(describe(c->terminal1), describe(c->terminal2), c->properties().text);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString QetScriptApi::conductorProperty(int folioIndex, const QString &elementUuid,
|
||||||
|
int terminalIndex, const QString &property) const
|
||||||
|
{
|
||||||
|
// const_cast: findConductor logs, and log() writes to stderr, which is
|
||||||
|
// not a const operation on this object. The lookup itself changes
|
||||||
|
// nothing.
|
||||||
|
auto *self = const_cast<QetScriptApi *>(this);
|
||||||
|
Conductor *conductor = self->findConductor(folioIndex, elementUuid, terminalIndex,
|
||||||
|
QStringLiteral("conductorProperty"));
|
||||||
|
if (!conductor) return QString();
|
||||||
|
return conductorPropertyValue(conductor->properties(), property);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QetScriptApi::setConductorProperty
|
||||||
|
Set one property on the conductor attached to a terminal -- and on
|
||||||
|
every other conductor of the same electrical potential.
|
||||||
|
|
||||||
|
That is not a convenience, it is the rule the application already
|
||||||
|
follows: SearchAndReplaceWorker does exactly this, pushing one
|
||||||
|
QPropertyUndoCommand per conductor of relatedPotentialConductors()
|
||||||
|
inside a single macro, because a wire number, colour or section
|
||||||
|
describes a potential and not one drawn segment. Setting it on one
|
||||||
|
conductor and leaving the rest of the potential disagreeing would
|
||||||
|
produce a file no GUI action could have produced.
|
||||||
|
@return true if anything was changed, or if it already held that value
|
||||||
|
*/
|
||||||
|
bool QetScriptApi::setConductorProperty(int folioIndex, const QString &elementUuid,
|
||||||
|
int terminalIndex, const QString &property,
|
||||||
|
const QString &value)
|
||||||
|
{
|
||||||
|
if (!m_project) return false;
|
||||||
|
const QString caller = QStringLiteral("setConductorProperty");
|
||||||
|
if (m_project->isReadOnly()) {
|
||||||
|
log(QStringLiteral("qet.%1: project is read-only").arg(caller));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!conductorPropertyNames().contains(property)) {
|
||||||
|
log(QStringLiteral("qet.%1: unknown property '%2'; expected one of %3")
|
||||||
|
.arg(caller, property, conductorPropertyNames().join(QStringLiteral(", "))));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Conductor *conductor = findConductor(folioIndex, elementUuid, terminalIndex, caller);
|
||||||
|
if (!conductor) return false;
|
||||||
|
|
||||||
|
ConductorProperties properties = conductor->properties();
|
||||||
|
if (!setConductorPropertyValue(properties, property, value)) {
|
||||||
|
log(QStringLiteral("qet.%1: '%2' is not a valid value for %3")
|
||||||
|
.arg(caller, value, property));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (properties == conductor->properties()) return true; // already so
|
||||||
|
|
||||||
|
QSet<Conductor *> potential = conductor->relatedPotentialConductors(true);
|
||||||
|
potential << conductor;
|
||||||
|
|
||||||
|
m_project->undoStack()->beginMacro(QObject::tr("Modifier les propriétés du conducteur"));
|
||||||
|
for (Conductor *c : std::as_const(potential))
|
||||||
|
{
|
||||||
|
QVariant old_value, new_value;
|
||||||
|
old_value.setValue(c->properties());
|
||||||
|
new_value.setValue(properties);
|
||||||
|
m_project->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value));
|
||||||
|
}
|
||||||
|
m_project->undoStack()->endMacro();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString QetScriptApi::elementLinkType(int folioIndex, const QString &elementUuid) const
|
||||||
|
{
|
||||||
|
Element *element = findElement(folioIndex, elementUuid);
|
||||||
|
if (!element) return QString();
|
||||||
|
switch (element->linkType())
|
||||||
|
{
|
||||||
|
case Element::Simple: return QStringLiteral("simple");
|
||||||
|
case Element::NextReport: return QStringLiteral("next_report");
|
||||||
|
case Element::PreviousReport: return QStringLiteral("previous_report");
|
||||||
|
case Element::Master: return QStringLiteral("master");
|
||||||
|
case Element::Slave: return QStringLiteral("slave");
|
||||||
|
case Element::Terminale: return QStringLiteral("terminal");
|
||||||
|
default: return QStringLiteral("unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList QetScriptApi::linkedElements(int folioIndex, const QString &elementUuid) const
|
||||||
|
{
|
||||||
|
QStringList list;
|
||||||
|
Element *element = findElement(folioIndex, elementUuid);
|
||||||
|
if (!element) return list;
|
||||||
|
const QList<Element *> linked = element->linkedElements();
|
||||||
|
for (Element *e : linked) {
|
||||||
|
list << e->uuid().toString();
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QetScriptApi::linkElements
|
||||||
|
Link two elements -- a master to a slave, or one report to its
|
||||||
|
counterpart. Two folio indices because a master and its slave normally
|
||||||
|
sit on different folios; that is the usual case, not the exception.
|
||||||
|
|
||||||
|
Whether a given pair may be linked is not decided here.
|
||||||
|
LinkElementCommand::isLinkable() already holds those rules -- that a
|
||||||
|
master takes a slave and not another master, that a PLC master pairs
|
||||||
|
only with a PLC slave, that a next-report pairs only with a
|
||||||
|
previous-report, and that the target is free -- and asking it rather
|
||||||
|
than re-deriving them is what keeps a script from producing a link the
|
||||||
|
GUI would refuse to make.
|
||||||
|
*/
|
||||||
|
bool QetScriptApi::linkElements(int folioIndexA, const QString &elementUuidA,
|
||||||
|
int folioIndexB, const QString &elementUuidB)
|
||||||
|
{
|
||||||
|
if (!m_project) return false;
|
||||||
|
if (m_project->isReadOnly()) {
|
||||||
|
log(QStringLiteral("qet.linkElements: project is read-only"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Element *a = findElement(folioIndexA, elementUuidA);
|
||||||
|
Element *b = findElement(folioIndexB, elementUuidB);
|
||||||
|
if (!a || !b) {
|
||||||
|
log(QStringLiteral("qet.linkElements: %1 does not resolve to an element")
|
||||||
|
.arg(a ? elementUuidB : elementUuidA));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (a == b) {
|
||||||
|
log(QStringLiteral("qet.linkElements: an element cannot be linked to itself"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!LinkElementCommand::isLinkable(a, b)) {
|
||||||
|
log(QStringLiteral("qet.linkElements: %1 (%2) cannot be linked to %3 (%4) -- "
|
||||||
|
"check the two link types, and that the target is still free")
|
||||||
|
.arg(elementUuidA, elementLinkType(folioIndexA, elementUuidA),
|
||||||
|
elementUuidB, elementLinkType(folioIndexB, elementUuidB)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *cmd = new LinkElementCommand(a);
|
||||||
|
cmd->setLink(b);
|
||||||
|
m_project->undoStack()->push(cmd);
|
||||||
|
return a->linkedElements().contains(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QetScriptApi::unlinkElement(int folioIndex, const QString &elementUuid)
|
||||||
|
{
|
||||||
|
if (!m_project) return false;
|
||||||
|
if (m_project->isReadOnly()) {
|
||||||
|
log(QStringLiteral("qet.unlinkElement: project is read-only"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Element *element = findElement(folioIndex, elementUuid);
|
||||||
|
if (!element) return false;
|
||||||
|
if (element->linkedElements().isEmpty()) return true; // nothing to undo
|
||||||
|
|
||||||
|
auto *cmd = new LinkElementCommand(element);
|
||||||
|
cmd->unlinkAll();
|
||||||
|
m_project->undoStack()->push(cmd);
|
||||||
|
return element->linkedElements().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/**
|
||||||
|
Reading order for items that have no identity but their position:
|
||||||
|
top to bottom, then left to right.
|
||||||
|
|
||||||
|
On sceneBoundingRect(), not pos(): a QetShapeItem keeps its geometry in
|
||||||
|
its line/rect/polygon, and its pos() stays at the origin, so three
|
||||||
|
shapes drawn in different places all sort as (0, 0) and the ordering
|
||||||
|
collapses -- which is exactly what the first version of this did, and
|
||||||
|
it made every shape index refer to whichever one the set happened to
|
||||||
|
yield first. The scene bounding rect reflects where the item actually
|
||||||
|
is for both kinds.
|
||||||
|
*/
|
||||||
|
template <typename T>
|
||||||
|
QList<T *> sortedByPosition(const QSet<T *> &items)
|
||||||
|
{
|
||||||
|
QList<T *> list(items.cbegin(), items.cend());
|
||||||
|
std::sort(list.begin(), list.end(), [](T *a, T *b) {
|
||||||
|
const QPointF pa = a->sceneBoundingRect().topLeft();
|
||||||
|
const QPointF pb = b->sceneBoundingRect().topLeft();
|
||||||
|
if (pa.y() != pb.y()) return pa.y() < pb.y();
|
||||||
|
if (pa.x() != pb.x()) return pa.x() < pb.x();
|
||||||
|
// Two items genuinely at the same point still need a total order,
|
||||||
|
// or std::sort's result depends on the set's iteration order.
|
||||||
|
return a < b;
|
||||||
|
});
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
QList<IndependentTextItem *> QetScriptApi::sortedTexts(int folioIndex) const
|
||||||
|
{
|
||||||
|
if (!m_project) return {};
|
||||||
|
const QList<Diagram *> diagrams = m_project->diagrams();
|
||||||
|
if (folioIndex < 0 || folioIndex >= diagrams.count()) return {};
|
||||||
|
DiagramContent content(diagrams.at(folioIndex), false);
|
||||||
|
return sortedByPosition(content.m_text_fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QetShapeItem *> QetScriptApi::sortedShapes(int folioIndex) const
|
||||||
|
{
|
||||||
|
if (!m_project) return {};
|
||||||
|
const QList<Diagram *> diagrams = m_project->diagrams();
|
||||||
|
if (folioIndex < 0 || folioIndex >= diagrams.count()) return {};
|
||||||
|
DiagramContent content(diagrams.at(folioIndex), false);
|
||||||
|
return sortedByPosition(content.m_shapes);
|
||||||
|
}
|
||||||
|
|
||||||
|
IndependentTextItem *QetScriptApi::findText(int folioIndex, int textIndex, const QString &caller)
|
||||||
|
{
|
||||||
|
const QList<IndependentTextItem *> list = sortedTexts(folioIndex);
|
||||||
|
if (textIndex < 0 || textIndex >= list.count()) {
|
||||||
|
log(QStringLiteral("qet.%1: folio %2 has %3 independent text(s), no index %4")
|
||||||
|
.arg(caller).arg(folioIndex).arg(list.count()).arg(textIndex));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return list.at(textIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList QetScriptApi::texts(int folioIndex) const
|
||||||
|
{
|
||||||
|
QStringList out;
|
||||||
|
const QList<IndependentTextItem *> list = sortedTexts(folioIndex);
|
||||||
|
for (int i = 0 ; i < list.count() ; ++i)
|
||||||
|
{
|
||||||
|
IndependentTextItem *t = list.at(i);
|
||||||
|
const QPointF at = t->sceneBoundingRect().topLeft();
|
||||||
|
out << QStringLiteral("%1: '%2' at (%3, %4)")
|
||||||
|
.arg(i)
|
||||||
|
.arg(t->toPlainText())
|
||||||
|
.arg(at.x())
|
||||||
|
.arg(at.y());
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QetScriptApi::addText
|
||||||
|
Place a free-standing text, as the "add text" tool does.
|
||||||
|
@return its index in texts(), or -1
|
||||||
|
*/
|
||||||
|
int QetScriptApi::addText(int folioIndex, const QString &text, double x, double y)
|
||||||
|
{
|
||||||
|
if (!m_project) return -1;
|
||||||
|
if (m_project->isReadOnly()) {
|
||||||
|
log(QStringLiteral("qet.addText: project is read-only"));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const QList<Diagram *> diagrams = m_project->diagrams();
|
||||||
|
if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1;
|
||||||
|
Diagram *diagram = diagrams.at(folioIndex);
|
||||||
|
|
||||||
|
auto *item = new IndependentTextItem();
|
||||||
|
item->setPlainText(text);
|
||||||
|
diagram->undoStack().push(new AddGraphicsObjectCommand(item, diagram, QPointF(x, y)));
|
||||||
|
return sortedTexts(folioIndex).indexOf(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QetScriptApi::setTextContent(int folioIndex, int textIndex, const QString &text)
|
||||||
|
{
|
||||||
|
if (!m_project) return false;
|
||||||
|
if (m_project->isReadOnly()) {
|
||||||
|
log(QStringLiteral("qet.setTextContent: project is read-only"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
IndependentTextItem *item = findText(folioIndex, textIndex, QStringLiteral("setTextContent"));
|
||||||
|
if (!item) return false;
|
||||||
|
if (item->toPlainText() == text) return true;
|
||||||
|
|
||||||
|
auto *cmd = new QPropertyUndoCommand(item, "plainText",
|
||||||
|
QVariant(item->toPlainText()), QVariant(text));
|
||||||
|
cmd->setText(QObject::tr("Modifier un texte"));
|
||||||
|
m_project->undoStack()->push(cmd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QetScriptApi::setTextColor(int folioIndex, int textIndex, const QString &color)
|
||||||
|
{
|
||||||
|
if (!m_project) return false;
|
||||||
|
if (m_project->isReadOnly()) {
|
||||||
|
log(QStringLiteral("qet.setTextColor: project is read-only"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QColor new_color(color);
|
||||||
|
if (!new_color.isValid()) {
|
||||||
|
log(QStringLiteral("qet.setTextColor: '%1' is not a valid colour").arg(color));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
IndependentTextItem *item = findText(folioIndex, textIndex, QStringLiteral("setTextColor"));
|
||||||
|
if (!item) return false;
|
||||||
|
if (item->color() == new_color) return true;
|
||||||
|
|
||||||
|
auto *cmd = new QPropertyUndoCommand(item, "color",
|
||||||
|
QVariant(item->color()), QVariant(new_color));
|
||||||
|
cmd->setText(QObject::tr("Modifier la couleur d'un texte"));
|
||||||
|
m_project->undoStack()->push(cmd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QetScriptApi::setTextRotation(int folioIndex, int textIndex, double angle)
|
||||||
|
{
|
||||||
|
if (!m_project) return false;
|
||||||
|
if (m_project->isReadOnly()) {
|
||||||
|
log(QStringLiteral("qet.setTextRotation: project is read-only"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
IndependentTextItem *item = findText(folioIndex, textIndex, QStringLiteral("setTextRotation"));
|
||||||
|
if (!item) return false;
|
||||||
|
|
||||||
|
auto *cmd = new QPropertyUndoCommand(item, "rotation",
|
||||||
|
QVariant(item->rotation()),
|
||||||
|
QVariant(item->rotation() + angle));
|
||||||
|
cmd->setText(QObject::tr("Pivoter un texte"));
|
||||||
|
m_project->undoStack()->push(cmd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QetScriptApi::deleteText(int folioIndex, int textIndex)
|
||||||
|
{
|
||||||
|
if (!m_project) return false;
|
||||||
|
if (m_project->isReadOnly()) {
|
||||||
|
log(QStringLiteral("qet.deleteText: project is read-only"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
IndependentTextItem *item = findText(folioIndex, textIndex, QStringLiteral("deleteText"));
|
||||||
|
if (!item) return false;
|
||||||
|
Diagram *diagram = m_project->diagrams().at(folioIndex);
|
||||||
|
|
||||||
|
DiagramContent content;
|
||||||
|
content.m_text_fields << item;
|
||||||
|
diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, content));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList QetScriptApi::shapes(int folioIndex) const
|
||||||
|
{
|
||||||
|
QStringList out;
|
||||||
|
const QList<QetShapeItem *> list = sortedShapes(folioIndex);
|
||||||
|
for (int i = 0 ; i < list.count() ; ++i)
|
||||||
|
{
|
||||||
|
QetShapeItem *shape = list.at(i);
|
||||||
|
const QRectF r = shape->sceneBoundingRect();
|
||||||
|
out << QStringLiteral("%1: %2 (%3, %4) to (%5, %6)")
|
||||||
|
.arg(i)
|
||||||
|
.arg(shape->name())
|
||||||
|
.arg(r.left()).arg(r.top()).arg(r.right()).arg(r.bottom());
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QetScriptApi::addShape
|
||||||
|
Draw a line, rectangle, ellipse or polygon, as the shape tools do.
|
||||||
|
Path is deliberately absent: it is built by successive clicks and has
|
||||||
|
no two-point form to give here.
|
||||||
|
@return the shape's index in shapes(), or -1
|
||||||
|
*/
|
||||||
|
int QetScriptApi::addShape(int folioIndex, const QString &type,
|
||||||
|
double x1, double y1, double x2, double y2)
|
||||||
|
{
|
||||||
|
if (!m_project) return -1;
|
||||||
|
if (m_project->isReadOnly()) {
|
||||||
|
log(QStringLiteral("qet.addShape: project is read-only"));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const QList<Diagram *> diagrams = m_project->diagrams();
|
||||||
|
if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1;
|
||||||
|
|
||||||
|
QetShapeItem::ShapeType shape_type;
|
||||||
|
const QString t = type.toLower();
|
||||||
|
if (t == QLatin1String("line")) shape_type = QetShapeItem::Line;
|
||||||
|
else if (t == QLatin1String("rectangle")) shape_type = QetShapeItem::Rectangle;
|
||||||
|
else if (t == QLatin1String("ellipse")) shape_type = QetShapeItem::Ellipse;
|
||||||
|
else if (t == QLatin1String("polygon")) shape_type = QetShapeItem::Polygon;
|
||||||
|
else {
|
||||||
|
log(QStringLiteral("qet.addShape: unknown shape '%1'; expected line, "
|
||||||
|
"rectangle, ellipse or polygon").arg(type));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Diagram *diagram = diagrams.at(folioIndex);
|
||||||
|
auto *shape = new QetShapeItem(QPointF(x1, y1), QPointF(x2, y2), shape_type);
|
||||||
|
diagram->undoStack().push(new AddGraphicsObjectCommand(shape, diagram, QPointF(0, 0)));
|
||||||
|
return sortedShapes(folioIndex).indexOf(shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QetScriptApi::deleteShape(int folioIndex, int shapeIndex)
|
||||||
|
{
|
||||||
|
if (!m_project) return false;
|
||||||
|
if (m_project->isReadOnly()) {
|
||||||
|
log(QStringLiteral("qet.deleteShape: project is read-only"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QList<QetShapeItem *> list = sortedShapes(folioIndex);
|
||||||
|
if (shapeIndex < 0 || shapeIndex >= list.count()) {
|
||||||
|
log(QStringLiteral("qet.deleteShape: folio %1 has %2 shape(s), no index %3")
|
||||||
|
.arg(folioIndex).arg(list.count()).arg(shapeIndex));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Diagram *diagram = m_project->diagrams().at(folioIndex);
|
||||||
|
|
||||||
|
DiagramContent content;
|
||||||
|
content.m_shapes << list.at(shapeIndex);
|
||||||
|
diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, content));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QetScriptApi::tables
|
||||||
|
The tables and views the project database holds, as "name (type)".
|
||||||
|
|
||||||
|
Worth reading before writing a query against them: the three *_view
|
||||||
|
entries are the queryable surface and are named for it; the tables are
|
||||||
|
how the cache is arranged today.
|
||||||
|
*/
|
||||||
|
QStringList QetScriptApi::tables() const
|
||||||
|
{
|
||||||
|
QStringList list;
|
||||||
|
if (!m_project || !m_project->dataBase()) return list;
|
||||||
|
|
||||||
|
QSqlQuery q = m_project->dataBase()->newQuery(QStringLiteral(
|
||||||
|
"SELECT name, type FROM sqlite_master WHERE type IN ('table','view') "
|
||||||
|
"ORDER BY type, name"));
|
||||||
|
while (q.next()) {
|
||||||
|
list << QStringLiteral("%1 (%2)").arg(q.value(0).toString(), q.value(1).toString());
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QetScriptApi::query
|
||||||
|
Run a read-only SELECT against the project database and return its rows
|
||||||
|
as objects, one property per column.
|
||||||
|
|
||||||
|
Goes through projectDataBase::newQuery(), which applies
|
||||||
|
isReadOnlySelect() itself -- the same rule, and the same rejection
|
||||||
|
message, that the "Requête SQL personnalisée" box in the element-query
|
||||||
|
dialog shows a user. Nothing here can write: a statement that is not a
|
||||||
|
single SELECT or WITH...SELECT is refused before it reaches SQLite.
|
||||||
|
|
||||||
|
No updateDB() first, deliberately. A script that has just edited
|
||||||
|
something is the expected caller, so querying a stale cache was the
|
||||||
|
obvious hazard -- but projectDataBase maintains itself incrementally
|
||||||
|
through addElement()/elementInfoChanged()/addConductor() and the rest,
|
||||||
|
which the undo commands behind every edit here already call. Tested
|
||||||
|
both ways on the cases most likely to be stale: an element added and
|
||||||
|
labelled, and a conductor property changed, each queried immediately
|
||||||
|
afterwards through both the table and the view. The counts are the
|
||||||
|
same with the rebuild and without it. Since updateDB() is a full
|
||||||
|
repopulation of every table, calling it per query would have been a
|
||||||
|
real cost for no observable benefit -- so it is not called, and this
|
||||||
|
note exists so it is not added back on the assumption that it must be
|
||||||
|
needed.
|
||||||
|
|
||||||
|
@return the rows; empty on refusal or SQL error, with queryError()
|
||||||
|
saying which. An empty result and a failure are not the same thing.
|
||||||
|
*/
|
||||||
|
QVariantList QetScriptApi::query(const QString &sql)
|
||||||
|
{
|
||||||
|
m_query_error.clear();
|
||||||
|
QVariantList rows;
|
||||||
|
if (!m_project || !m_project->dataBase()) {
|
||||||
|
m_query_error = QStringLiteral("no project database");
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString rejection;
|
||||||
|
QSqlQuery q = m_project->dataBase()->newQuery(sql, &rejection);
|
||||||
|
if (!rejection.isEmpty()) {
|
||||||
|
m_query_error = rejection;
|
||||||
|
log(QStringLiteral("qet.query: %1").arg(rejection));
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
if (q.lastError().isValid()) {
|
||||||
|
m_query_error = q.lastError().text();
|
||||||
|
log(QStringLiteral("qet.query: %1").arg(m_query_error));
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QSqlRecord record = q.record();
|
||||||
|
while (q.next())
|
||||||
|
{
|
||||||
|
QVariantMap row;
|
||||||
|
for (int i = 0 ; i < record.count() ; ++i) {
|
||||||
|
row.insert(record.fieldName(i), q.value(i));
|
||||||
|
}
|
||||||
|
rows << row;
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString QetScriptApi::queryError() const
|
||||||
|
{
|
||||||
|
return m_query_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
int QetScriptApi::addFolio()
|
||||||
|
{
|
||||||
|
if (!m_project) return -1;
|
||||||
|
if (m_project->isReadOnly()) {
|
||||||
|
log(QStringLiteral("qet.addFolio: project is read-only"));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
Diagram *diagram = m_project->addNewDiagram();
|
||||||
|
if (!diagram) return -1;
|
||||||
|
return m_project->diagrams().indexOf(diagram);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QetScriptApi::setFolioTitle(int folioIndex, const QString &title)
|
||||||
|
{
|
||||||
|
if (!m_project) return false;
|
||||||
|
if (m_project->isReadOnly()) {
|
||||||
|
log(QStringLiteral("qet.setFolioTitle: project is read-only"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QList<Diagram *> diagrams = m_project->diagrams();
|
||||||
|
if (folioIndex < 0 || folioIndex >= diagrams.count()) return false;
|
||||||
|
Diagram *diagram = diagrams.at(folioIndex);
|
||||||
|
|
||||||
|
// The folio title is one field of the title block properties, so it
|
||||||
|
// changes the way the title block dialog changes it: read the whole
|
||||||
|
// struct, set one member, push the command with both versions.
|
||||||
|
const TitleBlockProperties old_properties = diagram->border_and_titleblock.exportTitleBlock();
|
||||||
|
if (old_properties.title == title) return true;
|
||||||
|
TitleBlockProperties new_properties = old_properties;
|
||||||
|
new_properties.title = title;
|
||||||
|
|
||||||
|
auto *cmd = new ChangeTitleBlockCommand(diagram, old_properties, new_properties);
|
||||||
|
m_project->undoStack()->push(cmd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool QetScriptApi::undo()
|
bool QetScriptApi::undo()
|
||||||
{
|
{
|
||||||
if (!m_project || !m_project->undoStack()->canUndo()) return false;
|
if (!m_project || !m_project->undoStack()->canUndo()) return false;
|
||||||
|
|||||||
@@ -21,10 +21,15 @@
|
|||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
#include <QVariantList>
|
||||||
|
|
||||||
class QETProject;
|
class QETProject;
|
||||||
class DiagramView;
|
class DiagramView;
|
||||||
class Element;
|
class Element;
|
||||||
|
class Terminal;
|
||||||
|
class Conductor;
|
||||||
|
class IndependentTextItem;
|
||||||
|
class QetShapeItem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief The QetScriptApi class
|
@brief The QetScriptApi class
|
||||||
@@ -65,12 +70,86 @@ class Element;
|
|||||||
QPropertyUndoCommand merges consecutive commands on the same
|
QPropertyUndoCommand merges consecutive commands on the same
|
||||||
object+property when their text() also matches
|
object+property when their text() also matches
|
||||||
(QPropertyUndoCommand::mergeWith(), pre-existing), and
|
(QPropertyUndoCommand::mergeWith(), pre-existing), and
|
||||||
setElementPosition()/moveElement() always use the same text for a
|
setElementPosition()/moveElement()/rotateElement() always use the
|
||||||
given element -- so several position changes to the same element in a
|
same text for a given element -- so several position changes, or
|
||||||
row collapse into one undo step, the same way dragging an element
|
several rotations, of the same element in a row collapse into one
|
||||||
does, not one step per call. Verified against exactly that: two
|
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
|
consecutive calls on one element, then undo/undo/redo/redo, land
|
||||||
where a merge predicts, not where two independent steps would.
|
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. 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 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.
|
||||||
|
|
||||||
|
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 Navigating and @b messaging: select an element, zoom the active
|
- @b Navigating and @b messaging: select an element, zoom the active
|
||||||
view, and show the user a message. Deliberately narrow: selection and
|
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
|
messaging work with no view at all (headless `--run`); zoom is a no-op
|
||||||
@@ -87,7 +166,11 @@ class Element;
|
|||||||
import-collision case that would otherwise reach
|
import-collision case that would otherwise reach
|
||||||
QETProject::importElement()'s own ImportElementDialog::exec() and
|
QETProject::importElement()'s own ImportElementDialog::exec() and
|
||||||
refuses instead, rather than let a plain QDialog (not routed through
|
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
|
class QetScriptApi : public QObject
|
||||||
{
|
{
|
||||||
@@ -128,7 +211,62 @@ class QetScriptApi : public QObject
|
|||||||
Q_INVOKABLE QString addElement(int folioIndex, const QString &locationPath, double x, double y);
|
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 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 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);
|
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);
|
||||||
|
|
||||||
|
// -- 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);
|
||||||
|
Q_INVOKABLE bool unlinkElement(int folioIndex, const QString &elementUuid);
|
||||||
|
|
||||||
|
// -- 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 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);
|
||||||
|
|
||||||
|
// -- query the project database --
|
||||||
|
Q_INVOKABLE QStringList tables() const;
|
||||||
|
Q_INVOKABLE QVariantList query(const QString &sql);
|
||||||
|
Q_INVOKABLE QString queryError() const;
|
||||||
|
|
||||||
|
// -- folios --
|
||||||
|
Q_INVOKABLE int addFolio();
|
||||||
|
Q_INVOKABLE bool setFolioTitle(int folioIndex, const QString &title);
|
||||||
|
|
||||||
Q_INVOKABLE bool undo();
|
Q_INVOKABLE bool undo();
|
||||||
Q_INVOKABLE bool redo();
|
Q_INVOKABLE bool redo();
|
||||||
Q_INVOKABLE bool canUndo() const;
|
Q_INVOKABLE bool canUndo() const;
|
||||||
@@ -148,9 +286,19 @@ class QetScriptApi : public QObject
|
|||||||
private:
|
private:
|
||||||
bool runFlag(const QString &flag, const QStringList &args);
|
bool runFlag(const QString &flag, const QStringList &args);
|
||||||
Element *findElement(int folioIndex, const QString &elementUuid) const;
|
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;
|
||||||
|
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;
|
QETProject *m_project;
|
||||||
DiagramView *m_view;
|
DiagramView *m_view;
|
||||||
|
QString m_query_error;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // QET_SCRIPT_API_H
|
#endif // QET_SCRIPT_API_H
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include "NameList/nameslist.h"
|
#include "NameList/nameslist.h"
|
||||||
#include "createdxf.h"
|
#include "createdxf.h"
|
||||||
|
#include "diagram.h"
|
||||||
#include "qet.h"
|
#include "qet.h"
|
||||||
#include "qetapp.h"
|
#include "qetapp.h"
|
||||||
// uncomment the line below to get more debug information
|
// uncomment the line below to get more debug information
|
||||||
@@ -1589,8 +1590,10 @@ void TitleBlockTemplate::render(QPainter &painter,
|
|||||||
int titleblock_height = height();
|
int titleblock_height = height();
|
||||||
|
|
||||||
painter.save();
|
painter.save();
|
||||||
//Setup the QPainter
|
//Setup the QPainter - use a color that contrasts with the background
|
||||||
QPen pen(Qt::black);
|
QColor ink = Diagram::background_color.lightness() < 128
|
||||||
|
? QColor(Qt::white) : QColor(Qt::black);
|
||||||
|
QPen pen(ink);
|
||||||
painter.setPen(pen);
|
painter.setPen(pen);
|
||||||
|
|
||||||
// draw the titleblock border
|
// draw the titleblock border
|
||||||
@@ -1737,7 +1740,9 @@ void TitleBlockTemplate::renderCell(QPainter &painter,
|
|||||||
{
|
{
|
||||||
// draw the border rect of the current cell
|
// draw the border rect of the current cell
|
||||||
QPen pen(QBrush(), 1, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin);
|
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.setPen(pen);
|
||||||
painter.drawRect(cell_rect);
|
painter.drawRect(cell_rect);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
/*
|
||||||
|
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 "conductorcolortoolbutton.h"
|
||||||
|
|
||||||
|
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
|
||||||
|
#include "../diagram.h"
|
||||||
|
#include "../diagramcontent.h"
|
||||||
|
#include "../diagramview.h"
|
||||||
|
#include "../lastusedstyle.h"
|
||||||
|
#include "../qetdiagrameditor.h"
|
||||||
|
#include "../projectview.h"
|
||||||
|
#include "../qetgraphicsitem/conductor.h"
|
||||||
|
#include "../conductorproperties.h"
|
||||||
|
|
||||||
|
#include <QColorDialog>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPixmap>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
/**
|
||||||
|
The colours an electrician reaches for, in the order the trade
|
||||||
|
names them: the three phases, neutral, earth, then the ones used
|
||||||
|
for control and extra-low-voltage circuits. Not a standard in
|
||||||
|
itself -- IEC 60445 only fixes blue for neutral and green/yellow
|
||||||
|
for protective earth -- but it covers the wiring a schematic
|
||||||
|
actually shows, which is what makes the toolbar worth a click.
|
||||||
|
*/
|
||||||
|
struct NamedColor { const char *context_name; QColor color; };
|
||||||
|
|
||||||
|
QList<NamedColor> standardColors()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Noir"), QColor(0x00, 0x00, 0x00)},
|
||||||
|
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Marron"), QColor(0x7B, 0x3F, 0x00)},
|
||||||
|
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Gris"), QColor(0x80, 0x80, 0x80)},
|
||||||
|
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Bleu"), QColor(0x00, 0x00, 0xFF)},
|
||||||
|
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Vert"), QColor(0x00, 0x80, 0x00)},
|
||||||
|
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Rouge"), QColor(0xFF, 0x00, 0x00)},
|
||||||
|
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Orange"), QColor(0xFF, 0x80, 0x00)},
|
||||||
|
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Violet"), QColor(0x80, 0x00, 0x80)},
|
||||||
|
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Blanc"), QColor(0xFF, 0xFF, 0xFF)},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const int MAX_RECENT = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief ConductorColorToolButton::ConductorColorToolButton
|
||||||
|
@param editor : the diagram editor this button acts on
|
||||||
|
@param parent
|
||||||
|
*/
|
||||||
|
ConductorColorToolButton::ConductorColorToolButton(QETDiagramEditor *editor, QWidget *parent) :
|
||||||
|
QToolButton(parent),
|
||||||
|
m_editor(editor)
|
||||||
|
{
|
||||||
|
setPopupMode(QToolButton::InstantPopup);
|
||||||
|
setToolTip(tr("Couleur de conducteur"));
|
||||||
|
setStatusTip(tr("Applique une couleur aux conducteurs sélectionnés, et l'utilise pour les prochains conducteurs tracés",
|
||||||
|
"status bar tip"));
|
||||||
|
|
||||||
|
m_current = LastUsedStyle::hasConductorColor() ? LastUsedStyle::conductorColor()
|
||||||
|
: QColor(Qt::black);
|
||||||
|
setMenu(new QMenu(this));
|
||||||
|
rebuildMenu();
|
||||||
|
setSwatch(m_current);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief ConductorColorToolButton::currentView
|
||||||
|
@return the folio being edited, or nullptr when no project is open.
|
||||||
|
Goes through the project view because QETDiagramEditor keeps its own
|
||||||
|
currentDiagramView() private.
|
||||||
|
*/
|
||||||
|
DiagramView *ConductorColorToolButton::currentView() const
|
||||||
|
{
|
||||||
|
ProjectView *pv = m_editor ? m_editor->currentProjectView() : nullptr;
|
||||||
|
return pv ? pv->currentDiagram() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief ConductorColorToolButton::updateEnabledState
|
||||||
|
Greyed out when there is no folio to act on, or when the project is
|
||||||
|
read-only -- the same rule the other conductor actions follow.
|
||||||
|
*/
|
||||||
|
void ConductorColorToolButton::updateEnabledState()
|
||||||
|
{
|
||||||
|
DiagramView *dv = currentView();
|
||||||
|
setEnabled(dv && dv->diagram() && !dv->diagram()->isReadOnly());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief ConductorColorToolButton::rebuildMenu
|
||||||
|
The standard colours never change; the recent ones do, so the menu is
|
||||||
|
rebuilt rather than kept in sync entry by entry.
|
||||||
|
*/
|
||||||
|
void ConductorColorToolButton::rebuildMenu()
|
||||||
|
{
|
||||||
|
QMenu *m = menu();
|
||||||
|
m->clear();
|
||||||
|
|
||||||
|
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, &ConductorColorToolButton::chooseOtherColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief ConductorColorToolButton::applyColor
|
||||||
|
Recolour the selected conductors, and remember the colour for the next
|
||||||
|
one drawn.
|
||||||
|
@param color
|
||||||
|
*/
|
||||||
|
void ConductorColorToolButton::applyColor(const QColor &color)
|
||||||
|
{
|
||||||
|
if (!color.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Always: this is the half that works with nothing selected.
|
||||||
|
LastUsedStyle::setConductorColor(color);
|
||||||
|
rememberRecent(color);
|
||||||
|
setSwatch(color);
|
||||||
|
|
||||||
|
DiagramView *dv = currentView();
|
||||||
|
if (!dv) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Diagram *diagram = dv->diagram();
|
||||||
|
if (!diagram || diagram->isReadOnly()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DiagramContent dc(diagram);
|
||||||
|
const auto conductors = dc.conductors(DiagramContent::AnyConductor);
|
||||||
|
if (conductors.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QUndoCommand *undo = new QUndoCommand(tr("Modifier la couleur de %n conducteur(s)",
|
||||||
|
"undo caption", conductors.count()));
|
||||||
|
int changed = 0;
|
||||||
|
for (Conductor *conductor : conductors)
|
||||||
|
{
|
||||||
|
ConductorProperties before = conductor->properties();
|
||||||
|
if (before.color == color) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ConductorProperties after = before;
|
||||||
|
after.color = color;
|
||||||
|
|
||||||
|
QVariant old_value, new_value;
|
||||||
|
old_value.setValue(before);
|
||||||
|
new_value.setValue(after);
|
||||||
|
new QPropertyUndoCommand(conductor, "properties", old_value, new_value, undo);
|
||||||
|
++changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Every selected conductor was already this colour: pushing an
|
||||||
|
//empty command would put a no-op step in the undo stack.
|
||||||
|
if (changed) {
|
||||||
|
diagram->undoStack().push(undo);
|
||||||
|
} else {
|
||||||
|
delete undo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief ConductorColorToolButton::chooseOtherColor
|
||||||
|
*/
|
||||||
|
void ConductorColorToolButton::chooseOtherColor()
|
||||||
|
{
|
||||||
|
const QColor c = QColorDialog::getColor(m_current, this,
|
||||||
|
tr("Choisir une couleur de conducteur"));
|
||||||
|
if (c.isValid()) {
|
||||||
|
applyColor(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief ConductorColorToolButton::rememberRecent
|
||||||
|
Most recent first, no duplicates, capped.
|
||||||
|
@param color
|
||||||
|
*/
|
||||||
|
void ConductorColorToolButton::rememberRecent(const QColor &color)
|
||||||
|
{
|
||||||
|
//A colour that is already one row up in the standard list would
|
||||||
|
//only appear twice, under a hex name it does not need.
|
||||||
|
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 ConductorColorToolButton::setSwatch
|
||||||
|
@param color
|
||||||
|
*/
|
||||||
|
void ConductorColorToolButton::setSwatch(const QColor &color)
|
||||||
|
{
|
||||||
|
m_current = color;
|
||||||
|
setIcon(swatchIcon(color));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief ConductorColorToolButton::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 ConductorColorToolButton::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,69 @@
|
|||||||
|
/*
|
||||||
|
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 CONDUCTORCOLORTOOLBUTTON_H
|
||||||
|
#define CONDUCTORCOLORTOOLBUTTON_H
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QList>
|
||||||
|
#include <QToolButton>
|
||||||
|
|
||||||
|
class QETDiagramEditor;
|
||||||
|
class DiagramView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief The ConductorColorToolButton class
|
||||||
|
One-click conductor colour, from the "Schéma" toolbar.
|
||||||
|
|
||||||
|
Picking a colour does two things: it recolours every conductor
|
||||||
|
currently selected (one undo command for the lot), and it becomes the
|
||||||
|
colour of the next conductor drawn this session, through the existing
|
||||||
|
LastUsedStyle mechanism. Either half is useful on its own -- with
|
||||||
|
nothing selected it only sets the pen for what comes next.
|
||||||
|
|
||||||
|
This deliberately stores nothing in the project and nothing in
|
||||||
|
QSettings. It is the same session-scoped "what did I just use" idea
|
||||||
|
LastUsedStyle already implements; named presets that persist per
|
||||||
|
project are a separate, larger feature (upstream issue #461) that
|
||||||
|
needs a maintainer decision first.
|
||||||
|
*/
|
||||||
|
class ConductorColorToolButton : public QToolButton
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ConductorColorToolButton(QETDiagramEditor *editor,
|
||||||
|
QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void updateEnabledState();
|
||||||
|
|
||||||
|
private:
|
||||||
|
DiagramView *currentView() const;
|
||||||
|
void rebuildMenu();
|
||||||
|
void applyColor(const QColor &color);
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // CONDUCTORCOLORTOOLBUTTON_H
|
||||||
@@ -31,6 +31,8 @@
|
|||||||
#include "../titleblockpropertieswidget.h"
|
#include "../titleblockpropertieswidget.h"
|
||||||
#include "../xrefpropertieswidget.h"
|
#include "../xrefpropertieswidget.h"
|
||||||
#include "guidespropertieswidget.h"
|
#include "guidespropertieswidget.h"
|
||||||
|
#include "../autoNum/numerotationcontext.h"
|
||||||
|
#include "../autoNum/ui/selectautonumw.h"
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
#include <QFontDialog>
|
#include <QFontDialog>
|
||||||
#include <QSizePolicy>
|
#include <QSizePolicy>
|
||||||
@@ -101,6 +103,32 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
|
|||||||
}
|
}
|
||||||
m_gpw->setGuides(loaded_guides);
|
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 there is a project, we edit his properties
|
||||||
if (m_project) {
|
if (m_project) {
|
||||||
bpw -> setProperties (m_project -> defaultBorderProperties());
|
bpw -> setProperties (m_project -> defaultBorderProperties());
|
||||||
@@ -114,6 +142,7 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
|
|||||||
|
|
||||||
// main tab widget
|
// main tab widget
|
||||||
QTabWidget *tab_widget = new QTabWidget(this);
|
QTabWidget *tab_widget = new QTabWidget(this);
|
||||||
|
m_tab_widget = tab_widget;
|
||||||
QWidget *diagram_widget = new QWidget();
|
QWidget *diagram_widget = new QWidget();
|
||||||
QVBoxLayout *diagram_layout = new QVBoxLayout(diagram_widget);
|
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 (xrefpw, tr("Références croisées"));
|
||||||
tab_widget -> addTab (m_gpw, tr("Guides"));
|
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();
|
QVBoxLayout *vlayout1 = new QVBoxLayout();
|
||||||
vlayout1->addWidget(tab_widget);
|
vlayout1->addWidget(tab_widget);
|
||||||
|
|
||||||
@@ -230,6 +272,9 @@ void NewDiagramPage::applyConf()
|
|||||||
settings.setValue(QStringLiteral("color"), current_guides[i].color.name());
|
settings.setValue(QStringLiteral("color"), current_guides[i].color.name());
|
||||||
}
|
}
|
||||||
settings.endArray();
|
settings.endArray();
|
||||||
|
|
||||||
|
// save global auto-numbering defaults
|
||||||
|
persistAutonumSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,6 +340,134 @@ void NewDiagramPage::loadSavedTbp()
|
|||||||
applyConf();
|
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
|
Constructeur
|
||||||
@param parent QWidget parent
|
@param parent QWidget parent
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include "configpage.h"
|
#include "configpage.h"
|
||||||
#include "../projectpropertiesdialog.h"
|
#include "../projectpropertiesdialog.h"
|
||||||
#include "../titleblockpropertieswidget.h"
|
#include "../titleblockpropertieswidget.h"
|
||||||
|
#include "../autoNum/numerotationcontext.h"
|
||||||
|
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QtWidgets>
|
#include <QtWidgets>
|
||||||
@@ -32,6 +33,7 @@ class XRefPropertiesWidget;
|
|||||||
class GuidesPropertiesWidget;
|
class GuidesPropertiesWidget;
|
||||||
class QETProject;
|
class QETProject;
|
||||||
class TitleBlockProperties;
|
class TitleBlockProperties;
|
||||||
|
class SelectAutonumW;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief The NewDiagramPage class
|
@brief The NewDiagramPage class
|
||||||
@@ -48,6 +50,7 @@ class NewDiagramPage : public ConfigPage {
|
|||||||
~NewDiagramPage() override;
|
~NewDiagramPage() override;
|
||||||
private:
|
private:
|
||||||
NewDiagramPage(const NewDiagramPage &);
|
NewDiagramPage(const NewDiagramPage &);
|
||||||
|
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||||
public slots:
|
public slots:
|
||||||
void changeToAutoFolioTab();
|
void changeToAutoFolioTab();
|
||||||
void setFolioAutonum(QString);
|
void setFolioAutonum(QString);
|
||||||
@@ -72,7 +75,25 @@ public slots:
|
|||||||
XRefPropertiesWidget *xrefpw; ///< Widget to edit default xref properties
|
XRefPropertiesWidget *xrefpw; ///< Widget to edit default xref properties
|
||||||
GuidesPropertiesWidget *m_gpw; ///< Widget to edit guides
|
GuidesPropertiesWidget *m_gpw; ///< Widget to edit guides
|
||||||
TitleBlockProperties savedTbp; ///< Used to save current TBP and retrieve later
|
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);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -381,5 +381,5 @@ QString ShortcutsConfigPage::title() const
|
|||||||
|
|
||||||
QIcon ShortcutsConfigPage::icon() 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
|
||||||
@@ -55,20 +55,12 @@ m_diagram(diagram)
|
|||||||
{
|
{
|
||||||
openDialog();
|
openDialog();
|
||||||
|
|
||||||
QString text;
|
QStringList parts;
|
||||||
if(texts_list.count())
|
if (texts_list.count())
|
||||||
text.append(QObject::tr("Pivoter %1 textes").arg(texts_list.count()));
|
parts << QObject::tr("%n texte(s)", "", texts_list.count());
|
||||||
if(groups_list.count())
|
if (groups_list.count())
|
||||||
{
|
parts << QObject::tr("%n groupe(s) de textes", "", groups_list.count());
|
||||||
if(text.isEmpty())
|
setText(QObject::tr("Pivoter %1").arg(QLocale().createSeparatedList(parts)));
|
||||||
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);
|
|
||||||
|
|
||||||
for(DiagramTextItem *dti : texts_list)
|
for(DiagramTextItem *dti : texts_list)
|
||||||
setupAnimation(dti, "rotation", dti->rotation(), m_rotation);
|
setupAnimation(dti, "rotation", dti->rotation(), m_rotation);
|
||||||
@@ -77,7 +69,6 @@ m_diagram(diagram)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
setObsolete(true);
|
setObsolete(true);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RotateTextsCommand::undo()
|
void RotateTextsCommand::undo()
|
||||||
|
|||||||
@@ -95,6 +95,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
|
@brief ConductorCreator::propertieToUse
|
||||||
@return true if the caller should proceed with conductor creation,
|
@return true if the caller should proceed with conductor creation,
|
||||||
@@ -104,7 +127,7 @@ void ConductorCreator::create(Diagram *d, const QPolygonF &polygon)
|
|||||||
*/
|
*/
|
||||||
bool ConductorCreator::setUpPropertieToUse()
|
bool ConductorCreator::setUpPropertieToUse()
|
||||||
{
|
{
|
||||||
QList<Conductor *> potentials = existingPotential();
|
QList<Conductor *> potentials = existingPotential(m_terminals_list);
|
||||||
|
|
||||||
//There is an existing potential
|
//There is an existing potential
|
||||||
//we get one of them
|
//we get one of them
|
||||||
@@ -145,14 +168,15 @@ bool ConductorCreator::setUpPropertieToUse()
|
|||||||
@brief ConductorCreator::existingPotential
|
@brief ConductorCreator::existingPotential
|
||||||
Return the list of existing potential of
|
Return the list of existing potential of
|
||||||
the terminal list
|
the terminal list
|
||||||
|
@param terminals_list the terminals to inspect
|
||||||
@return c_list QList<Conductor *>
|
@return c_list QList<Conductor *>
|
||||||
*/
|
*/
|
||||||
QList<Conductor *> ConductorCreator::existingPotential()
|
QList<Conductor *> ConductorCreator::existingPotential(const QList<Terminal *> &terminals_list)
|
||||||
{
|
{
|
||||||
QList<Conductor *> c_list;
|
QList<Conductor *> c_list;
|
||||||
QList<Terminal *> t_exclude;
|
QList<Terminal *> t_exclude;
|
||||||
|
|
||||||
for (Terminal *t : m_terminals_list)
|
for (Terminal *t : terminals_list)
|
||||||
{
|
{
|
||||||
if (t_exclude.contains(t)) {
|
if (t_exclude.contains(t)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -166,9 +190,9 @@ QList<Conductor *> ConductorCreator::existingPotential()
|
|||||||
//in the same potential of c, and if true, exclude this terminal from the search.
|
//in the same potential of c, and if true, exclude this terminal from the search.
|
||||||
for (Conductor *c : t->conductors().first()->relatedPotentialConductors(false))
|
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);
|
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);
|
t_exclude.append(c->terminal2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||