Compare commits
61 Commits
2da516667e
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 70599c4ae1 | |||
| e42ebdc861 | |||
| 3c1fb2fe72 | |||
| 95d0e523fe |
@@ -8,3 +8,11 @@ doc/*
|
||||
QElectroTech.tag
|
||||
!doc/doc-utils
|
||||
lang/*.qm
|
||||
|
||||
# -- IDE settings/intermediate files --
|
||||
# zed
|
||||
.zed
|
||||
.cache
|
||||
|
||||
# VS Code
|
||||
.vscode
|
||||
|
||||
@@ -113,7 +113,9 @@ if(Backtrace_FOUND)
|
||||
include_directories(${Backtrace_INCLUDE_DIRS})
|
||||
add_compile_definitions(QET_CRASH_BACKTRACE)
|
||||
else()
|
||||
message(STATUS "backtrace() not available: crash dumps will carry the log ring without a backtrace")
|
||||
message(STATUS
|
||||
"backtrace() not available: crash dumps will carry the log ring "
|
||||
"without a backtrace")
|
||||
endif()
|
||||
|
||||
find_package(SQLite3 REQUIRED)
|
||||
|
||||
@@ -271,6 +271,22 @@ mkdir build && cd build
|
||||
cmake .. -G Ninja -DBUILD_WITH_KF=OFF -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build .
|
||||
```
|
||||
|
||||
If `libbacktrace` is installed in your MSYS2 environment, pass the following
|
||||
cached CMake variables when configuring. They are only necessary in that
|
||||
case, because `FindBacktrace` needs them to locate the library:
|
||||
|
||||
```sh
|
||||
cmake .. -G Ninja -DBUILD_WITH_KF=OFF -DCMAKE_BUILD_TYPE=Release \
|
||||
-DBacktrace_INCLUDE_DIR=/c/msys64/clang64/include \
|
||||
-DBacktrace_LIBRARY=/c/msys64/clang64/lib/libbacktrace.a
|
||||
```
|
||||
|
||||
`Backtrace_INCLUDE_DIR` must point to the directory containing `backtrace.h`,
|
||||
and `Backtrace_LIBRARY` to the `libbacktrace.a` file. The paths above are the
|
||||
usual locations for the MSYS2 `clang64` environment; adjust them if your
|
||||
installation uses a different prefix.
|
||||
|
||||
(KF6 isn't packaged in MSYS2 either, hence `-DBUILD_WITH_KF=OFF` again.)
|
||||
|
||||
Using the Qt Online Installer's bundled MinGW kit instead: point
|
||||
|
||||
@@ -23,8 +23,31 @@ if(BUILD_WITH_KF)
|
||||
|
||||
if(BUILD_KF)
|
||||
|
||||
if(NOT DEFINED KF_GIT_TAG)
|
||||
# this is a more or less random version, taken as an conservative approach
|
||||
# v6.10.0 is a more or less random version, taken as an conservative
|
||||
# approach. Pinned to the commits v6.10.0 points at, not to the tags
|
||||
# themselves; see the note in fetch_pugixml.cmake. Each module lives in its
|
||||
# own repository, so the same v6.10.0 release is a different commit in
|
||||
# each.
|
||||
#
|
||||
# KDE uses annotated tags, so "git ls-remote <repo> 'refs/tags/v6.10.0*'"
|
||||
# prints two hashes per module: refs/tags/v6.10.0 is the tag object (the
|
||||
# tagger, the date and the tag message) and refs/tags/v6.10.0^{} is the
|
||||
# commit that object points at. The hashes below are the "^{}" ones, i.e.
|
||||
# the commits. Lightweight tags, such as pugixml's v1.15 and
|
||||
# SingleApplication's v3.2.0, have no tag object and print only the
|
||||
# commit line.
|
||||
set(KF_ECM_GIT_COMMIT 7dd28cc56c339c3f8fb356f7c53c0e8f61433d81) # v6.10.0
|
||||
set(KF_KCOREADDONS_GIT_COMMIT c569f974dab24b4784ad186a3db4b76b2fa36612) # v6.10.0
|
||||
set(KF_KWIDGETSADDONS_GIT_COMMIT 1abbed8a280d6626c59fb197f2c4667d2b1e7445) # v6.10.0
|
||||
|
||||
if(DEFINED KF_GIT_TAG)
|
||||
# Explicit override: -DKF_GIT_TAG=<ref> selects one ref for all three
|
||||
# modules, unpinned, exactly as it did before.
|
||||
set(KF_ECM_GIT_COMMIT ${KF_GIT_TAG})
|
||||
set(KF_KCOREADDONS_GIT_COMMIT ${KF_GIT_TAG})
|
||||
set(KF_KWIDGETSADDONS_GIT_COMMIT ${KF_GIT_TAG})
|
||||
else()
|
||||
# Keep KF_GIT_TAG defined: define_definitions.cmake reports it.
|
||||
set(KF_GIT_TAG v6.10.0)
|
||||
endif()
|
||||
# using a function in order to limit the scope of the variables
|
||||
@@ -53,19 +76,19 @@ if(BUILD_WITH_KF)
|
||||
FetchContent_Declare(
|
||||
ecm
|
||||
GIT_REPOSITORY https://invent.kde.org/frameworks/extra-cmake-modules.git
|
||||
GIT_TAG ${KF_GIT_TAG})
|
||||
GIT_TAG ${KF_ECM_GIT_COMMIT})
|
||||
FetchContent_MakeAvailable(ecm)
|
||||
|
||||
FetchContent_Declare(
|
||||
kcoreaddons
|
||||
GIT_REPOSITORY https://invent.kde.org/frameworks/kcoreaddons.git
|
||||
GIT_TAG ${KF_GIT_TAG})
|
||||
GIT_TAG ${KF_KCOREADDONS_GIT_COMMIT})
|
||||
FetchContent_MakeAvailable(kcoreaddons)
|
||||
|
||||
FetchContent_Declare(
|
||||
kwidgetsaddons
|
||||
GIT_REPOSITORY https://invent.kde.org/frameworks/kwidgetsaddons.git
|
||||
GIT_TAG ${KF_GIT_TAG})
|
||||
GIT_TAG ${KF_KWIDGETSADDONS_GIT_COMMIT})
|
||||
FetchContent_MakeAvailable(kwidgetsaddons)
|
||||
endfunction()
|
||||
qet_make_kf_available()
|
||||
|
||||
@@ -22,10 +22,51 @@ option(BUILD_PUGIXML "Build pugixml library, use system one otherwise" YES)
|
||||
|
||||
if(BUILD_PUGIXML)
|
||||
|
||||
# Pinned to the commit v1.15 points at, not to the tag itself.
|
||||
#
|
||||
# A git tag is only a named pointer to a commit, and anyone with push access
|
||||
# to the upstream repository can move it (git push --force) to any other
|
||||
# commit. FetchContent fetches whatever the tag points at when the build
|
||||
# runs, so if a maintainer account or CI token is compromised, the attacker
|
||||
# can retarget a well-known release tag to malicious code: every fresh build
|
||||
# of QElectroTech then compiles it, while nothing changes in this repository
|
||||
# and the tag name still looks correct. A commit hash cannot be moved, because
|
||||
# it is derived from the content: different code always has a different hash.
|
||||
#
|
||||
# This attack has been used in the wild:
|
||||
# - March 2025, tj-actions/changed-files (CVE-2025-30066): tags v1 through
|
||||
# v45.0.7 were retargeted to a commit that dumped CI secrets into build
|
||||
# logs, affecting more than 23,000 repositories.
|
||||
# - March 2026, aquasecurity/trivy-action (CVE-2026-33634): 76 of 77
|
||||
# version tags were force-pushed to a credential stealer and stayed
|
||||
# malicious for about 12 hours.
|
||||
# Both were GitHub Actions rather than CMake dependencies, but the mechanism
|
||||
# is the same one FetchContent relies on here: resolving a git tag at build
|
||||
# time.
|
||||
#
|
||||
# To upgrade, look up the commit the new tag points at with
|
||||
# git ls-remote <repo> 'refs/tags/<tag>*', check that it is the release you
|
||||
# expect, and update both the hash and the trailing tag comment.
|
||||
#
|
||||
# How many lines that prints depends on which of the two kinds of tag
|
||||
# upstream created:
|
||||
# - A lightweight tag is nothing but a ref pointing straight at the commit,
|
||||
# so ls-remote prints a single line, "refs/tags/<tag>", and its hash is
|
||||
# the commit to pin. pugixml tags this way, which is why the v1.15 hash
|
||||
# below is what "git ls-remote ... refs/tags/v1.15" reports directly;
|
||||
# SingleApplication (v3.2.0) does the same.
|
||||
# - An annotated tag is a git object in its own right, carrying a tagger,
|
||||
# a date, a message and optionally a GPG signature, and pointing at the
|
||||
# commit. ls-remote then prints two lines: "refs/tags/<tag>" is the tag
|
||||
# object and "refs/tags/<tag>^{}" is that object dereferenced, i.e. the
|
||||
# commit. The KDE Frameworks modules tag this way, so for them it is the
|
||||
# "^{}" hash that belongs in the pin; the other hash identifies the tag
|
||||
# object itself, which is not the source revision and changes whenever
|
||||
# upstream re-creates the tag, even over the very same commit.
|
||||
FetchContent_Declare(
|
||||
pugixml
|
||||
GIT_REPOSITORY https://github.com/zeux/pugixml.git
|
||||
GIT_TAG v1.15)
|
||||
GIT_TAG ee86beb30e4973f5feffe3ce63bfa4fbadf72f38) # v1.15
|
||||
set(PUGIXML_INSTALL OFF CACHE INTERNAL "")
|
||||
FetchContent_MakeAvailable(pugixml)
|
||||
else()
|
||||
|
||||
@@ -31,9 +31,15 @@ if(EXISTS "${CMAKE_SOURCE_DIR}/SingleApplication/CMakeLists.txt")
|
||||
set(FETCHCONTENT_SOURCE_DIR_SINGLEAPPLICATION "${CMAKE_SOURCE_DIR}/SingleApplication")
|
||||
endif()
|
||||
|
||||
# Pinned to the commit v3.2.0 points at, not to the tag itself; see the note in
|
||||
# fetch_pugixml.cmake. v3.2.0 is a lightweight tag, a ref pointing straight at
|
||||
# the commit, so "git ls-remote <repo> refs/tags/v3.2.0" prints that commit and
|
||||
# nothing else. An annotated tag, as KDE uses in fetch_kdeaddons.cmake, would
|
||||
# print the tag object under refs/tags/v3.2.0 as well, with the commit on the
|
||||
# refs/tags/v3.2.0^{} line.
|
||||
FetchContent_Declare(
|
||||
SingleApplication
|
||||
GIT_REPOSITORY https://github.com/itay-grudev/SingleApplication.git
|
||||
GIT_TAG v3.2.0)
|
||||
GIT_TAG aede311d28d20179216c5419b581087be2a8409f) # v3.2.0
|
||||
set(QT_DEFAULT_MAJOR_VERSION 6)
|
||||
FetchContent_MakeAvailable(SingleApplication)
|
||||
|
||||
@@ -248,8 +248,12 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/qet.h
|
||||
${QET_DIR}/sources/qeticons.cpp
|
||||
${QET_DIR}/sources/qeticons.h
|
||||
${QET_DIR}/sources/palettegraphicsview.cpp
|
||||
${QET_DIR}/sources/palettegraphicsview.h
|
||||
${QET_DIR}/sources/qetpalette.cpp
|
||||
${QET_DIR}/sources/qetpalette.h
|
||||
${QET_DIR}/sources/qetstyle.cpp
|
||||
${QET_DIR}/sources/qetstyle.h
|
||||
${QET_DIR}/sources/qetinformation.cpp
|
||||
${QET_DIR}/sources/qetinformation.h
|
||||
${QET_DIR}/sources/qetmainwindow.cpp
|
||||
@@ -434,6 +438,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/ElementsCollection/elementcollectionhandler.h
|
||||
${QET_DIR}/sources/ElementsCollection/elementcollectionitem.cpp
|
||||
${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.h
|
||||
${QET_DIR}/sources/ElementsCollection/elementscollectionwidget.cpp
|
||||
@@ -699,6 +705,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/ui/contactgroupselectiondialog.h
|
||||
${QET_DIR}/sources/ui/conductorpropertiesdialog.cpp
|
||||
${QET_DIR}/sources/ui/conductorpropertiesdialog.h
|
||||
${QET_DIR}/sources/ui/conductorcolortoolbutton.cpp
|
||||
${QET_DIR}/sources/ui/conductorcolortoolbutton.h
|
||||
${QET_DIR}/sources/ui/conductorpropertieswidget.cpp
|
||||
${QET_DIR}/sources/ui/conductorpropertieswidget.h
|
||||
${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/document-export.png by the QElectroTech team (License CC BY-ND 3.0)
|
||||
ico/128x128/project.png by the QElectroTech team (License CC BY-ND 3.0)
|
||||
ico/128x128/terminalstrip.png and configure-shortcuts.png by Jeff Patterson from the QElectroTech team (License CC BY-ND 3.0), rendered from the .svg files beside them
|
||||
ico/scalable/pdf-import.svg by Jeff Patterson from the QElectroTech team (License CC BY-ND 3.0), laid out like ico/22x22/insert-image.png
|
||||
ico/scalable/diagram.svg, folio-new.svg, folio-delete.svg, folio-properties.svg, label.svg by Jeff Patterson from the QElectroTech team (License CC BY-ND 3.0), the folio icons redrawn in the same style
|
||||
ico/256x256/* by Nuri from the QElectroTech team (License CC BY-ND 3.0)
|
||||
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)
|
||||
|
||||
@@ -148,15 +148,8 @@
|
||||
<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.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_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-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/document-close.png">22x22/document-close.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/hotspot.png">22x22/hotspot.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/line.png">22x22/line.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-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/pdf-import.png">22x22/pdf-import.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/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.png">48x48/user-away.png</file>
|
||||
<file alias="themes/qet/48x48/view-pim-journal.png">48x48/view-pim-journal.png</file>
|
||||
<file alias="themes/qet/128x128/configure-shortcuts.png">128x128/configure-shortcuts.png</file>
|
||||
<file alias="themes/qet/128x128/diagram.png">128x128/diagram.png</file>
|
||||
<file alias="themes/qet/128x128/document-export.png">128x128/document-export.png</file>
|
||||
<file alias="themes/qet/128x128/plasmagik.png">128x128/plasmagik.png</file>
|
||||
<file alias="themes/qet/128x128/printer.png">128x128/printer.png</file>
|
||||
<file alias="themes/qet/128x128/project.png">128x128/project.png</file>
|
||||
<file alias="themes/qet/128x128/settings.png">128x128/settings.png</file>
|
||||
<file alias="themes/qet/128x128/terminalstrip.png">128x128/terminalstrip.png</file>
|
||||
<file alias="themes/qet/scalable/diagram.svg">scalable/diagram.svg</file>
|
||||
<file alias="themes/qet/scalable/folio-delete.svg">scalable/folio-delete.svg</file>
|
||||
<file alias="themes/qet/scalable/folio-new.svg">scalable/folio-new.svg</file>
|
||||
<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/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>
|
||||
@@ -290,7 +289,6 @@
|
||||
<file>themes/qet-dark/16x16/conductor-edit.png</file>
|
||||
<file>themes/qet-dark/16x16/configure-toolbars.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-ok.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-show-all.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/go-company.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-move.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-remove.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/configure-toolbars.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-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/document-export.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/grid.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/line.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-rotate-right.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/portrait.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/view-pim-journal.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/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/image-flip-horizontal.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/rect-to-bezier.svg</file>
|
||||
<file>themes/qet-dark/scalable/rect-to-polyline.svg</file>
|
||||
<file alias="themes/qet-dark/128x128/document-export.png">128x128/document-export.png</file>
|
||||
<file alias="themes/qet-dark/128x128/printer.png">128x128/printer.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -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
|
||||
touched; the dark theme inherits them from the light one.
|
||||
|
||||
Qt inherits by name, not by size: once a name has any file in the dark
|
||||
theme, the parent theme is never consulted for that name, and a size the
|
||||
dark theme lacks is served by scaling the nearest dark file. An icon that
|
||||
is line art at 22 pixels and colored at 128 (the printer) would then come
|
||||
out as the 22 pixel copy scaled up on a dark palette. So for every name
|
||||
the dark theme holds, the .qrc also aliases the light files of the sizes
|
||||
the dark theme does not have, when they read on the dark window (3:1,
|
||||
measured as tests/qttest/tst_qeticons.cpp does); a light file that does
|
||||
not is left out, and Qt scales the nearest dark copy as before.
|
||||
|
||||
An icon counts as line art when fewer than 20% of its visible pixels are
|
||||
saturated. Its dark copy keeps hue and alpha and inverts lightness, scaled
|
||||
so pure black becomes (220, 220, 220), the dark palette's text color.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
@@ -64,16 +81,21 @@ SIZES = ["16x16", "22x22", "32x32", "48x48", "128x128"]
|
||||
|
||||
# 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
|
||||
# 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 = {
|
||||
"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 = [
|
||||
"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/image-flip-horizontal-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"
|
||||
WHITE_LIGHTNESS = 0.85 # a visible pixel this light counts as white
|
||||
WHITE_FRACTION = 0.30 # at or above this an icon is "light art"
|
||||
INK = 220 / 255.0 # lightness of pure black after inversion
|
||||
SVG_INK = "#dcdcdc"
|
||||
DARK_WINDOW = (53, 53, 53) # QET::Palette::fusionDark() window color
|
||||
DARK_RATIO = 3.0 # what tst_qeticons requires of a dark theme file
|
||||
|
||||
|
||||
def visible_pixels(image):
|
||||
@@ -107,6 +133,38 @@ def is_line_art(image):
|
||||
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):
|
||||
"""Invert lightness, keeping hue, saturation and alpha.
|
||||
|
||||
@@ -177,7 +235,7 @@ def main():
|
||||
light = [] # (alias, source) pairs, paths relative to ico/
|
||||
dark = [] # paths relative to ico/, files exist on disk
|
||||
changed = 0
|
||||
line_art = colored = 0
|
||||
line_art = colored = light_art = 0
|
||||
|
||||
for size in SIZES:
|
||||
folder = ICO / size
|
||||
@@ -188,7 +246,10 @@ def main():
|
||||
names.append(ALIASES[rel])
|
||||
image = Image.open(png).convert("RGBA")
|
||||
art = is_line_art(image)
|
||||
if art:
|
||||
if art and has_light_fill(image):
|
||||
art = False
|
||||
light_art += 1
|
||||
elif art:
|
||||
line_art += 1
|
||||
dark_image = None
|
||||
else:
|
||||
@@ -213,6 +274,29 @@ def main():
|
||||
changed += write_if_changed(target, text)
|
||||
dark.append(f"themes/qet-dark/scalable/{name}")
|
||||
|
||||
# Complete each dark name with the light files of its other sizes
|
||||
# that read on a dark window (see the module docstring): aliases, no
|
||||
# copies.
|
||||
dark_sizes = {}
|
||||
for path in dark:
|
||||
size, name = path.split("/")[2:]
|
||||
dark_sizes.setdefault(name, set()).add(size)
|
||||
dark_aliases = []
|
||||
for alias, source in light:
|
||||
size, name = alias.split("/")[2:]
|
||||
if name in dark_sizes and size not in dark_sizes[name] \
|
||||
and reads_on_dark(Image.open(ICO / source).convert("RGBA")):
|
||||
dark_aliases.append((f"themes/qet-dark/{size}/{name}", source))
|
||||
|
||||
# Drop dark files from an earlier run that are no longer generated, so
|
||||
# a reclassified icon falls back to the light theme instead of keeping
|
||||
# a stale copy.
|
||||
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"]
|
||||
changed += write_if_changed(THEMES / "qet" / "index.theme",
|
||||
index_theme("qet", "QElectroTech icons", dirs))
|
||||
@@ -228,11 +312,13 @@ def main():
|
||||
qrc.append(f' <file alias="{alias}">{source}</file>')
|
||||
for path in dark:
|
||||
qrc.append(f" <file>{path}</file>")
|
||||
for alias, source in dark_aliases:
|
||||
qrc.append(f' <file alias="{alias}">{source}</file>')
|
||||
qrc += [" </qresource>", "</RCC>", ""]
|
||||
changed += write_if_changed(QRC, "\n".join(qrc))
|
||||
|
||||
print(f"{line_art} line-art icons, {colored} colored icons, {len(SVGS)} SVGs; "
|
||||
f"{changed} files written")
|
||||
print(f"{line_art} line-art icons, {light_art} light icons, {colored} colored icons, "
|
||||
f"{len(SVGS)} SVGs; {changed} files written or removed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -93,10 +93,7 @@
|
||||
<file>ico/22x22/conductor2.png</file>
|
||||
<file>ico/22x22/configure.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-information.png</file>
|
||||
<file>ico/22x22/dialog-ok.png</file>
|
||||
<file>ico/22x22/document-close.png</file>
|
||||
<file>ico/22x22/document-export.png</file>
|
||||
@@ -140,8 +137,6 @@
|
||||
<file>ico/22x22/go-up.png</file>
|
||||
<file>ico/22x22/hotspot.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/line.png</file>
|
||||
<file>ico/22x22/list-add.png</file>
|
||||
@@ -242,7 +237,6 @@
|
||||
<file>ico/16x16/project.png</file>
|
||||
<file>ico/16x16/qt.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/export-csv.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 =
|
||||
elementCollectionItemForIndex(m_showed_index);
|
||||
if (eci)
|
||||
{
|
||||
eci->setBackground(QBrush());
|
||||
eci->setForeground(QBrush());
|
||||
}
|
||||
}
|
||||
|
||||
m_showed_index = m_index_at_context_menu;
|
||||
@@ -736,7 +739,11 @@ void ElementsCollectionWidget::showThisDir()
|
||||
ElementCollectionItem *eci =
|
||||
elementCollectionItemForIndex(m_showed_index);
|
||||
if (eci)
|
||||
{
|
||||
// Amber under black, whatever the palette's text color.
|
||||
eci->setBackground(QBrush(QColor(255, 204, 0, 255)));
|
||||
eci->setForeground(QBrush(Qt::black));
|
||||
}
|
||||
search();
|
||||
}
|
||||
else
|
||||
@@ -755,7 +762,10 @@ void ElementsCollectionWidget::resetShowThisDir()
|
||||
ElementCollectionItem *eci = elementCollectionItemForIndex(
|
||||
m_showed_index);
|
||||
if (eci)
|
||||
{
|
||||
eci->setBackground(QBrush());
|
||||
eci->setForeground(QBrush());
|
||||
}
|
||||
}
|
||||
|
||||
m_showed_index = QModelIndex();
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "elementstreeview.h"
|
||||
#include "elementpreviewdelegate.h"
|
||||
#include "../qetpalette.h"
|
||||
|
||||
#include "../factory/elementfactory.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
@@ -41,24 +43,11 @@ static int MAX_DND_PIXMAP_HEIGHT = 375;
|
||||
ElementsTreeView::ElementsTreeView(QWidget *parent) :
|
||||
QTreeView(parent)
|
||||
{
|
||||
// force du noir sur une alternance de blanc (comme le schema) et de gris
|
||||
// clair, avec du blanc sur bleu pas trop fonce pour la selection
|
||||
//
|
||||
// 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);
|
||||
// Rows follow the application palette. Element previews are black
|
||||
// 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
|
||||
// (bugtracker #335).
|
||||
setItemDelegate(new ElementPreviewDelegate(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,7 +209,7 @@ void ElementsTreeView::startElementDrag(const ElementsLocation &location)
|
||||
&elmt_creation_state));
|
||||
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());
|
||||
|
||||
//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 ts_name = strip->name();
|
||||
|
||||
const auto str_1 = t_label.isEmpty() ? QObject::tr("Ajouter une borne") :
|
||||
QObject::tr("Ajouter la borne %1").arg(t_label);
|
||||
|
||||
const auto str_2 = ts_name.isEmpty() ? QObject::tr("à un groupe de bornes") :
|
||||
QObject::tr("au groupe de bornes %1").arg(ts_name);
|
||||
|
||||
setText(str_1 % " " % str_2);
|
||||
QString text;
|
||||
if (ts_name.isEmpty()) {
|
||||
text = t_label.isEmpty()
|
||||
? QObject::tr("Ajouter une borne à un groupe de bornes")
|
||||
: QObject::tr("Ajouter la borne %1 à un groupe de bornes").arg(t_label);
|
||||
} else {
|
||||
text = t_label.isEmpty()
|
||||
? QObject::tr("Ajouter une borne au groupe de bornes %1").arg(ts_name)
|
||||
: QObject::tr("Ajouter la borne %1 au groupe de bornes %2").arg(t_label, ts_name);
|
||||
}
|
||||
setText(text);
|
||||
}
|
||||
|
||||
AddTerminalToStripCommand::AddTerminalToStripCommand(QVector<QSharedPointer<RealTerminal>> terminals, TerminalStrip *strip, QUndoCommand *parent) :
|
||||
@@ -50,14 +54,11 @@ AddTerminalToStripCommand::AddTerminalToStripCommand(QVector<QSharedPointer<Real
|
||||
m_new_strip{strip}
|
||||
{
|
||||
const auto ts_name = strip->name();
|
||||
const auto count = m_terminal.size();
|
||||
|
||||
const auto str_1 = m_terminal.size() > 1 ? QObject::tr("Ajouter %1 bornes").arg(m_terminal.size()) :
|
||||
QObject::tr("Ajouter une borne");
|
||||
|
||||
const auto str_2 = ts_name.isEmpty() ? QObject::tr("à un groupe de bornes") :
|
||||
QObject::tr("au groupe de bornes %1").arg(ts_name);
|
||||
|
||||
setText(str_1 % " " % str_2);
|
||||
setText(ts_name.isEmpty()
|
||||
? QObject::tr("Ajouter %n borne(s) à un groupe de bornes", "", count)
|
||||
: QObject::tr("Ajouter %n borne(s) au groupe de bornes %1", "", count).arg(ts_name));
|
||||
}
|
||||
|
||||
|
||||
@@ -137,13 +138,11 @@ void RemoveTerminalFromStripCommand::redo()
|
||||
void RemoveTerminalFromStripCommand::setCommandTitle()
|
||||
{
|
||||
const auto strip_name = m_strip->name();
|
||||
const auto count = m_terminals.size();
|
||||
|
||||
const auto str_1 = m_terminals.size()>1 ? QObject::tr("Enlever %1 bornes").arg(m_terminals.size()):
|
||||
QObject::tr("Enlever une borne");
|
||||
|
||||
const auto str_2 = strip_name.isEmpty() ? QObject::tr("d'un groupe de bornes") :
|
||||
QObject::tr("du groupe de bornes %1").arg(strip_name);
|
||||
setText(str_1 % " " % str_2);
|
||||
setText(strip_name.isEmpty()
|
||||
? QObject::tr("Enlever %n borne(s) d'un groupe de bornes", "", count)
|
||||
: QObject::tr("Enlever %n borne(s) du groupe de bornes %1", "", count).arg(strip_name));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,19 +165,30 @@ MoveTerminalCommand::MoveTerminalCommand(QSharedPointer<PhysicalTerminal> termin
|
||||
t_label.append(", ");
|
||||
t_label.append(real_t->label());
|
||||
}
|
||||
const auto strip_name = old_strip->name();
|
||||
const auto new_strip_name = new_strip->name();
|
||||
|
||||
auto strip_name = old_strip->name();
|
||||
auto new_strip_name = new_strip->name();
|
||||
|
||||
auto str_1 = t_label.isEmpty() ? QObject::tr("Déplacer une borne") :
|
||||
QObject::tr("Déplacer la borne %1").arg(t_label);
|
||||
|
||||
auto str_2 = strip_name.isEmpty() ? QObject::tr(" d'un groupe de bornes") :
|
||||
QObject::tr(" du groupe de bornes %1").arg(strip_name);
|
||||
|
||||
auto str_3 = new_strip_name.isEmpty() ? QObject::tr("vers un groupe de bornes") :
|
||||
QObject::tr("vers le groupe de bornes %1").arg(new_strip_name);
|
||||
setText(str_1 % " " % str_2 % " " % str_3);
|
||||
QString text;
|
||||
if (t_label.isEmpty()) {
|
||||
if (strip_name.isEmpty() && new_strip_name.isEmpty())
|
||||
text = QObject::tr("Déplacer une borne d'un groupe de bornes vers un groupe de bornes");
|
||||
else if (strip_name.isEmpty())
|
||||
text = QObject::tr("Déplacer une borne d'un groupe de bornes vers le groupe de bornes %1").arg(new_strip_name);
|
||||
else if (new_strip_name.isEmpty())
|
||||
text = QObject::tr("Déplacer une borne du groupe de bornes %1 vers un groupe de bornes").arg(strip_name);
|
||||
else
|
||||
text = QObject::tr("Déplacer une borne du groupe de bornes %1 vers le groupe de bornes %2").arg(strip_name, new_strip_name);
|
||||
} else {
|
||||
if (strip_name.isEmpty() && new_strip_name.isEmpty())
|
||||
text = QObject::tr("Déplacer la borne %1 d'un groupe de bornes vers un 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,
|
||||
@@ -191,17 +201,18 @@ MoveTerminalCommand::MoveTerminalCommand(QVector<QSharedPointer<PhysicalTerminal
|
||||
{
|
||||
const auto strip_name = old_strip->name();
|
||||
const auto new_strip_name = new_strip->name();
|
||||
const auto count = m_terminal.size();
|
||||
|
||||
const auto str_1 = m_terminal.size() > 1 ? QObject::tr("Déplacer des bornes") :
|
||||
QObject::tr("Déplacer une borne");
|
||||
|
||||
const auto str_2 = strip_name.isEmpty() ? QObject::tr(" d'un groupe de bornes") :
|
||||
QObject::tr(" du groupe de bornes %1").arg(strip_name);
|
||||
|
||||
const auto str_3 = new_strip_name.isEmpty() ? QObject::tr("vers un groupe de bornes") :
|
||||
QObject::tr("vers le groupe de bornes %1").arg(new_strip_name);
|
||||
|
||||
setText(str_1 % " " % str_2 % " " % str_3);
|
||||
QString text;
|
||||
if (strip_name.isEmpty() && new_strip_name.isEmpty())
|
||||
text = QObject::tr("Déplacer %n borne(s) d'un groupe de bornes vers un groupe de bornes", "", count);
|
||||
else if (strip_name.isEmpty())
|
||||
text = QObject::tr("Déplacer %n borne(s) d'un groupe de bornes vers le groupe de bornes %1", "", count).arg(new_strip_name);
|
||||
else if (new_strip_name.isEmpty())
|
||||
text = QObject::tr("Déplacer %n borne(s) du groupe de bornes %1 vers un groupe de bornes", "", count).arg(strip_name);
|
||||
else
|
||||
text = QObject::tr("Déplacer %n borne(s) du groupe de bornes %1 vers le groupe de bornes %2", "", count).arg(strip_name, new_strip_name);
|
||||
setText(text);
|
||||
}
|
||||
|
||||
void MoveTerminalCommand::undo()
|
||||
|
||||
@@ -275,3 +275,80 @@ QString NumerotationContext::formatValue(const QStringList &item)
|
||||
return QString("%1").arg(value.toInt(), 3, 10, QChar('0'));
|
||||
return QString::number(value.toInt());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NumerotationContext::saveToSettings
|
||||
Save a hash of named NumerotationContexts to QSettings.
|
||||
@param contexts : the named rules to save
|
||||
@param currentRule : the name of the currently active rule
|
||||
@param settings : QSettings instance
|
||||
@param prefix : settings key prefix (e.g. "autonum/conductor")
|
||||
*/
|
||||
void NumerotationContext::saveToSettings(
|
||||
const QHash<QString, NumerotationContext> &contexts,
|
||||
const QString ¤tRule,
|
||||
QSettings &settings,
|
||||
const QString &prefix)
|
||||
{
|
||||
settings.setValue(prefix + "/current", currentRule);
|
||||
|
||||
// Clear stale array entries before writing (beginWriteArray does not
|
||||
// remove entries beyond the new size).
|
||||
settings.remove(prefix + "/rules");
|
||||
|
||||
QStringList names = contexts.keys();
|
||||
settings.beginWriteArray(prefix + "/rules", names.size());
|
||||
for (int i = 0; i < names.size(); ++i) {
|
||||
settings.setArrayIndex(i);
|
||||
const QString &name = names.at(i);
|
||||
const NumerotationContext &nc = contexts.value(name);
|
||||
|
||||
settings.setValue("name", name);
|
||||
|
||||
// Serialize context to XML string
|
||||
QDomDocument doc;
|
||||
NumerotationContext nc_copy = nc;
|
||||
QDomElement root = nc_copy.toXml(doc, "context");
|
||||
doc.appendChild(root);
|
||||
settings.setValue("xml", doc.toString());
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NumerotationContext::loadFromSettings
|
||||
Load named NumerotationContexts from QSettings.
|
||||
@param settings : QSettings instance
|
||||
@param prefix : settings key prefix (e.g. "autonum/conductor")
|
||||
@return pair of (hash of named rules, name of current rule)
|
||||
*/
|
||||
QPair<QHash<QString, NumerotationContext>, QString> NumerotationContext::loadFromSettings(
|
||||
QSettings &settings,
|
||||
const QString &prefix)
|
||||
{
|
||||
QPair<QHash<QString, NumerotationContext>, QString> result;
|
||||
QHash<QString, NumerotationContext> &contexts = result.first;
|
||||
QString ¤tRule = result.second;
|
||||
|
||||
currentRule = settings.value(prefix + "/current").toString();
|
||||
|
||||
int size = settings.beginReadArray(prefix + "/rules");
|
||||
for (int i = 0; i < size; ++i) {
|
||||
settings.setArrayIndex(i);
|
||||
QString name = settings.value("name").toString();
|
||||
QString xmlStr = settings.value("xml").toString();
|
||||
|
||||
if (name.isEmpty() || xmlStr.isEmpty()) continue;
|
||||
|
||||
QDomDocument doc;
|
||||
if (!doc.setContent(xmlStr)) continue;
|
||||
|
||||
QDomElement root = doc.documentElement();
|
||||
NumerotationContext nc;
|
||||
nc.fromXml(root);
|
||||
contexts.insert(name, nc);
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#include <QStringList>
|
||||
#include <QVariant>
|
||||
#include <QDomElement>
|
||||
#include <QHash>
|
||||
#include <QSettings>
|
||||
|
||||
/**
|
||||
This class represents a numerotation context, i.e. the data (type, value, increase)
|
||||
@@ -60,6 +62,14 @@ class NumerotationContext
|
||||
/// UI preview of a part's value matches what actually gets rendered.
|
||||
static QString formatValue(const QStringList &item);
|
||||
|
||||
static void saveToSettings(const QHash<QString, NumerotationContext> &contexts,
|
||||
const QString ¤tRule,
|
||||
QSettings &settings,
|
||||
const QString &prefix);
|
||||
static QPair<QHash<QString, NumerotationContext>, QString> loadFromSettings(
|
||||
QSettings &settings,
|
||||
const QString &prefix);
|
||||
|
||||
private:
|
||||
QStringList content_;
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "diagramposition.h"
|
||||
#include "factory/elementfactory.h"
|
||||
#include "qetapp.h"
|
||||
#include "qetpalette.h"
|
||||
#include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
|
||||
#include "qetgraphicsitem/conductor.h"
|
||||
#include "qetgraphicsitem/conductortextitem.h"
|
||||
@@ -283,10 +284,11 @@ void Diagram::drawBackground(QPainter *p, const QRectF &r) {
|
||||
* if background color is black,
|
||||
* then grid spots shall be white,
|
||||
* else they shall be black in color.
|
||||
* A view that shows the sheet with its lightness inverted
|
||||
* gets softer dots, see QET::Palette::gridDotColor.
|
||||
*/
|
||||
QPen pen;
|
||||
Diagram::background_color == Qt::black? pen.setColor(Qt::white)
|
||||
: pen.setColor(Qt::black);
|
||||
pen.setColor(QET::Palette::gridDotColor(Diagram::background_color, m_inverted_lightness));
|
||||
pen.setCosmetic(true);
|
||||
p->setPen(pen);
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ class Diagram : public QGraphicsScene
|
||||
qreal diagram_qet_version_;
|
||||
|
||||
bool draw_grid_;
|
||||
bool m_inverted_lightness = false;
|
||||
bool use_border_;
|
||||
bool draw_guides_;
|
||||
QList<Diagram::Guide> m_guides_list;
|
||||
@@ -222,6 +223,7 @@ class Diagram : public QGraphicsScene
|
||||
ExportProperties applyProperties(const ExportProperties &);
|
||||
void setDisplayGrid(bool);
|
||||
bool displayGrid();
|
||||
void setInvertedLightness(bool);
|
||||
void setDisplayGuides(bool);
|
||||
bool displayGuides();
|
||||
void updateProjectGuides(const QList<GuideProperties> &guides);
|
||||
@@ -355,6 +357,18 @@ inline void Diagram::setDisplayGrid(bool dg) {
|
||||
draw_grid_ = dg;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Diagram::setInvertedLightness
|
||||
Tell the diagram whether the view painting it will show the result
|
||||
with its lightness inverted (PaletteGraphicsView on a dark palette).
|
||||
drawBackground draws a softer grid in that case. Printing
|
||||
and export never set this.
|
||||
@param inverted
|
||||
*/
|
||||
inline void Diagram::setInvertedLightness(bool inverted) {
|
||||
m_inverted_lightness = inverted;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Diagram::displayGrid
|
||||
@return draw_grid_ true if the grid is drawn, false otherwise.
|
||||
|
||||
@@ -29,7 +29,14 @@
|
||||
*/
|
||||
DiagramEventAddText::DiagramEventAddText(Diagram *diagram) :
|
||||
DiagramEventInterface(diagram)
|
||||
{}
|
||||
{
|
||||
//The tool is armed from the moment it is attached: the next left
|
||||
//click places a text. DiagramView::keyPressEvent() asks
|
||||
//Diagram::eventInterfaceIsRunning() -- which is isRunning(), i.e.
|
||||
//m_running -- before letting Escape through to the tool, so without
|
||||
//this the view swallows Escape and the tool cannot be cancelled.
|
||||
m_running = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddText::~DiagramEventAddText
|
||||
@@ -52,6 +59,8 @@ void DiagramEventAddText::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
event->scenePos()));
|
||||
text->setTextInteractionFlags(Qt::TextEditorInteraction);
|
||||
text->setFocus(Qt::MouseFocusReason);
|
||||
//Placed: the tool is done before it announces it.
|
||||
m_running = false;
|
||||
emit finish();
|
||||
event->setAccepted(true);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,9 @@
|
||||
#include "ElementsCollection/xmlelementcollection.h"
|
||||
#include "NameList/nameslist.h"
|
||||
#include "elementdialog.h"
|
||||
#include <QApplication>
|
||||
#include <QDropEvent>
|
||||
#include <QPainter>
|
||||
#include <QPointer>
|
||||
|
||||
/**
|
||||
@@ -48,7 +50,7 @@
|
||||
@param parent Le QWidget parent de cette vue de schema
|
||||
*/
|
||||
DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
|
||||
QGraphicsView (parent),
|
||||
PaletteGraphicsView (parent),
|
||||
m_diagram (diagram)
|
||||
{
|
||||
grabGesture(Qt::PinchGesture);
|
||||
@@ -1081,6 +1083,16 @@ bool DiagramView::event(QEvent *e) {
|
||||
return(QGraphicsView::event(e));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramView::paintingInverted
|
||||
Reimplemented from PaletteGraphicsView: tell the diagram it is being
|
||||
drawn for an inverted display, so it softens its grid.
|
||||
*/
|
||||
void DiagramView::paintingInverted(bool inverted)
|
||||
{
|
||||
m_diagram->setInvertedLightness(inverted);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramView::paintEvent
|
||||
Reimplemented from QGraphicsView
|
||||
@@ -1088,7 +1100,7 @@ bool DiagramView::event(QEvent *e) {
|
||||
*/
|
||||
void DiagramView::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
QGraphicsView::paintEvent(event);
|
||||
PaletteGraphicsView::paintEvent(event);
|
||||
|
||||
if (m_free_rubberbanding && m_free_rubberband.count() >= 3)
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#include "titleblock/templatelocation.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QGraphicsView>
|
||||
#include "palettegraphicsview.h"
|
||||
|
||||
class Conductor;
|
||||
class Diagram;
|
||||
@@ -35,7 +35,7 @@ class QGestureEvent;
|
||||
This class provides a widget to render an electric diagram in an editable,
|
||||
interactive way.
|
||||
*/
|
||||
class DiagramView : public QGraphicsView
|
||||
class DiagramView : public PaletteGraphicsView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@@ -84,6 +84,7 @@ class DiagramView : public QGraphicsView
|
||||
///Set for one call only, by the Escape handler, to let focus leave the view.
|
||||
bool m_releasing_focus = false;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void paintingInverted(bool inverted) override;
|
||||
void mousePressEvent(QMouseEvent *) override;
|
||||
void mouseMoveEvent(QMouseEvent *) override;
|
||||
void mouseReleaseEvent(QMouseEvent *) override;
|
||||
|
||||
@@ -52,6 +52,18 @@ ElementsCollectionCache::ElementsCollectionCache(const QString &database_path, Q
|
||||
QSqlQuery(cache_db_).exec("PRAGMA locking_mode = EXCLUSIVE");
|
||||
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
|
||||
#pragma message("@TODO the tables could already exist, handle that case.")
|
||||
#endif
|
||||
|
||||
@@ -54,24 +54,8 @@ ElementsPanel::ElementsPanel(QWidget *parent) :
|
||||
setDropIndicatorShown(true);
|
||||
setAutoExpandDelay(1000);
|
||||
|
||||
// force du noir sur une alternance de blanc (comme le schema) et de gris
|
||||
// clair, avec du blanc sur bleu pas trop fonce pour la selection
|
||||
//
|
||||
// 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);
|
||||
// Rows follow the application palette; the icons shown here come from
|
||||
// the icon theme, which has a dark variant.
|
||||
|
||||
// we handle double click on items ourselves
|
||||
connect(this, &ElementsPanel::itemDoubleClicked, this, &ElementsPanel::slot_doubleClick);
|
||||
|
||||
@@ -137,25 +137,14 @@ void ElementTextsMover::endMovement()
|
||||
|
||||
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)
|
||||
undo_text.append(QObject::tr("Déplacer un texte d'élément"));
|
||||
else if(m_text_count > 1)
|
||||
undo_text.append(QObject::tr("Déplacer %1 textes d'élément").arg(m_items_hash.size()));
|
||||
if (parts.isEmpty())
|
||||
return QString(); // should never occur
|
||||
|
||||
if(m_group_count >= 1)
|
||||
{
|
||||
if(undo_text.isEmpty())
|
||||
undo_text.append(QObject::tr("Déplacer"));
|
||||
else
|
||||
undo_text.append(QObject::tr(" et"));
|
||||
|
||||
if(m_group_count == 1)
|
||||
undo_text.append(QObject::tr(" un groupe de texte"));
|
||||
else
|
||||
undo_text.append(QObject::tr((" %1 groupes de textes")).arg(m_group_count));
|
||||
}
|
||||
|
||||
return undo_text;
|
||||
return QObject::tr("Déplacer %1").arg(QLocale().createSeparatedList(parts));
|
||||
}
|
||||
|
||||
@@ -146,12 +146,13 @@ QPixmap ElementPictureFactory::pixmap(const ElementsLocation &location)
|
||||
QPixmap pix(w, h);
|
||||
//Element definitions almost always draw with a hardcoded black
|
||||
//stroke color, on the assumption of the white diagram sheet they
|
||||
//are normally placed on. A transparent background here makes
|
||||
//that stroke disappear against a dark widget/tree-view background
|
||||
//(bugtracker #335). Give it an opaque white background instead -
|
||||
//exactly what the element already assumes visually, in every
|
||||
//context this pixmap is used (tree icons, drag icon, previews).
|
||||
pix.fill(Qt::white);
|
||||
//are normally placed on. The pixmap is kept as drawn, on a
|
||||
//transparent background: the places that show it (the
|
||||
//collection tree through ElementPreviewDelegate, the drag icon)
|
||||
//adapt it to the palette with QET::Palette::forPalette(), so a
|
||||
//dark palette gets light ink instead of black on black
|
||||
//(bugtracker #335).
|
||||
pix.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&pix);
|
||||
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())
|
||||
{
|
||||
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,95 @@
|
||||
/*
|
||||
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;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -271,84 +271,88 @@ QString QET::ElementsAndConductorsSentence(
|
||||
int tables_count,
|
||||
int terminal_strip_count)
|
||||
{
|
||||
QString text;
|
||||
QStringList parts;
|
||||
if (elements_count) {
|
||||
text += QObject::tr(
|
||||
parts.append(
|
||||
QObject::tr(
|
||||
"%n élément(s)",
|
||||
"part of a sentence listing the content of a diagram",
|
||||
"part of a enumerative partial sentence listing the content of a diagram",
|
||||
elements_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (conductors_count) {
|
||||
if (!text.isEmpty()) text += ", ";
|
||||
text += QObject::tr(
|
||||
parts.append(
|
||||
QObject::tr(
|
||||
"%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
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (texts_count) {
|
||||
if (!text.isEmpty()) text += ", ";
|
||||
text += QObject::tr(
|
||||
parts.append(
|
||||
QObject::tr(
|
||||
"%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
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (images_count) {
|
||||
if (!text.isEmpty()) text += ", ";
|
||||
// Qt's %n only selects a grammatical singular/plural form (the
|
||||
// "(s)" convention used by every other count here) -- it never
|
||||
// spells the number out as a word, so getting "une image"
|
||||
// instead of the literal "1 image" for the single-item case
|
||||
// means handling that count outside %n entirely, with its own
|
||||
// fixed string.
|
||||
text += images_count == 1
|
||||
? QObject::tr("une image", "part of a sentence listing the content of a diagram")
|
||||
: QObject::tr(
|
||||
"%n images",
|
||||
"part of a sentence listing the content of a diagram",
|
||||
parts.append(
|
||||
QObject::tr(
|
||||
"%n image(s)",
|
||||
"part of a enumerative partial sentence listing the content of a diagram",
|
||||
images_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (shapes_count) {
|
||||
if (!text.isEmpty()) text += ", ";
|
||||
text += QObject::tr(
|
||||
parts.append(
|
||||
QObject::tr(
|
||||
"%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
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (element_text_count) {
|
||||
if (!text.isEmpty()) text += ", ";
|
||||
text += QObject::tr(
|
||||
parts.append(
|
||||
QObject::tr(
|
||||
"%n texte(s) d'élément",
|
||||
"part of a sentence listing the content of a diagram",
|
||||
element_text_count);
|
||||
"part of a enumerative partial sentence listing the content of a diagram",
|
||||
element_text_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (tables_count) {
|
||||
if (!text.isEmpty()) text += ", ";
|
||||
text += QObject::tr(
|
||||
parts.append(
|
||||
QObject::tr(
|
||||
"%n tableau(s)",
|
||||
"part of a sentence listing the content of diagram",
|
||||
tables_count);
|
||||
"part of a enumerative partial sentence listing the content of diagram",
|
||||
tables_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (terminal_strip_count) {
|
||||
if (!text.isEmpty()) text += ", ";
|
||||
text += QObject::tr(
|
||||
"%n plan de bornes",
|
||||
"part of a sentence listing the content of a diagram",
|
||||
terminal_strip_count);
|
||||
parts.append(
|
||||
QObject::tr(
|
||||
"%n plan(s) de bornes",
|
||||
"part of a enumerative partial sentence listing the content of a diagram",
|
||||
terminal_strip_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return(text);
|
||||
return QLocale().createSeparatedList(parts);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "qetdiagrameditor.h"
|
||||
#include "qeticons.h"
|
||||
#include "qetpalette.h"
|
||||
#include "qetstyle.h"
|
||||
#include "utils/qetutils.h"
|
||||
#include "qetmessagebox.h"
|
||||
#include "qetproject.h"
|
||||
@@ -52,6 +53,7 @@
|
||||
#include <QFontDatabase>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QRegularExpression>
|
||||
#include <QStyleFactory>
|
||||
#include <QStyleHints>
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
# include "ui/nokde/kautosavefile.h"
|
||||
@@ -234,6 +236,8 @@ QString QETApp::loadedQtTranslationFile()
|
||||
void QETApp::setLanguage(const QString &desired_language) {
|
||||
QString languages_path = languagesPath();
|
||||
|
||||
QLocale::setDefault(QLocale(desired_language));
|
||||
|
||||
// load Qt library translations
|
||||
QString qt_l10n_path = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
|
||||
if (!qtTranslator.load("qt_" + desired_language, qt_l10n_path))
|
||||
@@ -1806,6 +1810,10 @@ void QETApp::useSystemPalette(bool use) {
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
// Widgets with their own style sheet keep the palette they were
|
||||
// polished with; after a live light/dark switch they would stay in
|
||||
// the old colors (see QET::Palette::refreshStyleSheets).
|
||||
QET::Palette::refreshStyleSheets();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2368,6 +2376,12 @@ void QETApp::applyIconTheme(const QPalette &palette)
|
||||
*/
|
||||
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();
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
#include "recentfiles.h"
|
||||
#include "shortcutmanager.h"
|
||||
#include "ui/bomexportdialog.h"
|
||||
#include "ui/conductorcolortoolbutton.h"
|
||||
#include "ui/jumptoelementdialog.h"
|
||||
#include "ui/diagrampropertieseditordockwidget.h"
|
||||
#include "ui/backupdialog.h"
|
||||
@@ -389,6 +390,13 @@ void QETDiagramEditor::setUpActions()
|
||||
if (ProjectView *pv = currentProjectView())
|
||||
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
|
||||
m_auto_break_conductor = new QAction (QET::Icons::Conductor, tr("Coupure automatique de conducteur(s)","Tool tip of auto break conductor"), this);
|
||||
@@ -912,6 +920,10 @@ void QETDiagramEditor::setUpToolBar()
|
||||
diagram_tool_bar -> addAction (m_conductor_reset);
|
||||
diagram_tool_bar -> addAction (m_auto_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->setObjectName("adding");
|
||||
@@ -2110,6 +2122,10 @@ void QETDiagramEditor::slot_updateModeActions()
|
||||
m_auto_conductor -> setDisabled(true);
|
||||
m_auto_break_conductor -> setDisabled(true);
|
||||
}
|
||||
|
||||
if (m_conductor_color_button) {
|
||||
m_conductor_color_button->updateEnabledState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,7 @@ class QMdiSubWindow;
|
||||
class QETProject;
|
||||
class QETResult;
|
||||
class ProjectView;
|
||||
class ConductorColorToolButton;
|
||||
class CustomElement;
|
||||
class Diagram;
|
||||
class DiagramView;
|
||||
@@ -237,6 +238,9 @@ class QETDiagramEditor : public QETMainWindow
|
||||
*m_find = nullptr,
|
||||
*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;
|
||||
|
||||
QList <QAction *> m_zoom_action_toolBar; ///Only zoom action must displayed in the toolbar
|
||||
|
||||
void removeDiagramSilent(Diagram *diagram);
|
||||
|
||||
@@ -629,7 +629,13 @@ void DynamicElementTextItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||
int diffx = qRound(current_parent_pos.x() - button_down_parent_pos.x());
|
||||
int diffy = qRound(current_parent_pos.y() - button_down_parent_pos.y());
|
||||
QPointF new_pos = m_initial_position + QPointF(diffx, diffy);
|
||||
setPos(new_pos);
|
||||
//Snap to the grid, Ctrl to place freely -- the same line
|
||||
//ElementTextItemGroup::mouseMoveEvent() and
|
||||
//ElementTextsMover::continueMovement() already use, and
|
||||
//DiagramTextItem::mouseMoveEvent() for independent texts.
|
||||
//Without it this was the only text move in the editor that
|
||||
//ignored the grid.
|
||||
event->modifiers() == Qt::ControlModifier ? setPos(new_pos) : setPos(Diagram::snapToGrid(new_pos));
|
||||
|
||||
if(diagram())
|
||||
diagram()->elementTextsMover().continueMovement(event);
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace QET {
|
||||
QIcon ConductorEdit;
|
||||
QIcon ConductorSettings;
|
||||
QIcon Configure;
|
||||
QIcon ConfigureShortcuts;
|
||||
QIcon ConfigureToolbars;
|
||||
QIcon IC_CopyFile;
|
||||
QIcon DefaultConductor;
|
||||
@@ -408,6 +409,7 @@ void QET::Icons::initIcons()
|
||||
ConductorEdit = QIcon::fromTheme("conductor-edit");
|
||||
ConductorSettings = QIcon::fromTheme("conductor-reset");
|
||||
Configure = QIcon::fromTheme("configure");
|
||||
ConfigureShortcuts = QIcon::fromTheme("configure-shortcuts");
|
||||
ConfigureToolbars = QIcon::fromTheme("configure-toolbars");
|
||||
IC_CopyFile = QIcon::fromTheme("item-copy");
|
||||
DiagramAdd = QIcon::fromTheme("folio-new");
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace QET {
|
||||
extern QIcon ConductorEdit;
|
||||
extern QIcon ConductorSettings;
|
||||
extern QIcon Configure;
|
||||
extern QIcon ConfigureShortcuts;
|
||||
extern QIcon ConfigureToolbars;
|
||||
extern QIcon IC_CopyFile;
|
||||
extern QIcon DefaultConductor;
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
*/
|
||||
#include "qetpalette.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QColor>
|
||||
#include <QImage>
|
||||
#include <QStyle>
|
||||
#include <QWidget>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
@@ -74,6 +78,57 @@ bool QET::Palette::isDark(const QPalette &palette)
|
||||
return palette.color(QPalette::Active, QPalette::Window).lightness() < 128;
|
||||
}
|
||||
|
||||
void QET::Palette::invertLightness(QImage &image, const QColor &sheet,
|
||||
const QColor &ink)
|
||||
{
|
||||
if (image.format() != QImage::Format_RGB32)
|
||||
image.convertTo(QImage::Format_RGB32);
|
||||
|
||||
// One table per channel maps the inverted value (0 = was white,
|
||||
// 255 = was black) onto the sheet..ink span.
|
||||
uchar red_of[256], green_of[256], blue_of[256];
|
||||
for (int v = 0; v < 256; ++v) {
|
||||
red_of[v] = uchar(sheet.red() + (ink.red() - sheet.red()) * v / 255);
|
||||
green_of[v] = uchar(sheet.green() + (ink.green() - sheet.green()) * v / 255);
|
||||
blue_of[v] = uchar(sheet.blue() + (ink.blue() - sheet.blue()) * v / 255);
|
||||
}
|
||||
|
||||
/* Inverting the lightness of an HSL color while keeping its hue and
|
||||
saturation leaves the distance between the highest and the lowest
|
||||
channel unchanged, so it comes down to one offset per pixel:
|
||||
c + 255 - max - min. The offset turns the highest channel into
|
||||
255 - min and the lowest into 255 - max, so no channel can leave
|
||||
the 0..255 range and no clamping is needed. The loop runs on every
|
||||
repaint of a folio, hence the plain integer arithmetic. */
|
||||
for (int y = 0; y < image.height(); ++y) {
|
||||
quint32 *line = reinterpret_cast<quint32 *>(image.scanLine(y));
|
||||
for (int x = 0, width = image.width(); x < width; ++x) {
|
||||
const quint32 pixel = line[x];
|
||||
const int red = (pixel >> 16) & 0xff;
|
||||
const int green = (pixel >> 8) & 0xff;
|
||||
const int blue = pixel & 0xff;
|
||||
int highest = red > green ? red : green;
|
||||
int lowest = red < green ? red : green;
|
||||
if (blue > highest) highest = blue;
|
||||
if (blue < lowest) lowest = blue;
|
||||
const int offset = 255 - highest - lowest;
|
||||
line[x] = 0xff000000u
|
||||
| (quint32(red_of[red + offset]) << 16)
|
||||
| (quint32(green_of[green + offset]) << 8)
|
||||
| quint32(blue_of[blue + offset]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QColor QET::Palette::gridDotColor(const QColor &sheet, bool inverted)
|
||||
{
|
||||
if (sheet == QColor(Qt::black))
|
||||
return Qt::white;
|
||||
if (inverted)
|
||||
return QColor(sheet.red() * 2 / 3, sheet.green() * 2 / 3, sheet.blue() * 2 / 3);
|
||||
return Qt::black;
|
||||
}
|
||||
|
||||
double QET::Palette::contrastRatio(const QColor &a, const QColor &b)
|
||||
{
|
||||
double lighter = relativeLuminance(a);
|
||||
@@ -177,3 +232,79 @@ QPalette QET::Palette::forFusion(const QPalette &platform)
|
||||
return withPlatformAccent(isDark(platform) ? fusionDark() : fusionLight(),
|
||||
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
|
||||
#define QET_PALETTE_H
|
||||
|
||||
#include <QImage>
|
||||
#include <QPalette>
|
||||
#include <QPixmap>
|
||||
|
||||
class QImage;
|
||||
class QStyle;
|
||||
|
||||
/**
|
||||
@@ -52,6 +55,28 @@ namespace QET {
|
||||
*/
|
||||
bool isDark(const QPalette &palette);
|
||||
|
||||
/**
|
||||
Invert the lightness of every pixel of \a image, keeping its hue
|
||||
and saturation, then stretch the result between two colors: pure
|
||||
white becomes \a sheet, pure black becomes \a ink, and a red
|
||||
line stays red, only lighter. Made for a rendering of a white
|
||||
sheet that has to read on a dark palette, with sheet = Base and
|
||||
ink = Text. The image must be opaque; an image in another format
|
||||
is converted to RGB32 first.
|
||||
*/
|
||||
void invertLightness(QImage &image, const QColor &sheet = Qt::black,
|
||||
const QColor &ink = Qt::white);
|
||||
|
||||
/**
|
||||
The color of the grid dots on a sheet of color \a sheet: black,
|
||||
or white on a black sheet. With \a inverted the sheet is about to
|
||||
be shown with its lightness inverted (PaletteGraphicsView), where
|
||||
black dots would come out as bright as the ink; the dots are then
|
||||
a third of the way from the sheet color to black, which shows as
|
||||
a soft gray.
|
||||
*/
|
||||
QColor gridDotColor(const QColor &sheet, bool inverted);
|
||||
|
||||
/**
|
||||
WCAG 2 contrast ratio between two opaque colors, from 1 (equal)
|
||||
to 21 (black on white). Normal text needs at least 4.5, large
|
||||
@@ -87,6 +112,41 @@ namespace QET {
|
||||
platform's accent color kept when it is readable.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
@@ -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 "../xrefpropertieswidget.h"
|
||||
#include "guidespropertieswidget.h"
|
||||
#include "../autoNum/numerotationcontext.h"
|
||||
#include "../autoNum/ui/selectautonumw.h"
|
||||
#include <QFont>
|
||||
#include <QFontDialog>
|
||||
#include <QSizePolicy>
|
||||
@@ -101,6 +103,32 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
|
||||
}
|
||||
m_gpw->setGuides(loaded_guides);
|
||||
|
||||
// global auto-numbering defaults (only when editing global settings, not a project)
|
||||
if (!m_project) {
|
||||
auto saw_conductor = new SelectAutonumW(1);
|
||||
auto saw_element = new SelectAutonumW(0);
|
||||
auto saw_folio = new SelectAutonumW(2);
|
||||
|
||||
initAutoNumTab(m_autonum_conductor, saw_conductor, QStringLiteral("autonum/conductor"));
|
||||
initAutoNumTab(m_autonum_element, saw_element, QStringLiteral("autonum/element"));
|
||||
initAutoNumTab(m_autonum_folio, saw_folio, QStringLiteral("autonum/folio"));
|
||||
|
||||
QSettings autonum_settings;
|
||||
loadAutoNumTab(m_autonum_conductor, autonum_settings);
|
||||
loadAutoNumTab(m_autonum_element, autonum_settings);
|
||||
loadAutoNumTab(m_autonum_folio, autonum_settings);
|
||||
|
||||
// Intercept Return key in the combo line edits so it doesn't
|
||||
// activate the dialog's default button (OK).
|
||||
for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) {
|
||||
if (QComboBox *combo = tab->widget->contextComboBox()) {
|
||||
if (combo->lineEdit()) {
|
||||
combo->lineEdit()->installEventFilter(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//If there is a project, we edit his properties
|
||||
if (m_project) {
|
||||
bpw -> setProperties (m_project -> defaultBorderProperties());
|
||||
@@ -114,6 +142,7 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
|
||||
|
||||
// main tab widget
|
||||
QTabWidget *tab_widget = new QTabWidget(this);
|
||||
m_tab_widget = tab_widget;
|
||||
QWidget *diagram_widget = new QWidget();
|
||||
QVBoxLayout *diagram_layout = new QVBoxLayout(diagram_widget);
|
||||
|
||||
@@ -127,6 +156,19 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
|
||||
tab_widget -> addTab (xrefpw, tr("Références croisées"));
|
||||
tab_widget -> addTab (m_gpw, tr("Guides"));
|
||||
|
||||
// add auto-numbering tab only for global settings (not per project)
|
||||
if (!m_project) {
|
||||
QWidget *autonum_widget = new QWidget();
|
||||
QVBoxLayout *autonum_layout = new QVBoxLayout(autonum_widget);
|
||||
autonum_layout->addWidget(new QLabel(tr("Définir les règles de numérotation automatique par défaut pour les nouveaux projets :")));
|
||||
QTabWidget *autonum_inner_tab = new QTabWidget();
|
||||
autonum_inner_tab->addTab(m_autonum_conductor.widget, tr("Conducteurs"));
|
||||
autonum_inner_tab->addTab(m_autonum_element.widget, tr("Eléments"));
|
||||
autonum_inner_tab->addTab(m_autonum_folio.widget, tr("Folios"));
|
||||
autonum_layout->addWidget(autonum_inner_tab);
|
||||
tab_widget -> addTab (autonum_widget, tr("Numérotation auto"));
|
||||
}
|
||||
|
||||
QVBoxLayout *vlayout1 = new QVBoxLayout();
|
||||
vlayout1->addWidget(tab_widget);
|
||||
|
||||
@@ -230,6 +272,9 @@ void NewDiagramPage::applyConf()
|
||||
settings.setValue(QStringLiteral("color"), current_guides[i].color.name());
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
// save global auto-numbering defaults
|
||||
persistAutonumSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,6 +340,134 @@ void NewDiagramPage::loadSavedTbp()
|
||||
applyConf();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::isPlaceholder
|
||||
Return true if @a name matches the combo box's built-in placeholder text
|
||||
(first item). This is locale-independent because it reads the actual item text.
|
||||
*/
|
||||
bool NewDiagramPage::isPlaceholder(QComboBox *combo, const QString &name)
|
||||
{
|
||||
return !combo->count() || name == combo->itemText(0);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::initAutoNumTab
|
||||
Initialise an AutoNumTab struct and connect its signals.
|
||||
*/
|
||||
void NewDiagramPage::initAutoNumTab(AutoNumTab &tab, SelectAutonumW *w, const QString &prefix)
|
||||
{
|
||||
tab.widget = w;
|
||||
tab.prefix = prefix;
|
||||
|
||||
connect(w, &SelectAutonumW::applyPressed, this, [this, &tab]() { saveAutoNumContext(tab); });
|
||||
connect(w, &SelectAutonumW::removeClicked, this, [this, &tab]() { removeAutoNumContext(tab); });
|
||||
connect(w->contextComboBox(), &QComboBox::activated, this, [this, &tab](int index) {
|
||||
if (index >= 0) {
|
||||
QString name = tab.widget->contextComboBox()->itemText(index);
|
||||
if (tab.contexts.contains(name)) {
|
||||
tab.widget->setContext(tab.contexts.value(name));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::loadAutoNumTab
|
||||
Load saved rules from QSettings into an AutoNumTab.
|
||||
*/
|
||||
void NewDiagramPage::loadAutoNumTab(AutoNumTab &tab, QSettings &settings)
|
||||
{
|
||||
auto data = NumerotationContext::loadFromSettings(settings, tab.prefix);
|
||||
tab.contexts = data.first;
|
||||
for (auto it = tab.contexts.constBegin(); it != tab.contexts.constEnd(); ++it) {
|
||||
tab.widget->contextComboBox()->addItem(it.key());
|
||||
}
|
||||
if (!tab.contexts.isEmpty() && !data.second.isEmpty()
|
||||
&& tab.contexts.contains(data.second)) {
|
||||
tab.widget->contextComboBox()->setCurrentText(data.second);
|
||||
tab.widget->setContext(tab.contexts.value(data.second));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::saveAutoNumContext
|
||||
Save the current context from an AutoNumTab's widget into its hash and
|
||||
persist to QSettings immediately.
|
||||
*/
|
||||
void NewDiagramPage::saveAutoNumContext(AutoNumTab &tab)
|
||||
{
|
||||
QString name = tab.widget->contextComboBox()->currentText().trimmed();
|
||||
if (name.isEmpty() || isPlaceholder(tab.widget->contextComboBox(), name)) {
|
||||
return;
|
||||
}
|
||||
tab.contexts.insert(name, tab.widget->toNumContext());
|
||||
if (tab.widget->contextComboBox()->findText(name) == -1) {
|
||||
tab.widget->contextComboBox()->addItem(name);
|
||||
}
|
||||
persistAutonumSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::removeAutoNumContext
|
||||
Remove the current context from an AutoNumTab's hash and persist.
|
||||
*/
|
||||
void NewDiagramPage::removeAutoNumContext(AutoNumTab &tab)
|
||||
{
|
||||
QString name = tab.widget->contextComboBox()->currentText().trimmed();
|
||||
if (name.isEmpty() || isPlaceholder(tab.widget->contextComboBox(), name)) {
|
||||
return;
|
||||
}
|
||||
int idx = tab.widget->contextComboBox()->findText(name);
|
||||
if (idx == -1) return;
|
||||
tab.contexts.remove(name);
|
||||
tab.widget->contextComboBox()->removeItem(idx);
|
||||
tab.widget->contextComboBox()->setCurrentText(QString());
|
||||
tab.widget->setContext(NumerotationContext());
|
||||
persistAutonumSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::persistAutonumSettings
|
||||
Save all autonum contexts to QSettings immediately.
|
||||
*/
|
||||
void NewDiagramPage::persistAutonumSettings()
|
||||
{
|
||||
QSettings settings;
|
||||
for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) {
|
||||
QString current;
|
||||
QComboBox *combo = tab->widget->contextComboBox();
|
||||
if (!isPlaceholder(combo, combo->currentText().trimmed())
|
||||
&& tab->contexts.contains(combo->currentText().trimmed())) {
|
||||
current = combo->currentText().trimmed();
|
||||
}
|
||||
NumerotationContext::saveToSettings(tab->contexts, current,
|
||||
settings, tab->prefix);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::eventFilter
|
||||
Intercept Return/Enter in combo box line edits so it doesn't close the
|
||||
settings dialog.
|
||||
*/
|
||||
bool NewDiagramPage::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::KeyPress) {
|
||||
auto *ke = static_cast<QKeyEvent *>(event);
|
||||
if ((ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter)) {
|
||||
// Check if this is a line edit inside one of our autonum combos
|
||||
for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) {
|
||||
if (QComboBox *combo = tab->widget->contextComboBox()) {
|
||||
if (combo->lineEdit() && combo->lineEdit() == obj) {
|
||||
return true; // eat the event
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ConfigPage::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
/**
|
||||
Constructeur
|
||||
@param parent QWidget parent
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "configpage.h"
|
||||
#include "../projectpropertiesdialog.h"
|
||||
#include "../titleblockpropertieswidget.h"
|
||||
#include "../autoNum/numerotationcontext.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QtWidgets>
|
||||
@@ -32,6 +33,7 @@ class XRefPropertiesWidget;
|
||||
class GuidesPropertiesWidget;
|
||||
class QETProject;
|
||||
class TitleBlockProperties;
|
||||
class SelectAutonumW;
|
||||
|
||||
/**
|
||||
@brief The NewDiagramPage class
|
||||
@@ -48,6 +50,7 @@ class NewDiagramPage : public ConfigPage {
|
||||
~NewDiagramPage() override;
|
||||
private:
|
||||
NewDiagramPage(const NewDiagramPage &);
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
public slots:
|
||||
void changeToAutoFolioTab();
|
||||
void setFolioAutonum(QString);
|
||||
@@ -72,7 +75,25 @@ public slots:
|
||||
XRefPropertiesWidget *xrefpw; ///< Widget to edit default xref properties
|
||||
GuidesPropertiesWidget *m_gpw; ///< Widget to edit guides
|
||||
TitleBlockProperties savedTbp; ///< Used to save current TBP and retrieve later
|
||||
QTabWidget *m_tab_widget; ///< Main tab widget (stored for later access)
|
||||
|
||||
// auto-numbering tab data
|
||||
struct AutoNumTab {
|
||||
SelectAutonumW *widget = nullptr;
|
||||
QHash<QString, NumerotationContext> contexts;
|
||||
QString prefix;
|
||||
};
|
||||
AutoNumTab m_autonum_conductor;
|
||||
AutoNumTab m_autonum_element;
|
||||
AutoNumTab m_autonum_folio;
|
||||
|
||||
void initAutoNumTab(AutoNumTab &tab, SelectAutonumW *w, const QString &prefix);
|
||||
void loadAutoNumTab(AutoNumTab &tab, QSettings &settings);
|
||||
void saveAutoNumContext(AutoNumTab &tab);
|
||||
void removeAutoNumContext(AutoNumTab &tab);
|
||||
void persistAutonumSettings();
|
||||
|
||||
static bool isPlaceholder(QComboBox *combo, const QString &name);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -381,5 +381,5 @@ QString ShortcutsConfigPage::title() const
|
||||
|
||||
QIcon ShortcutsConfigPage::icon() const
|
||||
{
|
||||
return QET::Icons::ConfigureToolbars;
|
||||
return QET::Icons::ConfigureShortcuts;
|
||||
}
|
||||
|
||||
@@ -55,20 +55,12 @@ m_diagram(diagram)
|
||||
{
|
||||
openDialog();
|
||||
|
||||
QString text;
|
||||
QStringList parts;
|
||||
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(text.isEmpty())
|
||||
text.append(QObject::tr("Pivoter"));
|
||||
else
|
||||
text.append(QObject::tr(" et"));
|
||||
|
||||
text.append(QObject::tr(" %1 groupes de textes").arg(groups_list.count()));
|
||||
}
|
||||
if(!text.isNull())
|
||||
setText(text);
|
||||
parts << QObject::tr("%n groupe(s) de textes", "", groups_list.count());
|
||||
setText(QObject::tr("Pivoter %1").arg(QLocale().createSeparatedList(parts)));
|
||||
|
||||
for(DiagramTextItem *dti : texts_list)
|
||||
setupAnimation(dti, "rotation", dti->rotation(), m_rotation);
|
||||
@@ -77,7 +69,6 @@ m_diagram(diagram)
|
||||
}
|
||||
else
|
||||
setObsolete(true);
|
||||
|
||||
}
|
||||
|
||||
void RotateTextsCommand::undo()
|
||||
|
||||
@@ -95,8 +95,12 @@ add_executable(
|
||||
tst_qetpalette
|
||||
tst_qetpalette.cpp
|
||||
inkcontrast.h
|
||||
${QET_DIR}/sources/palettegraphicsview.cpp
|
||||
${QET_DIR}/sources/palettegraphicsview.h
|
||||
${QET_DIR}/sources/qetpalette.cpp
|
||||
${QET_DIR}/sources/qetpalette.h)
|
||||
${QET_DIR}/sources/qetpalette.h
|
||||
${QET_DIR}/sources/ElementsCollection/elementpreviewdelegate.cpp
|
||||
${QET_DIR}/sources/ElementsCollection/elementpreviewdelegate.h)
|
||||
add_test(NAME tst_qetpalette COMMAND tst_qetpalette)
|
||||
target_include_directories(tst_qetpalette PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_qetpalette PRIVATE Qt::Test Qt::Widgets)
|
||||
@@ -110,6 +114,7 @@ add_executable(
|
||||
${QET_DIR}/sources/qeticons.cpp
|
||||
${QET_DIR}/sources/qeticons.h
|
||||
${QET_DIR}/sources/qetpalette.cpp
|
||||
${QET_DIR}/sources/qetstyle.cpp
|
||||
${QET_DIR}/sources/qetpalette.h
|
||||
${QET_DIR}/ico/icon-themes.qrc)
|
||||
add_test(NAME tst_qeticons COMMAND tst_qeticons)
|
||||
@@ -152,7 +157,12 @@ add_test(NAME tst_crashhandler COMMAND tst_crashhandler)
|
||||
target_include_directories(tst_crashhandler PRIVATE ${QET_DIR}/sources)
|
||||
# Qt::Xml because crashhandler.cpp includes qetversion.h for the header it
|
||||
# builds at install() time, and that pulls in QDomElement.
|
||||
target_link_libraries(tst_crashhandler PRIVATE Qt::Test Qt::Xml ${Backtrace_LIBRARIES})
|
||||
if(Backtrace_FOUND)
|
||||
target_link_libraries(tst_crashhandler PRIVATE
|
||||
Qt::Test Qt::Xml ${Backtrace_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(tst_crashhandler PRIVATE Qt::Test Qt::Xml)
|
||||
endif()
|
||||
|
||||
# The crash-dump bookkeeping from #905: which dumps get listed, offered and
|
||||
# deleted, and what redact() masks. qetlogger.cpp needs exactly one symbol
|
||||
@@ -171,7 +181,12 @@ target_include_directories(tst_crashdumps PRIVATE
|
||||
${QET_DIR}/sources
|
||||
${QET_DIR}/sources/NameList
|
||||
${QET_DIR}/pugixml/src)
|
||||
target_link_libraries(tst_crashdumps PRIVATE Qt::Test Qt::Widgets Qt::Xml ${Backtrace_LIBRARIES})
|
||||
if(Backtrace_FOUND)
|
||||
target_link_libraries(tst_crashdumps PRIVATE
|
||||
Qt::Test Qt::Widgets Qt::Xml ${Backtrace_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(tst_crashdumps PRIVATE Qt::Test Qt::Widgets Qt::Xml)
|
||||
endif()
|
||||
|
||||
add_executable(
|
||||
tst_menubarkeyboard
|
||||
|
||||
@@ -38,9 +38,8 @@ namespace QET {
|
||||
image is also written there as <row>-<name>.png so a failure
|
||||
can be looked at.
|
||||
*/
|
||||
inline QImage grab(QWidget *widget, const char *name)
|
||||
inline QImage dump(const QImage &image, const char *name)
|
||||
{
|
||||
const QImage image = widget->grab().toImage();
|
||||
const QByteArray dir = qgetenv("QET_TEST_DUMP_DIR");
|
||||
if (!dir.isEmpty())
|
||||
image.save(QString("%1/%2-%3.png").arg(QString::fromLocal8Bit(dir),
|
||||
@@ -48,28 +47,55 @@ namespace QET {
|
||||
return image;
|
||||
}
|
||||
|
||||
inline QImage grab(QWidget *widget, const char *name)
|
||||
{
|
||||
return dump(widget->grab().toImage(), name);
|
||||
}
|
||||
|
||||
/**
|
||||
Contrast between the background of a rendered widget (its most
|
||||
frequent color) and the ink drawn on it (the pixel whose
|
||||
luminance differs most from the background), inside rect.
|
||||
*/
|
||||
inline double inkContrast(const QImage &image, const QRect &rect)
|
||||
inline QHash<QRgb, int> histogram(const QImage &image, const QRect &rect)
|
||||
{
|
||||
QHash<QRgb, int> histogram;
|
||||
QHash<QRgb, int> counts;
|
||||
for (int y = rect.top(); y <= rect.bottom(); ++y)
|
||||
for (int x = rect.left(); x <= rect.right(); ++x)
|
||||
++histogram[image.pixel(x, y)];
|
||||
++counts[image.pixel(x, y)];
|
||||
return counts;
|
||||
}
|
||||
|
||||
QRgb background = 0;
|
||||
/// The most frequent color inside rect.
|
||||
inline QRgb background(const QImage &image, const QRect &rect)
|
||||
{
|
||||
const QHash<QRgb, int> counts = histogram(image, rect);
|
||||
QRgb color = 0;
|
||||
int best = -1;
|
||||
for (auto it = histogram.cbegin(); it != histogram.cend(); ++it)
|
||||
if (it.value() > best) { best = it.value(); background = it.key(); }
|
||||
for (auto it = counts.cbegin(); it != counts.cend(); ++it)
|
||||
if (it.value() > best) { best = it.value(); color = it.key(); }
|
||||
return color;
|
||||
}
|
||||
|
||||
/// The color inside rect that contrasts most with the background: the ink.
|
||||
inline QRgb ink(const QImage &image, const QRect &rect)
|
||||
{
|
||||
const QHash<QRgb, int> counts = histogram(image, rect);
|
||||
const QColor bg(background(image, rect));
|
||||
QRgb color = bg.rgb();
|
||||
double contrast = 1.0;
|
||||
for (auto it = histogram.cbegin(); it != histogram.cend(); ++it)
|
||||
contrast = qMax(contrast, QET::Palette::contrastRatio(QColor(background),
|
||||
QColor(it.key())));
|
||||
return contrast;
|
||||
for (auto it = counts.cbegin(); it != counts.cend(); ++it)
|
||||
{
|
||||
const double c = QET::Palette::contrastRatio(bg, QColor(it.key()));
|
||||
if (c > contrast) { contrast = c; color = it.key(); }
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
inline double inkContrast(const QImage &image, const QRect &rect)
|
||||
{
|
||||
return QET::Palette::contrastRatio(QColor(background(image, rect)),
|
||||
QColor(ink(image, rect)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,9 @@ void tst_CrashHandler::formatsTheHandledSignals()
|
||||
{
|
||||
QCOMPARE(format(SIGSEGV), QByteArray::number(SIGSEGV));
|
||||
QCOMPARE(format(SIGABRT), QByteArray::number(SIGABRT));
|
||||
#if defined(SIGBUS)
|
||||
QCOMPARE(format(SIGBUS), QByteArray::number(SIGBUS));
|
||||
#endif
|
||||
QCOMPARE(format(SIGFPE), QByteArray::number(SIGFPE));
|
||||
QCOMPARE(format(SIGILL), QByteArray::number(SIGILL));
|
||||
}
|
||||
|
||||
@@ -21,6 +21,12 @@
|
||||
#include <QMenuBar>
|
||||
#include <QShortcut>
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
// Exported from QtGui but declared in qkeysequence.h only for the
|
||||
// documentation build; Qt's own docs say to declare it like this.
|
||||
Q_GUI_EXPORT void qt_set_sequence_auto_mnemonic(bool b);
|
||||
#endif
|
||||
|
||||
/*
|
||||
F10 should open the menu bar, as it does in most applications and as
|
||||
someone working without a mouse will expect. Qt provides this on Windows
|
||||
@@ -38,6 +44,7 @@ class TstMenuBarKeyboard : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void initTestCase();
|
||||
void altLetterOpensMenu(); // control -- must pass, or nothing below means anything
|
||||
void plainF10DoesNothingInQt(); // the gap being filled
|
||||
void shortcutOpensMenuBar(); // the mechanism QETMainWindow uses
|
||||
@@ -48,6 +55,13 @@ namespace {
|
||||
QMainWindow *makeWindow(QMenu **file_menu)
|
||||
{
|
||||
auto *w = new QMainWindow;
|
||||
#ifdef Q_OS_MACOS
|
||||
// A QMenuBar is native on macOS: its menus live in the system menu
|
||||
// bar, outside the Qt widget tree, where QTest key events never reach
|
||||
// them and QMenu::isVisible() stays false. The in-window bar runs the
|
||||
// same QMenuBar code the other platforms use.
|
||||
w->menuBar()->setNativeMenuBar(false);
|
||||
#endif
|
||||
*file_menu = w->menuBar()->addMenu(QStringLiteral("&File"));
|
||||
(*file_menu)->addAction(QStringLiteral("Quit"));
|
||||
w->menuBar()->addMenu(QStringLiteral("&Edit"))->addAction(QStringLiteral("Copy"));
|
||||
@@ -60,6 +74,18 @@ QMainWindow *makeWindow(QMenu **file_menu)
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
macOS has no Alt+letter menu mnemonics, so Qt does not turn "&File"
|
||||
into a shortcut there (qt_set_sequence_auto_mnemonic is off). The
|
||||
control below needs one; switch mnemonics on for this process.
|
||||
*/
|
||||
void TstMenuBarKeyboard::initTestCase()
|
||||
{
|
||||
#ifdef Q_OS_MACOS
|
||||
qt_set_sequence_auto_mnemonic(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
The control. Alt and a menu's letter is known to work, so if this fails
|
||||
the environment cannot open menus at all and the other two tests are
|
||||
|
||||
@@ -18,15 +18,20 @@
|
||||
#include <QtTest>
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QIcon>
|
||||
#include <QMainWindow>
|
||||
#include <QMenu>
|
||||
#include <QPainter>
|
||||
#include <QStyleFactory>
|
||||
#include <QStyleOption>
|
||||
#include <QToolBar>
|
||||
#include <QToolButton>
|
||||
|
||||
#include "inkcontrast.h"
|
||||
#include "qeticons.h"
|
||||
#include "qetpalette.h"
|
||||
#include "qetstyle.h"
|
||||
|
||||
/**
|
||||
Checks on QET's two icon themes, "qet" and "qet-dark", generated by
|
||||
@@ -34,11 +39,15 @@
|
||||
table that uses them.
|
||||
|
||||
Every icon name must resolve in both themes. The dark theme's files
|
||||
must be light ink. A toolbar button painted with Fusion must show its
|
||||
icon at 3:1 (WCAG 1.4.11) in both themes, with the disabled state
|
||||
reading weaker than the enabled one, which is what was reversed on
|
||||
macOS before (GitHub #466, #870). And the icons the elements panel
|
||||
draws in its 50 px slots must stay small.
|
||||
must read on the dark palette, and an icon drawn as a light object
|
||||
(a page sheet, the PDF import icon of GitHub #919) must come through
|
||||
the dark theme untouched rather than inverted into a black page.
|
||||
QET's own vector icons must serve every size in both themes. A
|
||||
toolbar button painted with Fusion must show its icon at 3:1 (WCAG
|
||||
1.4.11) in both themes, with the disabled state reading weaker than
|
||||
the enabled one, which is what was reversed on macOS before (GitHub
|
||||
#466, #870). And the icons the elements panel draws in its 50 px
|
||||
slots must stay small.
|
||||
*/
|
||||
class tst_qeticons : public QObject
|
||||
{
|
||||
@@ -47,10 +56,20 @@ class tst_qeticons : public QObject
|
||||
private slots:
|
||||
void initTestCase();
|
||||
void everyIconResolvesInBothThemes();
|
||||
void darkThemeFilesAreLightInk();
|
||||
void darkThemeFilesReadOnDarkPalette();
|
||||
void lightIconsStayLightInDarkTheme();
|
||||
void scalableIconsServeEverySize();
|
||||
void toolbarIconIsReadable_data();
|
||||
void toolbarIconIsReadable();
|
||||
void hoverChangesTheIcon_data();
|
||||
void hoverChangesTheIcon();
|
||||
void hoverInkReadsWithAnyAccent_data();
|
||||
void hoverInkReadsWithAnyAccent();
|
||||
void coloredIconKeepsItsColorsOnHover();
|
||||
void menuIconReadsOnHighlight_data();
|
||||
void menuIconReadsOnHighlight();
|
||||
void panelProjectIconStaysSmall();
|
||||
void configPageIconsComeAtPageSize();
|
||||
};
|
||||
|
||||
namespace {
|
||||
@@ -70,17 +89,36 @@ namespace {
|
||||
return names;
|
||||
}
|
||||
|
||||
/// Lightness of the lightest visible pixel: the ink, in a dark-theme file.
|
||||
double maxLightness(const QImage &image)
|
||||
/**
|
||||
Mean color of the visible pixels: what the icon reads as, taken
|
||||
as a whole, on the background behind it. A page-shaped icon whose
|
||||
white body was inverted keeps a light border, so the lightest
|
||||
pixel would pass it; the mean does not.
|
||||
*/
|
||||
QColor meanVisibleColor(const QImage &image)
|
||||
{
|
||||
double max = 0.0;
|
||||
qint64 r = 0, g = 0, b = 0, n = 0;
|
||||
for (int y = 0; y < image.height(); ++y)
|
||||
for (int x = 0; x < image.width(); ++x)
|
||||
{
|
||||
const QColor c = image.pixelColor(x, y);
|
||||
if (c.alpha() > 64) max = qMax(max, c.lightnessF());
|
||||
if (c.alpha() <= 64) continue;
|
||||
r += c.red(); g += c.green(); b += c.blue(); ++n;
|
||||
}
|
||||
return max;
|
||||
return n ? QColor(r / n, g / n, b / n) : QColor();
|
||||
}
|
||||
|
||||
const QStringList kSizes = {"16x16", "22x22", "32x32", "48x48", "128x128"};
|
||||
|
||||
/// palette with the given selection colors, as QET::Palette::withPlatformAccent leaves them.
|
||||
QPalette withAccent(QPalette palette, const QColor &highlight, const QColor &highlighted_text)
|
||||
{
|
||||
for (QPalette::ColorGroup group : {QPalette::Active, QPalette::Inactive})
|
||||
{
|
||||
palette.setColor(group, QPalette::Highlight, highlight);
|
||||
palette.setColor(group, QPalette::HighlightedText, highlighted_text);
|
||||
}
|
||||
return palette;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,27 +149,109 @@ void tst_qeticons::everyIconResolvesInBothThemes()
|
||||
QVERIFY2(!QIcon::fromTheme(name).pixmap(22).isNull(), qPrintable(name));
|
||||
}
|
||||
|
||||
void tst_qeticons::darkThemeFilesAreLightInk()
|
||||
/**
|
||||
Every file the generator put in the dark theme, taken as a whole,
|
||||
must reach 3:1 against the dark palette's window color. Inverted
|
||||
line art passes by a wide margin; an inverted light object (a white
|
||||
page turned black) sits below 2:1 and fails, which is the defect
|
||||
misc/make_icon_themes.py now avoids by leaving such icons to the
|
||||
light theme.
|
||||
*/
|
||||
void tst_qeticons::darkThemeFilesReadOnDarkPalette()
|
||||
{
|
||||
const QColor window = QET::Palette::fusionDark().color(QPalette::Active, QPalette::Window);
|
||||
int checked = 0;
|
||||
for (const QString &size : {"16x16", "22x22", "32x32"})
|
||||
for (const QString &size : kSizes)
|
||||
{
|
||||
QDir dir(QString(":/ico/themes/qet-dark/%1").arg(size));
|
||||
for (const QString &file : dir.entryList({"*.png"}, QDir::Files))
|
||||
{
|
||||
const QImage dark(dir.filePath(file));
|
||||
const QImage light(QString(":/ico/themes/qet/%1/%2").arg(size, file));
|
||||
QVERIFY2(!dark.isNull() && !light.isNull(), qPrintable(file));
|
||||
// A page-shaped icon keeps its white fill dark after inversion,
|
||||
// so the mean would mislead; what matters is that the ink is light.
|
||||
QVERIFY2(maxLightness(dark) >= 0.8,
|
||||
qPrintable(QString("%1/%2 has no light ink").arg(size, file)));
|
||||
QVERIFY2(!dark.isNull(), qPrintable(file));
|
||||
const QColor mean = meanVisibleColor(dark);
|
||||
QVERIFY2(mean.isValid(), qPrintable(file));
|
||||
const double contrast = QET::Palette::contrastRatio(mean, window);
|
||||
QVERIFY2(contrast >= kIconRatio,
|
||||
qPrintable(QString("%1/%2 reads %3:1 on the dark window").arg(size, file).arg(contrast)));
|
||||
++checked;
|
||||
}
|
||||
}
|
||||
QVERIFY(checked > 100);
|
||||
}
|
||||
|
||||
/**
|
||||
Icons drawn as a light object are not in the dark theme; the theme
|
||||
inherits them from "qet". Asking the dark theme for them must return
|
||||
the light art, not a dark page (GitHub #919, the PDF import icon).
|
||||
*/
|
||||
void tst_qeticons::lightIconsStayLightInDarkTheme()
|
||||
{
|
||||
const QColor window = QET::Palette::fusionDark().color(QPalette::Active, QPalette::Window);
|
||||
QIcon::setThemeName(QStringLiteral("qet-dark"));
|
||||
// The folio family is an SVG from 22 px up; their 16 px files are
|
||||
// still the light page art, as is the background swatch at 22 px.
|
||||
const QList<QPair<QString, int>> icons = {
|
||||
{"diagram", 16}, {"label", 16}, {"folio-new", 16}, {"folio-delete", 16},
|
||||
{"folio-properties", 16}, {"diagram_bg", 22}};
|
||||
for (const auto &[name, size] : icons)
|
||||
{
|
||||
QVERIFY2(!QFile::exists(QString(":/ico/themes/qet-dark/%1x%1/%2.png").arg(size).arg(name)),
|
||||
qPrintable(QString("%1 has a dark copy; the generator inverted a light icon").arg(name)));
|
||||
const QImage image = QIcon::fromTheme(name).pixmap(size).toImage();
|
||||
QVERIFY2(!image.isNull(), qPrintable(name));
|
||||
const QColor mean = meanVisibleColor(image);
|
||||
QVERIFY2(mean.lightnessF() > 0.6,
|
||||
qPrintable(QString("%1 comes out dark in the dark theme (lightness %2)")
|
||||
.arg(name).arg(mean.lightnessF())));
|
||||
QVERIFY2(QET::Palette::contrastRatio(mean, window) >= kIconRatio, qPrintable(name));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
QET's own vector icons live in ico/scalable/: one file for every size
|
||||
from the toolbar up, with a recolored copy in the dark theme. Each
|
||||
must resolve in both themes at 22, 24, 32 and 64 px, dark ink on the
|
||||
light theme and light ink on the dark one, with no 22 px PNG left
|
||||
beside it. Fusion's toolbar slot is 24 px, so the files are drawn on
|
||||
a 24 px canvas and their one pixel lines land on whole pixels there.
|
||||
*/
|
||||
void tst_qeticons::scalableIconsServeEverySize()
|
||||
{
|
||||
const QDir scalable(":/ico/themes/qet/scalable");
|
||||
QStringList names;
|
||||
for (const QString &file : scalable.entryList({"*.svg"}, QDir::Files))
|
||||
names << file.section('.', 0, -2);
|
||||
for (const QString &name : {"pdf-import", "folio-new", "folio-delete", "folio-properties", "diagram", "label"})
|
||||
QVERIFY2(names.contains(name), qPrintable(name + " is not in the scalable folder"));
|
||||
|
||||
const QByteArray dump = qgetenv("QET_TEST_DUMP_DIR");
|
||||
for (const QString &name : names)
|
||||
{
|
||||
QVERIFY2(QFile::exists(QString(":/ico/themes/qet-dark/scalable/%1.svg").arg(name)), qPrintable(name));
|
||||
QVERIFY2(!QFile::exists(QString(":/ico/themes/qet/22x22/%1.png").arg(name)),
|
||||
qPrintable(QString("%1 still has a 22 px PNG that hides the SVG").arg(name)));
|
||||
for (const QString &theme : {"qet", "qet-dark"})
|
||||
{
|
||||
QIcon::setThemeName(theme);
|
||||
const QIcon icon = QIcon::fromTheme(name);
|
||||
QVERIFY2(!icon.isNull(), qPrintable(name));
|
||||
for (int size : {22, 24, 32, 64})
|
||||
{
|
||||
const QPixmap pixmap = icon.pixmap(size);
|
||||
QCOMPARE(pixmap.width(), size);
|
||||
if (!dump.isEmpty())
|
||||
pixmap.save(QString("%1/%2-%3-%4.png").arg(QString::fromLocal8Bit(dump), name, theme).arg(size));
|
||||
const QColor mean = meanVisibleColor(pixmap.toImage());
|
||||
QVERIFY2(mean.isValid(), qPrintable(QString("%1 in %2 at %3 px is empty").arg(name, theme).arg(size)));
|
||||
if (theme == "qet")
|
||||
QVERIFY2(mean.lightnessF() < 0.5, qPrintable(QString("%1 light theme at %2 px: lightness %3").arg(name).arg(size).arg(mean.lightnessF())));
|
||||
else
|
||||
QVERIFY2(mean.lightnessF() > 0.6, qPrintable(QString("%1 dark theme at %2 px: lightness %3").arg(name).arg(size).arg(mean.lightnessF())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void tst_qeticons::toolbarIconIsReadable_data()
|
||||
{
|
||||
QTest::addColumn<QString>("theme");
|
||||
@@ -178,6 +298,175 @@ void tst_qeticons::toolbarIconIsReadable()
|
||||
.arg(disabled_contrast).arg(enabled_contrast)));
|
||||
}
|
||||
|
||||
namespace {
|
||||
/**
|
||||
Paint a tool button through the style with the given extra states,
|
||||
the way QToolButton::paintEvent does, so hover can be rendered
|
||||
without a real pointer.
|
||||
*/
|
||||
QImage renderToolButton(QToolButton *button, QStyle::State extra, const char *name)
|
||||
{
|
||||
QStyleOptionToolButton option;
|
||||
option.initFrom(button);
|
||||
option.state |= QStyle::State_Enabled | QStyle::State_AutoRaise | QStyle::State_Raised | extra;
|
||||
option.icon = button->icon();
|
||||
option.iconSize = button->iconSize();
|
||||
option.subControls = QStyle::SC_ToolButton;
|
||||
option.features = QStyleOptionToolButton::None;
|
||||
option.toolButtonStyle = Qt::ToolButtonIconOnly;
|
||||
|
||||
QImage image(button->size(), QImage::Format_ARGB32_Premultiplied);
|
||||
image.fill(option.palette.color(QPalette::Window));
|
||||
QPainter painter(&image);
|
||||
button->style()->drawComplexControl(QStyle::CC_ToolButton, &option, &painter, button);
|
||||
painter.end();
|
||||
return QET::Test::dump(image, name);
|
||||
}
|
||||
|
||||
QToolButton *toolButton(QMainWindow &window, const QIcon &icon)
|
||||
{
|
||||
QToolBar *toolbar = window.addToolBar("view");
|
||||
auto *button = new QToolButton;
|
||||
button->setIcon(icon);
|
||||
button->setAutoRaise(true);
|
||||
toolbar->addWidget(button);
|
||||
window.resize(300, 100);
|
||||
window.show();
|
||||
return button;
|
||||
}
|
||||
}
|
||||
|
||||
void tst_qeticons::hoverChangesTheIcon_data()
|
||||
{
|
||||
QTest::addColumn<QString>("theme");
|
||||
QTest::addColumn<QPalette>("palette");
|
||||
QTest::addColumn<bool>("checked");
|
||||
QTest::newRow("light") << "qet" << QET::Palette::fusionLight() << false;
|
||||
QTest::newRow("dark") << "qet-dark" << QET::Palette::fusionDark() << false;
|
||||
// A toggle that is on: Fusion draws it sunken, and hovering it changes
|
||||
// nothing in the frame, so the icon is the only hover signal there.
|
||||
QTest::newRow("light-checked") << "qet" << QET::Palette::fusionLight() << true;
|
||||
QTest::newRow("dark-checked") << "qet-dark" << QET::Palette::fusionDark() << true;
|
||||
// A platform accent QET keeps because its selection text reads on
|
||||
// it: macOS's green selection color comes with black text. Lightening
|
||||
// it toward 3:1 against a white face can only end at white, which
|
||||
// made every hovered line-art icon vanish on a light palette.
|
||||
QTest::newRow("light-pale-accent") << "qet" << withAccent(QET::Palette::fusionLight(), QColor(198, 231, 188), Qt::black) << false;
|
||||
QTest::newRow("dark-pale-accent") << "qet-dark" << withAccent(QET::Palette::fusionDark(), QColor(198, 231, 188), Qt::black) << false;
|
||||
}
|
||||
|
||||
/**
|
||||
Hovering a line-art icon must change the icon itself, not only the
|
||||
button frame, and the hovered icon must still reach 3:1 (GitHub #870).
|
||||
QETStyle answers QIcon::Active with the icon tinted in the highlight
|
||||
color; the built-in styles return it unchanged.
|
||||
*/
|
||||
void tst_qeticons::hoverChangesTheIcon()
|
||||
{
|
||||
QFETCH(QString, theme);
|
||||
QFETCH(QPalette, palette);
|
||||
QFETCH(bool, checked);
|
||||
QIcon::setThemeName(theme);
|
||||
QApplication::setStyle(new QETStyle(QStyleFactory::create("Fusion")));
|
||||
QApplication::setPalette(palette);
|
||||
|
||||
QMainWindow window;
|
||||
QToolButton *button = toolButton(window, QIcon::fromTheme("zoom-fit-best"));
|
||||
button->setCheckable(checked);
|
||||
button->setChecked(checked);
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&window));
|
||||
|
||||
const QStyle::State on = checked ? (QStyle::State_On | QStyle::State_Sunken) : QStyle::State_None;
|
||||
const QImage normal = renderToolButton(button, on, "normal");
|
||||
const QImage hover = renderToolButton(button, on | QStyle::State_MouseOver, "hover");
|
||||
// Inside the frame Fusion draws around a hovered button, so the ink
|
||||
// measured is the icon's, not the frame's.
|
||||
const QRect inside = button->rect().adjusted(3, 3, -3, -3);
|
||||
const QColor normal_ink(QET::Test::ink(normal, inside));
|
||||
const QColor hover_ink(QET::Test::ink(hover, inside));
|
||||
QVERIFY2(hover_ink != normal_ink,
|
||||
qPrintable(QString("hover leaves the icon ink at %1").arg(normal_ink.name())));
|
||||
|
||||
// The tint, give or take the icon's own anti-aliasing.
|
||||
const QColor expected = QETStyle::hoverColor(palette);
|
||||
const int distance = qMax(qMax(qAbs(hover_ink.red() - expected.red()),
|
||||
qAbs(hover_ink.green() - expected.green())),
|
||||
qAbs(hover_ink.blue() - expected.blue()));
|
||||
QVERIFY2(distance <= 16,
|
||||
qPrintable(QString("hovered ink %1, expected %2").arg(hover_ink.name(), expected.name())));
|
||||
|
||||
const double hover_contrast = QET::Test::inkContrast(hover, inside);
|
||||
QVERIFY2(hover_contrast >= kIconRatio,
|
||||
qPrintable(QString("hovered icon: %1").arg(hover_contrast)));
|
||||
}
|
||||
|
||||
/**
|
||||
A colored icon (old Oxygen art) is not line art, so hover leaves its
|
||||
pixels alone.
|
||||
*/
|
||||
void tst_qeticons::coloredIconKeepsItsColorsOnHover()
|
||||
{
|
||||
QIcon::setThemeName(QStringLiteral("qet"));
|
||||
QApplication::setStyle(new QETStyle(QStyleFactory::create("Fusion")));
|
||||
QApplication::setPalette(QET::Palette::fusionLight());
|
||||
|
||||
const QIcon icon = QIcon::fromTheme("document-open"); // colored Oxygen art
|
||||
QVERIFY(!icon.isNull());
|
||||
const QPixmap normal = icon.pixmap(QSize(22, 22), 1.0, QIcon::Normal);
|
||||
const QPixmap active = icon.pixmap(QSize(22, 22), 1.0, QIcon::Active);
|
||||
QVERIFY(!QETStyle::isLineArt(normal.toImage()));
|
||||
QCOMPARE(active.toImage(), normal.toImage());
|
||||
}
|
||||
|
||||
void tst_qeticons::menuIconReadsOnHighlight_data()
|
||||
{
|
||||
toolbarIconIsReadable_data();
|
||||
}
|
||||
|
||||
/**
|
||||
Fusion asks for the QIcon::Active pixmap of a highlighted menu item's
|
||||
icon too, and paints it on the highlight bar. The hover tint would
|
||||
vanish there, so on a menu item the icon must read against the bar at
|
||||
3:1 instead: the same ink as the item's text.
|
||||
*/
|
||||
void tst_qeticons::menuIconReadsOnHighlight()
|
||||
{
|
||||
QFETCH(QString, theme);
|
||||
QFETCH(QPalette, palette);
|
||||
QIcon::setThemeName(theme);
|
||||
QApplication::setStyle(new QETStyle(QStyleFactory::create("Fusion")));
|
||||
QApplication::setPalette(palette);
|
||||
|
||||
QMenu menu;
|
||||
QAction *action = menu.addAction(QIcon::fromTheme("zoom-fit-best"), "Zoom to fit");
|
||||
menu.resize(160, 32);
|
||||
|
||||
QStyleOptionMenuItem option;
|
||||
option.initFrom(&menu);
|
||||
option.state |= QStyle::State_Enabled | QStyle::State_Selected;
|
||||
option.menuItemType = QStyleOptionMenuItem::Normal;
|
||||
option.checkType = QStyleOptionMenuItem::NotCheckable;
|
||||
option.icon = action->icon();
|
||||
option.text = action->text();
|
||||
option.maxIconWidth = 22;
|
||||
option.rect = QRect(0, 0, 160, 24);
|
||||
|
||||
QImage image(160, 24, QImage::Format_ARGB32_Premultiplied);
|
||||
image.fill(palette.color(QPalette::Window));
|
||||
QPainter painter(&image);
|
||||
menu.style()->drawControl(QStyle::CE_MenuItem, &option, &painter, &menu);
|
||||
painter.end();
|
||||
QET::Test::dump(image, "menu-highlight");
|
||||
|
||||
// The icon column: the first maxIconWidth pixels plus Fusion's margin.
|
||||
const QRect icon_column(0, 0, 30, 24);
|
||||
const QColor bar(QET::Test::background(image, icon_column));
|
||||
QCOMPARE(bar, palette.color(QPalette::Active, QPalette::Highlight));
|
||||
const double contrast = QET::Test::inkContrast(image, icon_column);
|
||||
QVERIFY2(contrast >= kIconRatio,
|
||||
qPrintable(QString("highlighted menu icon: %1").arg(contrast)));
|
||||
}
|
||||
|
||||
/**
|
||||
The elements panel sets a 50 px icon size so element previews are
|
||||
large. Its project root item uses ProjectFileGP, whose theme name also
|
||||
@@ -203,6 +492,65 @@ void tst_qeticons::panelProjectIconStaysSmall()
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The configuration dialogs list their pages with 64 or 128 pixel icons
|
||||
(ConfigDialog::ConfigDialog). A page whose icon exists at 22 pixels
|
||||
only comes out as a 22 pixel stamp among 128 pixel neighbors, which
|
||||
the terminal-strip page of the project dialog and the shortcuts page
|
||||
of the settings dialog did.
|
||||
*/
|
||||
void tst_qeticons::configPageIconsComeAtPageSize()
|
||||
{
|
||||
const QStringList pages = {"settings", "project", "diagram", "plasmagik", "printer",
|
||||
"document-export", "terminalstrip", "configure-shortcuts"};
|
||||
QStringList small;
|
||||
for (const QString &theme : {"qet", "qet-dark"})
|
||||
{
|
||||
QIcon::setThemeName(theme);
|
||||
for (const QString &name : pages)
|
||||
{
|
||||
const QIcon icon = QIcon::fromTheme(name);
|
||||
QVERIFY2(!icon.isNull(), qPrintable(QString("%1 missing in theme %2").arg(name, theme)));
|
||||
const QSize size = icon.actualSize(QSize(128, 128));
|
||||
if (size.width() < 128 || size.height() < 128)
|
||||
small << QString("%1 in %2 at %3x%4").arg(name, theme).arg(size.width()).arg(size.height());
|
||||
}
|
||||
}
|
||||
QIcon::setThemeName("qet");
|
||||
QVERIFY2(small.isEmpty(), qPrintable("page icons short of 128 pixels: " + small.join(", ")));
|
||||
}
|
||||
|
||||
/**
|
||||
Whatever accent the platform hands QET, the hover ink must read on the
|
||||
hovered button face of both palettes: a sweep over hues at every
|
||||
lightness, including the pale and the near-black ones.
|
||||
*/
|
||||
void tst_qeticons::hoverInkReadsWithAnyAccent_data()
|
||||
{
|
||||
QTest::addColumn<QPalette>("palette");
|
||||
for (int lightness = 10; lightness <= 250; lightness += 40)
|
||||
for (int hue = 0; hue < 360; hue += 60)
|
||||
{
|
||||
const QColor accent = QColor::fromHsl(hue, 200, lightness);
|
||||
const QColor text = QET::Palette::contrastRatio(accent, Qt::white) >= 4.5 ? Qt::white : Qt::black;
|
||||
QTest::newRow(qPrintable(QString("light-%1").arg(accent.name())))
|
||||
<< withAccent(QET::Palette::fusionLight(), accent, text);
|
||||
QTest::newRow(qPrintable(QString("dark-%1").arg(accent.name())))
|
||||
<< withAccent(QET::Palette::fusionDark(), accent, text);
|
||||
}
|
||||
}
|
||||
|
||||
void tst_qeticons::hoverInkReadsWithAnyAccent()
|
||||
{
|
||||
QFETCH(QPalette, palette);
|
||||
const QColor face = palette.color(QPalette::Active, QPalette::Light);
|
||||
const QColor ink = QETStyle::hoverColor(palette);
|
||||
const double contrast = QET::Palette::contrastRatio(ink, face);
|
||||
QVERIFY2(contrast >= kIconRatio,
|
||||
qPrintable(QString("hover ink %1 reads %2:1 on the face %3").arg(ink.name()).arg(contrast).arg(face.name())));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM"))
|
||||
|
||||
@@ -19,16 +19,27 @@
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QGraphicsItem>
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsView>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMainWindow>
|
||||
#include <QPushButton>
|
||||
#include <QPainter>
|
||||
#include <QRadioButton>
|
||||
#include <QScreen>
|
||||
#include <QStandardItemModel>
|
||||
#include <QTreeView>
|
||||
#include <QStyleFactory>
|
||||
#include <QTabWidget>
|
||||
#include <QToolBar>
|
||||
#include <QToolButton>
|
||||
|
||||
#include "inkcontrast.h"
|
||||
#include "ElementsCollection/elementpreviewdelegate.h"
|
||||
#include "palettegraphicsview.h"
|
||||
#include "qetpalette.h"
|
||||
|
||||
using QET::Palette::contrastRatio;
|
||||
@@ -62,6 +73,24 @@ class tst_qetpalette : public QObject
|
||||
void styleIsFusionMatchesObjectName();
|
||||
void renderedWidgetsAreReadable_data();
|
||||
void renderedWidgetsAreReadable();
|
||||
void lineArtRuleSeparatesInkFromColor();
|
||||
void invertedLightnessKeepsHueAndAlpha();
|
||||
void elementPreviewReadsOnBothPalettes();
|
||||
void previewDelegateAdaptsLineArtOnly();
|
||||
void invertLightnessMapsSheetAndInk();
|
||||
void invertedViewReadsOnDarkSheet();
|
||||
void invertLightnessSpeed();
|
||||
void gridDotColorSoftensInvertedDots();
|
||||
void paletteViewFollowsThePalette();
|
||||
void paletteViewKeepsSceneUpdatesFlowing();
|
||||
void paletteViewDrawsTheRubberBand();
|
||||
void paletteViewFollowsTheApplicationUnderAStyleSheet();
|
||||
void paletteViewFillsWhatTheSceneLeavesBlank();
|
||||
void paletteViewErasesMovedChildren_data();
|
||||
void paletteViewErasesMovedChildren();
|
||||
void paletteViewErasesChildrenMovedWhilePainting_data();
|
||||
void paletteViewErasesChildrenMovedWhilePainting();
|
||||
void styleSheetWidgetsFollowPaletteChange();
|
||||
|
||||
private:
|
||||
static void addPaletteRows();
|
||||
@@ -308,6 +337,686 @@ void tst_qetpalette::renderedWidgetsAreReadable()
|
||||
qPrintable(QString("line edit text: %1").arg(edit_contrast)));
|
||||
}
|
||||
|
||||
namespace {
|
||||
/// A 40 x 40 transparent picture with a 3 px stroke square in color, as
|
||||
/// an element preview is drawn for the white sheet.
|
||||
QImage strokeSquare(const QColor &color)
|
||||
{
|
||||
QImage image(40, 40, QImage::Format_ARGB32);
|
||||
image.fill(Qt::transparent);
|
||||
QPainter painter(&image);
|
||||
painter.setPen(QPen(color, 3));
|
||||
painter.drawRect(6, 6, 27, 27);
|
||||
return image;
|
||||
}
|
||||
|
||||
/// The most frequent color of a rendering: its background.
|
||||
QRgb dominant(const QImage &image)
|
||||
{
|
||||
QHash<QRgb, int> histogram;
|
||||
for (int y = 0; y < image.height(); ++y)
|
||||
for (int x = 0; x < image.width(); ++x)
|
||||
++histogram[image.pixel(x, y)];
|
||||
QRgb best = 0;
|
||||
int count = -1;
|
||||
for (auto it = histogram.cbegin(); it != histogram.cend(); ++it)
|
||||
if (it.value() > count) { count = it.value(); best = it.key(); }
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
void tst_qetpalette::lineArtRuleSeparatesInkFromColor()
|
||||
{
|
||||
QVERIFY(QET::Palette::isLineArt(strokeSquare(Qt::black)));
|
||||
QVERIFY(QET::Palette::isLineArt(strokeSquare(QColor(80, 80, 80))));
|
||||
QVERIFY(!QET::Palette::isLineArt(strokeSquare(Qt::red)));
|
||||
QVERIFY(!QET::Palette::isLineArt(strokeSquare(QColor(30, 96, 176))));
|
||||
QVERIFY(!QET::Palette::isLineArt(QImage()));
|
||||
}
|
||||
|
||||
/**
|
||||
Black ink becomes the dark palette's light gray, a colored stroke keeps
|
||||
its hue, and transparency is untouched.
|
||||
*/
|
||||
void tst_qetpalette::invertedLightnessKeepsHueAndAlpha()
|
||||
{
|
||||
const QImage black = QET::Palette::invertedLightness(strokeSquare(Qt::black));
|
||||
QCOMPARE(black.pixelColor(6, 20).alpha(), 255);
|
||||
QVERIFY2(black.pixelColor(6, 20).lightnessF() > 0.8, "black ink did not become light");
|
||||
QCOMPARE(black.pixelColor(20, 20).alpha(), 0);
|
||||
|
||||
const QImage red = QET::Palette::invertedLightness(strokeSquare(Qt::red));
|
||||
const QColor stroke = red.pixelColor(6, 20);
|
||||
QVERIFY2(qAbs(stroke.hslHueF() - QColor(Qt::red).hslHueF()) < 0.02, "hue changed");
|
||||
QVERIFY(stroke.hslSaturationF() > 0.9);
|
||||
}
|
||||
|
||||
/**
|
||||
An element preview drawn for the white sheet must read at 3:1 on the
|
||||
Base color of both palettes: unchanged on the light one, inverted on
|
||||
the dark one.
|
||||
*/
|
||||
void tst_qetpalette::elementPreviewReadsOnBothPalettes()
|
||||
{
|
||||
const QPixmap preview = QPixmap::fromImage(strokeSquare(Qt::black));
|
||||
for (const QPalette &palette : {QET::Palette::fusionLight(), QET::Palette::fusionDark()})
|
||||
{
|
||||
const QColor base = palette.color(QPalette::Active, QPalette::Base);
|
||||
const QPixmap shown = QET::Palette::forPalette(preview, palette);
|
||||
QImage row(shown.size(), QImage::Format_ARGB32);
|
||||
row.fill(base);
|
||||
QPainter painter(&row);
|
||||
painter.drawPixmap(0, 0, shown);
|
||||
painter.end();
|
||||
const double contrast = QET::Test::inkContrast(row, row.rect());
|
||||
QVERIFY2(contrast >= 3.0, qPrintable(QString("preview reads %1:1 on Base %2").arg(contrast).arg(base.name())));
|
||||
}
|
||||
// A light palette hands the picture back untouched.
|
||||
QCOMPARE(QET::Palette::forPalette(preview, QET::Palette::fusionLight()).cacheKey(), preview.cacheKey());
|
||||
}
|
||||
|
||||
/**
|
||||
In a tree on the dark palette, the delegate inverts a line-art icon so
|
||||
it reads on the row, and leaves a colored icon (a folder) as it is.
|
||||
*/
|
||||
void tst_qetpalette::previewDelegateAdaptsLineArtOnly()
|
||||
{
|
||||
QApplication::setStyle(QStyleFactory::create("Fusion"));
|
||||
QApplication::setPalette(QET::Palette::fusionDark());
|
||||
|
||||
QStandardItemModel model;
|
||||
auto *element = new QStandardItem(QIcon(QPixmap::fromImage(strokeSquare(Qt::black))), "element");
|
||||
auto *folder = new QStandardItem(QIcon(QPixmap::fromImage(strokeSquare(QColor(30, 96, 176)))), "folder");
|
||||
model.appendRow(element);
|
||||
model.appendRow(folder);
|
||||
|
||||
QTreeView view;
|
||||
view.setModel(&model);
|
||||
view.setIconSize(QSize(40, 40));
|
||||
view.setItemDelegate(new ElementPreviewDelegate(&view));
|
||||
view.resize(300, 200);
|
||||
view.show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&view));
|
||||
|
||||
const QImage image = view.viewport()->grab().toImage();
|
||||
const QRect element_icon(view.visualRect(element->index()).topLeft(), QSize(40, 40));
|
||||
const QRect folder_icon(view.visualRect(folder->index()).topLeft(), QSize(40, 40));
|
||||
const QColor base = QET::Palette::fusionDark().color(QPalette::Active, QPalette::Base);
|
||||
QCOMPARE(QColor(dominant(image)), base);
|
||||
|
||||
const double element_contrast = QET::Test::inkContrast(image, element_icon);
|
||||
QVERIFY2(element_contrast >= 3.0, qPrintable(QString("element preview reads %1:1 on the dark row").arg(element_contrast)));
|
||||
|
||||
// The folder icon keeps its blue: some pixel in its slot is still saturated blue.
|
||||
bool blue = false;
|
||||
for (int y = folder_icon.top(); y <= folder_icon.bottom() && !blue; ++y)
|
||||
for (int x = folder_icon.left(); x <= folder_icon.right() && !blue; ++x)
|
||||
{
|
||||
const QColor c = image.pixelColor(x, y);
|
||||
blue = c.hslSaturationF() > 0.5 && c.blue() > c.red() + 60;
|
||||
}
|
||||
QVERIFY2(blue, "the colored icon lost its color");
|
||||
}
|
||||
|
||||
/**
|
||||
The lightness inversion sends white to the sheet color and black to
|
||||
the ink color, lands a mid gray between the two, and keeps the hue of
|
||||
a colored line. Without sheet and ink it is a plain inversion.
|
||||
*/
|
||||
void tst_qetpalette::invertLightnessMapsSheetAndInk()
|
||||
{
|
||||
const QColor sheet(30, 30, 30);
|
||||
const QColor ink(220, 220, 220);
|
||||
QImage image(4, 1, QImage::Format_ARGB32);
|
||||
image.setPixelColor(0, 0, Qt::white);
|
||||
image.setPixelColor(1, 0, Qt::black);
|
||||
image.setPixelColor(2, 0, QColor(128, 128, 128));
|
||||
image.setPixelColor(3, 0, QColor(200, 0, 0));
|
||||
QImage plain = image;
|
||||
|
||||
QET::Palette::invertLightness(image, sheet, ink);
|
||||
QCOMPARE(image.format(), QImage::Format_RGB32);
|
||||
QCOMPARE(image.pixelColor(0, 0), sheet);
|
||||
QCOMPARE(image.pixelColor(1, 0), ink);
|
||||
const int middle = (sheet.red() + ink.red()) / 2;
|
||||
QVERIFY(qAbs(image.pixelColor(2, 0).red() - middle) <= 2);
|
||||
const QColor red = image.pixelColor(3, 0);
|
||||
QCOMPARE(red.hslHue(), 0);
|
||||
QVERIFY2(red.hslSaturationF() > 0.5, qPrintable(red.name()));
|
||||
QVERIFY2(red.lightness() > QColor(200, 0, 0).lightness(), qPrintable(red.name()));
|
||||
|
||||
QET::Palette::invertLightness(plain);
|
||||
QCOMPARE(plain.pixelColor(0, 0), QColor(Qt::black));
|
||||
QCOMPARE(plain.pixelColor(1, 0), QColor(Qt::white));
|
||||
QCOMPARE(plain.pixelColor(3, 0), QColor(255, 55, 55));
|
||||
}
|
||||
|
||||
/**
|
||||
A view rendered the way DiagramView does it on a dark palette: a part
|
||||
of the viewport goes into an image, which is inverted between the
|
||||
palette's Base and Text. The white sheet comes out as Base, black
|
||||
lines read at text contrast, and a blue box is still blue.
|
||||
*/
|
||||
void tst_qetpalette::invertedViewReadsOnDarkSheet()
|
||||
{
|
||||
QGraphicsScene scene(0, 0, 200, 120);
|
||||
scene.setBackgroundBrush(Qt::white);
|
||||
scene.addLine(10, 60, 190, 60, QPen(Qt::black, 2));
|
||||
scene.addRect(20, 20, 40, 20, QPen(Qt::NoPen), QBrush(QColor(30, 96, 176)));
|
||||
|
||||
QGraphicsView view(&scene);
|
||||
view.setFrameShape(QFrame::NoFrame);
|
||||
view.setAlignment(Qt::AlignLeft | Qt::AlignTop);
|
||||
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
view.resize(200, 120);
|
||||
view.show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&view));
|
||||
|
||||
// A part of the viewport that is not at its origin, as a partial
|
||||
// repaint after a scroll would be.
|
||||
const QRect area(10, 10, 100, 60);
|
||||
QImage buffer(area.size(), QImage::Format_RGB32);
|
||||
QPainter painter(&buffer);
|
||||
view.render(&painter, QRectF(QPointF(0, 0), QSizeF(area.size())), area);
|
||||
painter.end();
|
||||
|
||||
const QPalette dark = QET::Palette::fusionDark();
|
||||
const QColor base = dark.color(QPalette::Active, QPalette::Base);
|
||||
const QColor text = dark.color(QPalette::Active, QPalette::Text);
|
||||
QET::Palette::invertLightness(buffer, base, text);
|
||||
|
||||
QHash<QRgb, int> histogram;
|
||||
for (int y = 0; y < buffer.height(); ++y)
|
||||
for (int x = 0; x < buffer.width(); ++x)
|
||||
++histogram[buffer.pixel(x, y)];
|
||||
QRgb dominant = 0;
|
||||
for (auto it = histogram.cbegin(); it != histogram.cend(); ++it)
|
||||
if (it.value() > histogram.value(dominant)) dominant = it.key();
|
||||
QCOMPARE(QColor(dominant), base);
|
||||
|
||||
const double contrast = inkContrast(buffer, buffer.rect());
|
||||
QVERIFY2(contrast >= QET::Palette::contrastRatio(base, text) - 0.5,
|
||||
qPrintable(QString("ink reads %1:1 on the dark sheet").arg(contrast)));
|
||||
|
||||
// The box at scene (20..60, 20..40) sits at (10..50, 10..30) in the buffer.
|
||||
const QColor box = buffer.pixelColor(30, 20);
|
||||
QVERIFY2(box.hslSaturationF() > 0.3 && box.blue() > box.red() + 60,
|
||||
qPrintable(QString("the blue box became %1").arg(box.name())));
|
||||
}
|
||||
|
||||
namespace {
|
||||
/**
|
||||
A PaletteGraphicsView that counts its paints and records the
|
||||
paintingInverted() calls it receives.
|
||||
*/
|
||||
class ProbeView : public PaletteGraphicsView
|
||||
{
|
||||
public:
|
||||
int paints = 0;
|
||||
QList<QRect> paint_rects;
|
||||
QList<bool> inverted_calls;
|
||||
using PaletteGraphicsView::PaletteGraphicsView;
|
||||
/// True when a paint since index \a from covered the whole viewport.
|
||||
bool fullyRepaintedSince(int from) const
|
||||
{
|
||||
for (int i = from; i < paint_rects.size(); ++i)
|
||||
if (paint_rects.at(i).contains(viewport()->rect()))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override
|
||||
{
|
||||
++paints;
|
||||
paint_rects << event->rect();
|
||||
PaletteGraphicsView::paintEvent(event);
|
||||
}
|
||||
void paintingInverted(bool inverted) override
|
||||
{
|
||||
inverted_calls << inverted;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
A parent that, like a Terminal with its help lines, gives its child
|
||||
line a new geometry from inside paint(): the line always runs
|
||||
across the sheet at the parent's height, whatever the parent's
|
||||
position. Where the child is painted is therefore only known once
|
||||
the parent has been painted.
|
||||
*/
|
||||
class PaintTimeHelpLine : public QGraphicsRectItem
|
||||
{
|
||||
public:
|
||||
explicit PaintTimeHelpLine(const QRectF &sheet) :
|
||||
QGraphicsRectItem(0, 0, 20, 20),
|
||||
m_sheet(sheet),
|
||||
m_line(new QGraphicsLineItem(this))
|
||||
{
|
||||
setPen(Qt::NoPen);
|
||||
setBrush(Qt::black);
|
||||
m_line->setPen(QPen(Qt::black, 2));
|
||||
}
|
||||
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override
|
||||
{
|
||||
QGraphicsRectItem::paint(painter, option, widget);
|
||||
const qreal y = scenePos().y() + 10;
|
||||
m_line->setLine(QLineF(m_line->mapFromScene(QPointF(m_sheet.left(), y)),
|
||||
m_line->mapFromScene(QPointF(m_sheet.right(), y))));
|
||||
}
|
||||
private:
|
||||
QRectF m_sheet;
|
||||
QGraphicsLineItem *m_line;
|
||||
};
|
||||
|
||||
/**
|
||||
A small folio: a white sheet with a black line and a red box.
|
||||
Returns the box, which is selectable.
|
||||
*/
|
||||
QGraphicsRectItem *fillSheet(QGraphicsScene &scene)
|
||||
{
|
||||
scene.setSceneRect(0, 0, 200, 120);
|
||||
scene.setBackgroundBrush(Qt::white);
|
||||
scene.addLine(10, 60, 190, 60, QPen(Qt::black, 2));
|
||||
QGraphicsRectItem *box = scene.addRect(20, 20, 40, 20, QPen(Qt::NoPen), QBrush(QColor(200, 0, 0)));
|
||||
box->setFlag(QGraphicsItem::ItemIsSelectable);
|
||||
return box;
|
||||
}
|
||||
|
||||
/// The view sized to its scene, without frame or scroll bars.
|
||||
void showAsSheet(QGraphicsView &view)
|
||||
{
|
||||
view.setFrameShape(QFrame::NoFrame);
|
||||
view.setAlignment(Qt::AlignLeft | Qt::AlignTop);
|
||||
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
view.resize(200, 120);
|
||||
view.show();
|
||||
}
|
||||
|
||||
/// The most frequent color of an image: the sheet.
|
||||
QColor sheetColor(const QImage &image)
|
||||
{
|
||||
QHash<QRgb, int> histogram;
|
||||
for (int y = 0; y < image.height(); ++y)
|
||||
for (int x = 0; x < image.width(); ++x)
|
||||
++histogram[image.pixel(x, y)];
|
||||
QRgb best = 0;
|
||||
for (auto it = histogram.cbegin(); it != histogram.cend(); ++it)
|
||||
if (it.value() > histogram.value(best)) best = it.key();
|
||||
return QColor(best);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Grid dots are black, white on a black sheet, and a third of the way
|
||||
from the sheet color to black when the sheet is about to be shown
|
||||
inverted, so that they do not come out as bright as the ink.
|
||||
*/
|
||||
void tst_qetpalette::gridDotColorSoftensInvertedDots()
|
||||
{
|
||||
QCOMPARE(QET::Palette::gridDotColor(Qt::white, false), QColor(Qt::black));
|
||||
QCOMPARE(QET::Palette::gridDotColor(Qt::white, true), QColor(170, 170, 170));
|
||||
QCOMPARE(QET::Palette::gridDotColor(Qt::darkGray, true), QColor(85, 85, 85));
|
||||
QCOMPARE(QET::Palette::gridDotColor(Qt::black, false), QColor(Qt::white));
|
||||
QCOMPARE(QET::Palette::gridDotColor(Qt::black, true), QColor(Qt::white));
|
||||
}
|
||||
|
||||
/**
|
||||
On a light palette the view shows the sheet as drawn and never tells
|
||||
the scene it inverts. On a dark palette, set while the view is
|
||||
showing, the sheet comes out as Base, the black line at text contrast,
|
||||
the red box still red, and the scene hears paintingInverted(true)
|
||||
before and (false) after. Back on a light palette the sheet is white
|
||||
again.
|
||||
*/
|
||||
void tst_qetpalette::paletteViewFollowsThePalette()
|
||||
{
|
||||
QApplication::setStyle(QStyleFactory::create("Fusion"));
|
||||
QApplication::setPalette(QET::Palette::fusionLight());
|
||||
|
||||
QGraphicsScene scene;
|
||||
fillSheet(scene);
|
||||
ProbeView view(&scene);
|
||||
showAsSheet(view);
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&view));
|
||||
|
||||
const QImage light = view.viewport()->grab().toImage();
|
||||
QCOMPARE(sheetColor(light), QColor(Qt::white));
|
||||
QVERIFY(view.inverted_calls.isEmpty());
|
||||
|
||||
QApplication::setPalette(QET::Palette::fusionDark());
|
||||
QTRY_VERIFY(view.invertsLightness());
|
||||
const QImage dark = view.viewport()->grab().toImage();
|
||||
const QColor base = QET::Palette::fusionDark().color(QPalette::Active, QPalette::Base);
|
||||
const QColor text = QET::Palette::fusionDark().color(QPalette::Active, QPalette::Text);
|
||||
QCOMPARE(sheetColor(dark), base);
|
||||
const double contrast = inkContrast(dark, dark.rect());
|
||||
QVERIFY2(contrast >= QET::Palette::contrastRatio(base, text) - 0.5,
|
||||
qPrintable(QString("ink reads %1:1 on the dark sheet").arg(contrast)));
|
||||
const QColor box = dark.pixelColor(40, 30);
|
||||
QVERIFY2(box.hslHue() == 0 && box.hslSaturationF() > 0.3 && box.red() > box.blue() + 60,
|
||||
qPrintable(QString("the red box became %1").arg(box.name())));
|
||||
QVERIFY(!view.inverted_calls.isEmpty());
|
||||
QCOMPARE(view.inverted_calls.first(), true);
|
||||
QCOMPARE(view.inverted_calls.last(), false);
|
||||
QCOMPARE(view.inverted_calls.count(true), view.inverted_calls.count(false));
|
||||
|
||||
QApplication::setPalette(QET::Palette::fusionLight());
|
||||
QTRY_VERIFY(!view.invertsLightness());
|
||||
QCOMPARE(sheetColor(view.viewport()->grab().toImage()), QColor(Qt::white));
|
||||
}
|
||||
|
||||
/**
|
||||
QGraphicsView clears the scene's "update everything" flag only when it
|
||||
paints the items straight onto its viewport, and while the flag is set
|
||||
every further QGraphicsScene::update() and item update is dropped. On
|
||||
a dark palette the view paints through render() instead, so it listens
|
||||
to the scene's changed() signal, which makes the scene clear the flag
|
||||
before it emits. Three whole-scene updates and a selection must each
|
||||
repaint the view, with the scene set after construction as
|
||||
DiagramView does it.
|
||||
*/
|
||||
void tst_qetpalette::paletteViewKeepsSceneUpdatesFlowing()
|
||||
{
|
||||
QApplication::setStyle(QStyleFactory::create("Fusion"));
|
||||
QApplication::setPalette(QET::Palette::fusionDark());
|
||||
|
||||
QGraphicsScene scene;
|
||||
QGraphicsRectItem *box = fillSheet(scene);
|
||||
ProbeView view;
|
||||
view.setScene(&scene);
|
||||
showAsSheet(view);
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&view));
|
||||
QTRY_VERIFY(view.paints >= 1);
|
||||
QVERIFY(view.invertsLightness());
|
||||
|
||||
for (int round = 1; round <= 3; ++round)
|
||||
{
|
||||
const int before = view.paints;
|
||||
scene.update();
|
||||
QTRY_VERIFY2(view.paints > before, qPrintable(QString("scene update %1 was dropped").arg(round)));
|
||||
}
|
||||
|
||||
const int before = view.paints;
|
||||
box->setSelected(true);
|
||||
QTRY_VERIFY2(view.paints > before, "the selection change was dropped");
|
||||
}
|
||||
|
||||
/**
|
||||
render() skips Qt's selection rubber band, so the view draws it after
|
||||
the inversion: while a drag on the sheet is in progress, the dragged
|
||||
area no longer shows the bare sheet.
|
||||
*/
|
||||
void tst_qetpalette::paletteViewDrawsTheRubberBand()
|
||||
{
|
||||
QApplication::setStyle(QStyleFactory::create("Fusion"));
|
||||
QApplication::setPalette(QET::Palette::fusionDark());
|
||||
|
||||
QGraphicsScene scene;
|
||||
fillSheet(scene);
|
||||
ProbeView view(&scene);
|
||||
view.setDragMode(QGraphicsView::RubberBandDrag);
|
||||
showAsSheet(view);
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&view));
|
||||
const QColor base = QET::Palette::fusionDark().color(QPalette::Active, QPalette::Base);
|
||||
QCOMPARE(view.viewport()->grab().toImage().pixelColor(170, 100), base);
|
||||
|
||||
QTest::mousePress(view.viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(120, 80));
|
||||
QTest::mouseMove(view.viewport(), QPoint(190, 115));
|
||||
QTRY_VERIFY(!view.rubberBandRect().isNull());
|
||||
const QColor inside = view.viewport()->grab().toImage().pixelColor(170, 100);
|
||||
QVERIFY2(inside != base, qPrintable(QString("no rubber band drawn, pixel is %1").arg(inside.name())));
|
||||
QTest::mouseRelease(view.viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(190, 115));
|
||||
}
|
||||
|
||||
/**
|
||||
A style sheet on an ancestor pins the palette of every widget under it
|
||||
to the application palette in force when the sheet was applied
|
||||
(QStyleSheetStyle keeps its own copy; the folio tab widget carries
|
||||
such a sheet). The view's own palette() is therefore stale after a
|
||||
live switch, and the view must follow the application palette
|
||||
instead: inside a tab widget with a style sheet, switching the
|
||||
application to dark and back still changes the sheet. Qt also skips
|
||||
the repaint of a widget whose palette did not change, so the view
|
||||
has to repaint its whole viewport by itself on each switch, without
|
||||
anyone asking for a rendering: otherwise the area around the sheet
|
||||
keeps the old colors.
|
||||
*/
|
||||
void tst_qetpalette::paletteViewFollowsTheApplicationUnderAStyleSheet()
|
||||
{
|
||||
QApplication::setStyle(QStyleFactory::create("Fusion"));
|
||||
QApplication::setPalette(QET::Palette::fusionLight());
|
||||
|
||||
QGraphicsScene scene;
|
||||
fillSheet(scene);
|
||||
QTabWidget tabs;
|
||||
tabs.setStyleSheet("QTabBar::scroller {width: 0px;}"); // as sources/projectview.cpp
|
||||
auto *view = new ProbeView(&scene);
|
||||
view->setFrameShape(QFrame::NoFrame);
|
||||
view->setAlignment(Qt::AlignLeft | Qt::AlignTop);
|
||||
tabs.addTab(view, "folio");
|
||||
tabs.resize(320, 240);
|
||||
tabs.show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&tabs));
|
||||
QCOMPARE(sheetColor(view->viewport()->grab().toImage()), QColor(Qt::white));
|
||||
|
||||
int since = view->paint_rects.size();
|
||||
QApplication::setPalette(QET::Palette::fusionDark());
|
||||
QTRY_VERIFY(view->invertsLightness());
|
||||
QTRY_VERIFY2(view->fullyRepaintedSince(since), "no full repaint after the switch to dark");
|
||||
const QColor base = QET::Palette::fusionDark().color(QPalette::Active, QPalette::Base);
|
||||
QCOMPARE(sheetColor(view->viewport()->grab().toImage()), base);
|
||||
|
||||
since = view->paint_rects.size();
|
||||
QApplication::setPalette(QET::Palette::fusionLight());
|
||||
QTRY_VERIFY(!view->invertsLightness());
|
||||
QTRY_VERIFY2(view->fullyRepaintedSince(since), "no full repaint after the switch to light");
|
||||
QCOMPARE(sheetColor(view->viewport()->grab().toImage()), QColor(Qt::white));
|
||||
}
|
||||
|
||||
/**
|
||||
render() paints only what the scene draws. A scene without a
|
||||
background brush leaves the rest of the buffer untouched, so the view
|
||||
fills it first: on the dark palette the blank area comes out as Base,
|
||||
not as whatever the memory held.
|
||||
*/
|
||||
void tst_qetpalette::paletteViewFillsWhatTheSceneLeavesBlank()
|
||||
{
|
||||
QApplication::setStyle(QStyleFactory::create("Fusion"));
|
||||
QApplication::setPalette(QET::Palette::fusionDark());
|
||||
|
||||
QGraphicsScene scene(0, 0, 200, 120);
|
||||
scene.addLine(10, 60, 190, 60, QPen(Qt::black, 2));
|
||||
ProbeView view(&scene);
|
||||
showAsSheet(view);
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&view));
|
||||
|
||||
const QImage image = view.viewport()->grab().toImage();
|
||||
const QColor base = QET::Palette::fusionDark().color(QPalette::Active, QPalette::Base);
|
||||
QCOMPARE(sheetColor(image), base);
|
||||
QCOMPARE(image.pixelColor(100, 20), base);
|
||||
QCOMPARE(image.pixelColor(100, 100), base);
|
||||
}
|
||||
|
||||
/**
|
||||
Moving an item must erase it where it was, children included, even a
|
||||
child far bigger than its parent: a terminal's help lines span the
|
||||
whole sheet. QGraphicsView does that on its default update path by
|
||||
remembering where each item was last painted. A receiver on
|
||||
QGraphicsScene::changed() switches the scene to its Qt 4.4
|
||||
compatibility path, which erases only the parent's own old rect and
|
||||
leaves the children's trails behind (reported on #954). The check
|
||||
reads the window's backing store, not a fresh rendering, and makes
|
||||
sure the repaint after the move was a partial one: a full repaint of
|
||||
the viewport would hide the bug, not fix it.
|
||||
*/
|
||||
void tst_qetpalette::paletteViewErasesMovedChildren_data()
|
||||
{
|
||||
addPaletteRows();
|
||||
}
|
||||
|
||||
void tst_qetpalette::paletteViewErasesMovedChildren()
|
||||
{
|
||||
QFETCH(QPalette, palette);
|
||||
QApplication::setStyle(QStyleFactory::create("Fusion"));
|
||||
QApplication::setPalette(palette);
|
||||
|
||||
QGraphicsScene scene(0, 0, 200, 120);
|
||||
scene.setBackgroundBrush(Qt::white);
|
||||
QGraphicsRectItem *parent = scene.addRect(0, 0, 20, 20, QPen(Qt::NoPen), QBrush(Qt::black));
|
||||
parent->setPos(20, 20);
|
||||
QGraphicsLineItem *line = new QGraphicsLineItem(-10, 10, 170, 10, parent);
|
||||
line->setPen(QPen(Qt::black, 2));
|
||||
|
||||
ProbeView view(&scene);
|
||||
showAsSheet(view);
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&view));
|
||||
QTRY_VERIFY(view.paints >= 1);
|
||||
QScreen *screen = view.screen();
|
||||
const WId window = view.window()->winId();
|
||||
const QImage before = screen->grabWindow(window, 0, 0, 200, 120).toImage();
|
||||
const QColor sheet = sheetColor(before);
|
||||
QVERIFY2(before.pixelColor(120, 30) != sheet, "the child line is not drawn");
|
||||
QVERIFY2(before.pixelColor(30, 30) != sheet, "the parent is not drawn");
|
||||
|
||||
const int from = view.paint_rects.size();
|
||||
parent->setPos(20, 70);
|
||||
QTRY_VERIFY(view.paint_rects.size() > from);
|
||||
QVERIFY2(!view.fullyRepaintedSince(from), "the move repainted the whole viewport");
|
||||
|
||||
const QImage after = screen->grabWindow(window, 0, 0, 200, 120).toImage();
|
||||
QVERIFY2(after.pixelColor(120, 30) == sheet,
|
||||
qPrintable(QString("the child line left a trail: %1").arg(after.pixelColor(120, 30).name())));
|
||||
QVERIFY2(after.pixelColor(30, 30) == sheet,
|
||||
qPrintable(QString("the parent left a trail: %1").arg(after.pixelColor(30, 30).name())));
|
||||
QVERIFY2(after.pixelColor(120, 80) != sheet, "the child line is missing at its new place");
|
||||
QVERIFY2(after.pixelColor(30, 80) != sheet, "the parent is missing at its new place");
|
||||
}
|
||||
|
||||
/**
|
||||
The same, for a child whose geometry is set while its parent is
|
||||
painted, as a Terminal does with its help lines: the view must record
|
||||
where the child was painted, not where it was before the paint, or
|
||||
the next move erases the wrong place. Two moves, because the first
|
||||
paint after a move is where the child gets its new geometry.
|
||||
*/
|
||||
void tst_qetpalette::paletteViewErasesChildrenMovedWhilePainting_data()
|
||||
{
|
||||
addPaletteRows();
|
||||
}
|
||||
|
||||
void tst_qetpalette::paletteViewErasesChildrenMovedWhilePainting()
|
||||
{
|
||||
QFETCH(QPalette, palette);
|
||||
QApplication::setStyle(QStyleFactory::create("Fusion"));
|
||||
QApplication::setPalette(palette);
|
||||
|
||||
QGraphicsScene scene(0, 0, 200, 120);
|
||||
scene.setBackgroundBrush(Qt::white);
|
||||
PaintTimeHelpLine *parent = new PaintTimeHelpLine(scene.sceneRect());
|
||||
scene.addItem(parent);
|
||||
parent->setPos(20, 20);
|
||||
|
||||
ProbeView view(&scene);
|
||||
showAsSheet(view);
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&view));
|
||||
QTRY_VERIFY(view.paints >= 1);
|
||||
QScreen *screen = view.screen();
|
||||
const WId window = view.window()->winId();
|
||||
const QColor sheet = sheetColor(screen->grabWindow(window, 0, 0, 200, 120).toImage());
|
||||
|
||||
int from = view.paint_rects.size();
|
||||
parent->setPos(20, 50);
|
||||
QTRY_VERIFY(view.paint_rects.size() > from);
|
||||
QTRY_VERIFY2(screen->grabWindow(window, 0, 0, 200, 120).toImage().pixelColor(120, 60) != sheet,
|
||||
"the line did not follow the parent");
|
||||
|
||||
from = view.paint_rects.size();
|
||||
parent->setPos(20, 80);
|
||||
QTRY_VERIFY(view.paint_rects.size() > from);
|
||||
QVERIFY2(!view.fullyRepaintedSince(from), "the move repainted the whole viewport");
|
||||
QImage after;
|
||||
QTRY_VERIFY2((after = screen->grabWindow(window, 0, 0, 200, 120).toImage()).pixelColor(120, 90) != sheet,
|
||||
"the line is missing at its new place");
|
||||
QVERIFY2(after.pixelColor(120, 60) == sheet,
|
||||
qPrintable(QString("the line left a trail: %1").arg(after.pixelColor(120, 60).name())));
|
||||
QVERIFY2(after.pixelColor(120, 30) == sheet,
|
||||
qPrintable(QString("the first line was never erased: %1").arg(after.pixelColor(120, 30).name())));
|
||||
}
|
||||
|
||||
/**
|
||||
The inversion runs on every repaint of the folio, so a 4K viewport
|
||||
has to cost a few milliseconds. Reported, not asserted: the bound
|
||||
depends on the build box.
|
||||
*/
|
||||
void tst_qetpalette::invertLightnessSpeed()
|
||||
{
|
||||
QImage image(3840, 2000, QImage::Format_RGB32);
|
||||
image.fill(Qt::white);
|
||||
QBENCHMARK {
|
||||
QET::Palette::invertLightness(image, QColor(30, 30, 30), QColor(220, 220, 220));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
A widget with a style sheet keeps the palette QStyleSheetStyle
|
||||
resolved when the sheet was applied: after QApplication::setPalette()
|
||||
it is still drawn in the old colors, which is what the folio tab bar
|
||||
showed after a live light/dark switch. refreshStyleSheets() brings
|
||||
it in line. Both directions are checked.
|
||||
*/
|
||||
void tst_qetpalette::styleSheetWidgetsFollowPaletteChange()
|
||||
{
|
||||
QApplication::setStyle(QStyleFactory::create("Fusion"));
|
||||
QApplication::setPalette(QET::Palette::fusionLight());
|
||||
|
||||
QWidget top;
|
||||
auto *layout = new QHBoxLayout(&top);
|
||||
auto *tabs = new QTabWidget;
|
||||
tabs->addTab(new QWidget, "1");
|
||||
tabs->setStyleSheet("QTabBar::scroller {width: 0px;}"); // as sources/projectview.cpp
|
||||
auto *button = new QToolButton;
|
||||
button->setText("+");
|
||||
button->setAutoRaise(true);
|
||||
tabs->setCornerWidget(button, Qt::TopRightCorner);
|
||||
auto *plain = new QLabel("plain");
|
||||
plain->setAutoFillBackground(true);
|
||||
layout->addWidget(tabs);
|
||||
layout->addWidget(plain);
|
||||
top.resize(300, 120);
|
||||
top.show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&top));
|
||||
|
||||
// The most frequent color of a widget's rendering: its background.
|
||||
auto background = [](QWidget *w) {
|
||||
const QImage image = w->grab().toImage();
|
||||
QHash<QRgb, int> histogram;
|
||||
for (int y = 0; y < image.height(); ++y)
|
||||
for (int x = 0; x < image.width(); ++x)
|
||||
++histogram[image.pixel(x, y)];
|
||||
QRgb best = 0;
|
||||
int count = -1;
|
||||
for (auto it = histogram.cbegin(); it != histogram.cend(); ++it)
|
||||
if (it.value() > count) { count = it.value(); best = it.key(); }
|
||||
return best;
|
||||
};
|
||||
auto window = [](const QPalette &p) { return p.color(QPalette::Active, QPalette::Window).rgb(); };
|
||||
|
||||
for (const QPalette &palette : {QET::Palette::fusionDark(), QET::Palette::fusionLight()})
|
||||
{
|
||||
QApplication::setPalette(palette);
|
||||
QTest::qWait(50);
|
||||
QCOMPARE(background(plain), window(palette));
|
||||
// Qt leaves the style-sheet widget behind; this is the defect.
|
||||
QVERIFY2(background(button) != window(palette), "Qt now updates style-sheet widgets itself; refreshStyleSheets() is redundant");
|
||||
|
||||
QET::Palette::refreshStyleSheets();
|
||||
QTest::qWait(50);
|
||||
QCOMPARE(background(button), window(palette));
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
// Widgets are painted for real, on Qt's offscreen platform so the test
|
||||
|
||||