Compare commits

..

34 Commits

Author SHA1 Message Date
Laurent Trinques c256e2dd1a Merge pull request #958 from bhangart/fix/pin-fetchcontent-dependencies
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m37s
Fix/pin fetchcontent dependencies
2026-09-20 20:13:37 +02:00
Laurent Trinques 8e3a7a6ab6 Merge pull request #927 from ispyisail/fix/923-snap-element-text
Snap a device's text to the grid when it is dragged (#923)
2026-09-20 20:10:13 +02:00
Laurent Trinques 740271c993 Merge pull request #938 from arummler/fix-translation-syntax
Fix various singular/plural cosntructions
2026-09-20 20:08:53 +02:00
Laurent Trinques 1bec2700bd Merge pull request #924 from Kellermorph/auto-numbering-new-project
Add global auto-numbering rules to QElectroTech settings
2026-09-20 20:07:16 +02:00
Laurent Trinques 268e9c7bd9 Merge pull request #959 from jp2images/fix-dark-canvas-item-updates
Keep the dark canvas on QGraphicsView's own update path
2026-09-20 20:02:50 +02:00
Laurent Trinques 264a0f677f Merge pull request #964 from jp2images/remove-stray-sbom-files
Remove two files that slipped into #944
2026-09-20 20:02:27 +02:00
Laurent Trinques 9ccfb5e116 Merge pull request #963 from jp2images/fix-hover-tint-light-face
Darken the hover ink on a light button face
2026-09-20 19:45:28 +02:00
Jeff Patterson 04463f0954 Darken the hover ink on a light button face
QETStyle::hoverColor() lightened the highlight color until it read at
3.5:1 against the Light role. On a dark face that is the way to go; on a
light face lightening only fades the ink, so with a pale platform accent
that QET keeps (macOS's green selection color, black selection text) the
loop ran to white and every hovered line-art icon vanished. The ink now
moves away from the face, darker on a light face, lighter on a dark one,
and falls back to the button text color if twenty steps are not enough.

The hover test gets a row with that accent on each palette, and a new
test sweeps accents across hues and lightness on both palettes and
requires the hover ink to read at 3:1 on the face.

Fixes #962
2026-09-20 12:45:07 -05:00
Laurent Trinques 3245919c19 Merge pull request #961 from jp2images/fix-small-page-icons
Give every configuration page an icon at page size
2026-09-20 19:44:00 +02:00
Jeff Patterson 55ad2eccbf Remove two files that slipped into #944
sources/qetsbom.cpp and sources/qetsbom.h were untracked local files
that a directory-wide add swept into the rebuilt #944 commit. Nothing
references them; the build does not compile them.
2026-09-20 12:43:57 -05:00
Jeff Patterson 8b2548d601 Keep the dark canvas on QGraphicsView's own update path
Laurent found that moving an element on a #954 build left its terminals'
help lines behind at every step, on both palettes. The view listened to
QGraphicsScene::changed() so that render() would keep the scene's updates
flowing, and any receiver on that signal puts the scene on its Qt 4.4
compatibility path, which erases a moved item's own old rect only:
children bigger than their parent stay on screen. Master hides that with
FullViewportUpdate, which repaints the whole viewport on every change.

Paint the inverted folio through QGraphicsView::paintEvent() instead, with
IndirectPainting set for that call and the draw hooks painting into a
viewport-sized image, and hand the scene the viewport when the items are
drawn so it records where each item was painted, including a child whose
geometry is set while its parent paints. The listener and the
full-viewport update go. Two tests move a parent with a sheet-wide child,
read the backing store, and require the repaint to be a partial one.
2026-09-20 12:43:50 -05:00
Laurent Trinques c3aeb0bf83 Merge pull request #944 from jp2images/fix-stylesheet-palette-switch
Refresh style-sheet widgets after a live palette switch
2026-09-20 19:36:26 +02:00
Jeff Patterson 16d89f64fd Refresh style-sheet widgets after a live palette switch
Switching the system between light and dark while QET runs changed
the palette of every plain widget but left widgets that carry a style
sheet in the colors they were created with: the folio tab bar stayed
light in dark mode, and after a dark-to-light switch its Add folio and
chevron buttons hovered as a near-black box with the icon lost inside
it. QApplication::setPalette() does not reach a widget with a style
sheet; QStyleSheetStyle resolved its palette once, when the sheet was
applied, and keeps it. Seventeen call sites set a sheet on a widget and
five .ui files carry one, so any of them could show the stale palette.

QET::Palette::refreshStyleSheets() re-applies each such widget's own
sheet, which makes QStyleSheetStyle resolve it against the palette now
in force. QETApp::useSystemPalette() calls it after installing the
palette, so both the OS color scheme change and the "use system
colors" setting are covered.

tests/qttest/tst_qetpalette: a tab widget with the folio tab bar's
sheet is still drawn in the old colors after setPalette(), which is the
defect, and follows the palette after refreshStyleSheets(), in both
directions.

Fixes #943.
2026-09-20 12:20:01 -05:00
Jeff Patterson ac78c51b70 Give every configuration page an icon at page size
The settings and project dialogs list their pages with 64 or 128 pixel
icons, and two pages had theirs at 22 pixels only: the terminal-strip
page and the shortcuts page, which borrowed configure-toolbars. Both get
a 128 pixel icon drawn in the style of the other page icons, the
shortcuts page under its own name, configure-shortcuts. The SVG sources
sit beside the PNGs.

On a dark palette the Printing and Export pages were small too: their
128 pixel icons exist in the light theme only, and Qt inherits by name,
not by size, so the dark theme's small copies were scaled up instead.
make_icon_themes.py now aliases the light files of the sizes a dark name
lacks, when they read on the dark window at 3:1.

A test asks the theme for every page icon at 128 pixels, on both
palettes.

Fixes #960
2026-09-20 10:44:09 -05:00
Beat Hangartner 2e27f77b28 Added more verbose description of the difference between lightweight and annotated tags as comment. 2026-09-20 14:21:36 +02:00
Laurent Trinques 83fb525e27 Merge pull request #939 from elevatormind/backtrace-windows-disable
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m39s
Fix issue #937
2026-09-20 14:03:43 +02:00
Laurent Trinques d1808c7a89 Fix Draw the folio with inverted lightness on a dark palette- #954 2026-09-20 13:07:57 +02:00
Laurent Trinques 4e4f029d2d Merge pull request #957 from qelectrotech/revert-955-revert-954-feature-dark-canvas
Revert "Revert "Draw the folio with inverted lightness on a dark palette""
2026-09-20 13:05:38 +02:00
Laurent Trinques 242f134e0f Revert "Revert "Draw the folio with inverted lightness on a dark palette"" 2026-09-20 13:05:13 +02:00
Magnus Hellströmer 2dc88df29c feat(build): Enable backtrace detection on Windows 2026-09-20 12:48:10 +02:00
Magnus Hellströmer fd8135264f fix(build): Remove stale Windows backtrace comments 2026-09-20 12:48:10 +02:00
Magnus Hellströmer 827cd2a91e fix(build): support backtrace in MSYS2 2026-09-20 12:48:10 +02:00
Magnus Hellströmer cdd25189b5 fix(build): guard backtrace detection on Windows 2026-09-20 12:48:04 +02:00
Laurent Trinques f1313f5895 Merge pull request #955 from qelectrotech/revert-954-feature-dark-canvas
Revert "Draw the folio with inverted lightness on a dark palette"
2026-09-20 07:20:03 +02:00
Laurent Trinques 75fafd5acc Revert "Draw the folio with inverted lightness on a dark palette" 2026-09-20 06:57:56 +02:00
Beat Hangartner 3f397f5f78 Explain why fetching by git tag is a supply chain risk
The pinning comment stated that a tag is mutable but not what an attacker
does with that, so the trade-off was hard to judge for anyone reviewing or
later undoing the pins. Spell out the mechanism: a tag is a name pointing
at a commit, anyone with push access upstream can force-push it elsewhere,
and FetchContent resolves it at build time, so a stolen maintainer account
or CI token makes every fresh build compile the attacker's code while
nothing changes here and the tag name still reads correctly. A commit hash
is derived from the content and cannot be moved that way.

Name the two cases where this was actually exploited: tj-actions/changed-
files in March 2025 (CVE-2025-30066), where tags v1 through v45.0.7 were
retargeted to a commit leaking CI secrets into build logs across more than
23,000 repositories, and aquasecurity/trivy-action in March 2026
(CVE-2026-33634), where 76 of 77 version tags were force-pushed to a
credential stealer for about twelve hours. Both were GitHub Actions rather
than CMake dependencies, which the comment says, because the point is the
shared mechanism of resolving a tag at build time.

Also document how to upgrade a pin, including that git ls-remote reports
the tag object for an annotated tag and the commit on the "^{}" line.

The note lives in fetch_pugixml.cmake, which fetch_kdeaddons.cmake and
fetch_singleapplication.cmake already refer to. Comments only; no build
behaviour changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 23:36:20 +02:00
Beat Hangartner 7ec13cbc1a Pin fetched dependencies to commit hashes instead of git tags
CMake fetches pugixml, SingleApplication and the three KDE Frameworks
modules by git tag. A tag is a mutable pointer that its owner can move,
so two builds of the same QElectroTech commit can silently get different
third-party sources, and a compromised upstream account can change what
every builder downloads without anything changing in this repository.
Pinning each dependency to the commit its tag currently points at closes
that, while keeping the tag name in a trailing comment so the intended
version stays readable.

No versions change. Every pinned commit is the one its tag resolves to,
checked with git ls-remote and confirmed by fetching each one and
verifying that git describe reports exactly the tag. The three KDE
modules live in separate repositories and therefore need separate
commits, so the single KF_GIT_TAG variable becomes three per-module
variables; passing -DKF_GIT_TAG=<ref> still selects one ref for all
three, unpinned, exactly as before, and KF_GIT_TAG stays defined so the
build summary in define_definitions.cmake is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 23:05:39 +02:00
Kellermorph 7fa1ff35b8 Add global auto-numbering rules to QElectroTech settings
Add a 'Numérotation auto' tab to the global settings page (Settings >
Nouveau projet) where users can define default auto-numbering rules
for Conducteurs, Eléments, and Folios. These rules are automatically
transferred to every new project created.

Changes:
- Add NumerotationContext::saveToSettings()/loadFromSettings() static
  helpers for persisting named numerotation contexts via QSettings
- Add 'Numérotation auto' tab to NewDiagramPage with three sub-tabs
  using SelectAutonumW widgets (same UI as project properties)
- Add save/remove/persist slots for conductor, element, and folio
  contexts with immediate QSettings persistence on every change
- NewDiagramPage::applyConf() saves autonum settings when editing
  global defaults (no project)
- QETProject constructor loads global autonum settings from QSettings
  for new empty projects
2026-09-19 13:47:28 +02:00
Andre Rummler 7e3420b1a5 Avoid constructing sentences. 2026-09-19 10:39:54 +02:00
Andre Rummler 183035d5ac Replace enumerator list. 2026-09-19 09:15:28 +02:00
Andre Rummler eb4109bb11 Replace manual enumerator sentence with qt trasnlator automatism. 2026-09-19 09:08:25 +02:00
Andre Rummler 717bf57677 Improve plural marking from previous commit. 2026-09-19 09:08:00 +02:00
Andre Rummler 861e5de25e Use qt own enumeration translation function. Fix plural forms. 2026-09-19 08:31:34 +02:00
ispyisail 95d0e523fe Snap a device text to the grid when it is dragged (#923)
Dragging an element's text -- its label, article number, any of its
information fields -- moved it in free one-unit steps while everything
else in the editor snapped to the grid. Reported by pki791 in #923 for
labels moved with Shift.

QET moves a text with the mouse along five paths. Four snap and let Ctrl
place freely:

  DiagramTextItem::mouseMoveEvent        an independent text
  ElementTextItemGroup::mouseMoveEvent   a group of element texts
  ElementTextsMover::continueMovement    every OTHER selected element text
  QetGraphicsItem::setPos                elements, images, shapes

DynamicElementTextItem::mouseMoveEvent, the text actually under the
cursor, ended "setPos(new_pos)" with no grid and no modifier check. It is
otherwise the same function as the group's, which is why this reads as an
omission rather than a decision: the line this adds is that function's,
character for character.

The inconsistency was visible in one gesture. With two element texts
selected and one of them dragged, ElementTextsMover skips the driver item
and snaps the rest, so the text under the cursor was the only one on the
folio that did not land on the grid.

Verified on a virtual display (Xvfb + openbox) against a two-lamp fixture,
grid 10, reading the saved positions rather than the screen:

  Shift+drag the label      before (32.95, -11.55) -> (7.95, 23.45) off-grid
                            after                  -> (10, 20)      on-grid
  the co-selected label     (10, -10) -> (50, 20) on-grid, before and after
  Shift to grab, then Ctrl  -> (7.95, 23.45) off-grid, free placement kept

The last line matters: moving an element text needs Shift at press, and
the modifier is read at move time, so Ctrl still places freely -- press
with Shift, hold Ctrl to drag. Holding both from the press is a different
gesture, reserved by DiagramView::isCtrlShifting() for the view's mode
switch, and does not move the text at all. Nothing that was possible
before is lost.

Worth knowing when reviewing: 470 of the 492 element texts in the 24
example projects (95.5 %) sit off the grid today, because element
definitions place their default text at fractional offsets. The first
drag of almost any existing label will pull it onto the grid, by at most
half a grid step.

ctest 12/12, Qt 6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 08:06:45 +12:00
35 changed files with 1159 additions and 223 deletions
+3 -1
View File
@@ -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)
+16
View File
@@ -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
+28 -5
View File
@@ -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()
+42 -1
View File
@@ -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()
+7 -1
View File
@@ -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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

+29
View File
@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
<!-- keyboard body with a lighter top edge -->
<rect x="6" y="30" width="116" height="68" rx="8" fill="#31363b"/>
<rect x="6" y="30" width="116" height="34" rx="8" fill="#4d4d4d"/>
<rect x="6" y="46" width="116" height="18" fill="#31363b"/>
<!-- key rows -->
<g fill="#eff0f1">
<rect x="14" y="38" width="8" height="8" rx="1.5"/><rect x="24" y="38" width="8" height="8" rx="1.5"/>
<rect x="34" y="38" width="8" height="8" rx="1.5"/><rect x="44" y="38" width="8" height="8" rx="1.5"/>
<rect x="54" y="38" width="8" height="8" rx="1.5"/><rect x="64" y="38" width="8" height="8" rx="1.5"/>
<rect x="74" y="38" width="8" height="8" rx="1.5"/><rect x="84" y="38" width="8" height="8" rx="1.5"/>
<rect x="94" y="38" width="8" height="8" rx="1.5"/><rect x="104" y="38" width="10" height="8" rx="1.5"/>
<rect x="14" y="50" width="12" height="8" rx="1.5"/><rect x="28" y="50" width="8" height="8" rx="1.5"/>
<rect x="38" y="50" width="8" height="8" rx="1.5"/><rect x="48" y="50" width="8" height="8" rx="1.5"/>
<rect x="58" y="50" width="8" height="8" rx="1.5"/><rect x="68" y="50" width="8" height="8" rx="1.5"/>
<rect x="78" y="50" width="8" height="8" rx="1.5"/><rect x="88" y="50" width="8" height="8" rx="1.5"/>
<rect x="98" y="50" width="16" height="8" rx="1.5"/>
<rect x="14" y="62" width="16" height="8" rx="1.5"/><rect x="32" y="62" width="8" height="8" rx="1.5"/>
<rect x="42" y="62" width="8" height="8" rx="1.5"/><rect x="52" y="62" width="8" height="8" rx="1.5"/>
<rect x="62" y="62" width="8" height="8" rx="1.5"/><rect x="72" y="62" width="8" height="8" rx="1.5"/>
<rect x="82" y="62" width="8" height="8" rx="1.5"/><rect x="92" y="62" width="22" height="8" rx="1.5"/>
<rect x="14" y="74" width="10" height="8" rx="1.5"/><rect x="26" y="74" width="10" height="8" rx="1.5"/>
<rect x="38" y="74" width="52" height="8" rx="1.5"/>
<rect x="92" y="74" width="10" height="8" rx="1.5"/><rect x="104" y="74" width="10" height="8" rx="1.5"/>
</g>
<!-- the two keys of a shortcut, pressed -->
<rect x="14" y="62" width="16" height="8" rx="1.5" fill="#3daee9"/>
<rect x="42" y="62" width="8" height="8" rx="1.5" fill="#3daee9"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

+48
View File
@@ -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

+1
View File
@@ -216,6 +216,7 @@ Clarification:
ico/128x128/diagram.png by the QElectroTech team (License CC BY-ND 3.0)
ico/128x128/document-export.png by the QElectroTech team (License CC BY-ND 3.0)
ico/128x128/project.png by the QElectroTech team (License CC BY-ND 3.0)
ico/128x128/terminalstrip.png and configure-shortcuts.png by Jeff Patterson from the QElectroTech team (License CC BY-ND 3.0), rendered from the .svg files beside them
ico/scalable/pdf-import.svg by Jeff Patterson from the QElectroTech team (License CC BY-ND 3.0), laid out like ico/22x22/insert-image.png
ico/scalable/diagram.svg, folio-new.svg, folio-delete.svg, folio-properties.svg, label.svg by Jeff Patterson from the QElectroTech team (License CC BY-ND 3.0), the folio icons redrawn in the same style
ico/256x256/* by Nuri from the QElectroTech team (License CC BY-ND 3.0)
+4
View File
@@ -259,12 +259,14 @@
<file alias="themes/qet/48x48/user-away-extended.png">48x48/user-away-extended.png</file>
<file alias="themes/qet/48x48/user-away.png">48x48/user-away.png</file>
<file alias="themes/qet/48x48/view-pim-journal.png">48x48/view-pim-journal.png</file>
<file alias="themes/qet/128x128/configure-shortcuts.png">128x128/configure-shortcuts.png</file>
<file alias="themes/qet/128x128/diagram.png">128x128/diagram.png</file>
<file alias="themes/qet/128x128/document-export.png">128x128/document-export.png</file>
<file alias="themes/qet/128x128/plasmagik.png">128x128/plasmagik.png</file>
<file alias="themes/qet/128x128/printer.png">128x128/printer.png</file>
<file alias="themes/qet/128x128/project.png">128x128/project.png</file>
<file alias="themes/qet/128x128/settings.png">128x128/settings.png</file>
<file alias="themes/qet/128x128/terminalstrip.png">128x128/terminalstrip.png</file>
<file alias="themes/qet/scalable/diagram.svg">scalable/diagram.svg</file>
<file alias="themes/qet/scalable/folio-delete.svg">scalable/folio-delete.svg</file>
<file alias="themes/qet/scalable/folio-new.svg">scalable/folio-new.svg</file>
@@ -469,5 +471,7 @@
<file>themes/qet-dark/scalable/ellipse-to-bezier.svg</file>
<file>themes/qet-dark/scalable/rect-to-bezier.svg</file>
<file>themes/qet-dark/scalable/rect-to-polyline.svg</file>
<file alias="themes/qet-dark/128x128/document-export.png">128x128/document-export.png</file>
<file alias="themes/qet-dark/128x128/printer.png">128x128/printer.png</file>
</qresource>
</RCC>
+50
View File
@@ -34,6 +34,16 @@ expects, so QIcon::fromTheme("name") finds them. The dark theme only holds
the icons that need a dark variant: black line art. Colored icons are not
touched; the dark theme inherits them from the light one.
Qt inherits by name, not by size: once a name has any file in the dark
theme, the parent theme is never consulted for that name, and a size the
dark theme lacks is served by scaling the nearest dark file. An icon that
is line art at 22 pixels and colored at 128 (the printer) would then come
out as the 22 pixel copy scaled up on a dark palette. So for every name
the dark theme holds, the .qrc also aliases the light files of the sizes
the dark theme does not have, when they read on the dark window (3:1,
measured as tests/qttest/tst_qeticons.cpp does); a light file that does
not is left out, and Qt scales the nearest dark copy as before.
An icon counts as line art when fewer than 20% of its visible pixels are
saturated. Its dark copy keeps hue and alpha and inverts lightness, scaled
so pure black becomes (220, 220, 220), the dark palette's text color.
@@ -101,6 +111,8 @@ WHITE_LIGHTNESS = 0.85 # a visible pixel this light counts as white
WHITE_FRACTION = 0.30 # at or above this an icon is "light art"
INK = 220 / 255.0 # lightness of pure black after inversion
SVG_INK = "#dcdcdc"
DARK_WINDOW = (53, 53, 53) # QET::Palette::fusionDark() window color
DARK_RATIO = 3.0 # what tst_qeticons requires of a dark theme file
def visible_pixels(image):
@@ -131,6 +143,28 @@ def has_light_fill(image):
return white / len(pixels) >= WHITE_FRACTION
def relative_luminance(rgb):
def linear(c):
c /= 255.0
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
r, g, b = rgb
return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b)
def reads_on_dark(image):
"""True when the icon's mean visible color reaches DARK_RATIO against
the dark window, the test tst_qeticons applies to every dark file."""
pixels = visible_pixels(image)
if not pixels:
return False
n = len(pixels)
mean = (sum(p[0] for p in pixels) // n, sum(p[1] for p in pixels) // n,
sum(p[2] for p in pixels) // n)
lighter, darker = sorted((relative_luminance(mean), relative_luminance(DARK_WINDOW)),
reverse=True)
return (lighter + 0.05) / (darker + 0.05) >= DARK_RATIO
def invert_lightness(image):
"""Invert lightness, keeping hue, saturation and alpha.
@@ -240,6 +274,20 @@ def main():
changed += write_if_changed(target, text)
dark.append(f"themes/qet-dark/scalable/{name}")
# Complete each dark name with the light files of its other sizes
# that read on a dark window (see the module docstring): aliases, no
# copies.
dark_sizes = {}
for path in dark:
size, name = path.split("/")[2:]
dark_sizes.setdefault(name, set()).add(size)
dark_aliases = []
for alias, source in light:
size, name = alias.split("/")[2:]
if name in dark_sizes and size not in dark_sizes[name] \
and reads_on_dark(Image.open(ICO / source).convert("RGBA")):
dark_aliases.append((f"themes/qet-dark/{size}/{name}", source))
# Drop dark files from an earlier run that are no longer generated, so
# a reclassified icon falls back to the light theme instead of keeping
# a stale copy.
@@ -264,6 +312,8 @@ def main():
qrc.append(f' <file alias="{alias}">{source}</file>')
for path in dark:
qrc.append(f" <file>{path}</file>")
for alias, source in dark_aliases:
qrc.append(f' <file alias="{alias}">{source}</file>')
qrc += [" </qresource>", "</RCC>", ""]
changed += write_if_changed(QRC, "\n".join(qrc))
@@ -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()
+77
View File
@@ -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 &currentRule,
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 &currentRule = 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;
}
+10
View File
@@ -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 &currentRule,
QSettings &settings,
const QString &prefix);
static QPair<QHash<QString, NumerotationContext>, QString> loadFromSettings(
QSettings &settings,
const QString &prefix);
private:
QStringList content_;
};
+10 -21
View File
@@ -137,25 +137,14 @@ void ElementTextsMover::endMovement()
QString ElementTextsMover::undoText() const
{
QString undo_text;
if(m_text_count == 1)
undo_text.append(QObject::tr("Déplacer un texte d'élément"));
else if(m_text_count > 1)
undo_text.append(QObject::tr("Déplacer %1 textes d'élément").arg(m_items_hash.size()));
if(m_group_count >= 1)
{
if(undo_text.isEmpty())
undo_text.append(QObject::tr("Déplacer"));
else
undo_text.append(QObject::tr(" et"));
if(m_group_count == 1)
undo_text.append(QObject::tr(" un groupe de texte"));
else
undo_text.append(QObject::tr((" %1 groupes de textes")).arg(m_group_count));
}
return undo_text;
QStringList parts;
if (m_text_count)
parts << QObject::tr("%n texte(s) d'élément", "", m_text_count);
if (m_group_count)
parts << QObject::tr("%n groupe(s) de textes", "", m_group_count);
if (parts.isEmpty())
return QString(); // should never occur
return QObject::tr("Déplacer %1").arg(QLocale().createSeparatedList(parts));
}
+110 -55
View File
@@ -21,12 +21,30 @@
#include <QApplication>
#include <QEvent>
#include <QPainter>
#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)
{
@@ -37,18 +55,6 @@ PaletteGraphicsView::PaletteGraphicsView(QGraphicsScene *scene, QWidget *parent)
QGraphicsView(scene, parent)
{
qApp->installEventFilter(this);
listenToScene(scene);
}
/**
@brief PaletteGraphicsView::setScene
Same as QGraphicsView::setScene, and keeps the scene's updates flowing
(see the class description).
*/
void PaletteGraphicsView::setScene(QGraphicsScene *scene)
{
QGraphicsView::setScene(scene);
listenToScene(scene);
}
/**
@@ -70,21 +76,6 @@ void PaletteGraphicsView::paintingInverted(bool inverted)
Q_UNUSED(inverted)
}
/**
@brief PaletteGraphicsView::listenToScene
Connect a receiver to the scene's changed() signal, dropping the one
on the previous scene. Any receiver does; this one has nothing to do.
The connection dies with the view.
*/
void PaletteGraphicsView::listenToScene(QGraphicsScene *scene)
{
disconnect(m_scene_connection);
m_scene_connection = QMetaObject::Connection();
if (scene)
m_scene_connection = connect(scene, &QGraphicsScene::changed,
this, [](const QList<QRectF> &) {});
}
/**
@brief PaletteGraphicsView::eventFilter
Repaint the whole viewport when the application palette changes. Qt
@@ -109,57 +100,121 @@ bool PaletteGraphicsView::eventFilter(QObject *watched, QEvent *event)
void PaletteGraphicsView::paintEvent(QPaintEvent *event)
{
if (invertsLightness())
paintInverted(event->rect());
else
QGraphicsView::paintEvent(event);
{
paintInverted(event);
return;
}
m_buffer = QImage();
QGraphicsView::paintEvent(event);
}
/**
@brief PaletteGraphicsView::paintInverted
Render \a area of the viewport into an off-screen image, invert the
lightness 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.
@param area the part of the viewport to repaint, in viewport coordinates
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(const QRect &area)
void PaletteGraphicsView::paintInverted(QPaintEvent *event)
{
const QRect rect = area.intersected(viewport()->rect());
if (rect.isEmpty())
const QRect exposed = event->rect().intersected(viewport()->rect());
if (exposed.isEmpty())
return;
const qreal ratio = viewport()->devicePixelRatioF();
QImage buffer(qCeil(rect.width() * ratio), qCeil(rect.height() * ratio),
QImage::Format_RGB32);
buffer.setDevicePixelRatio(ratio);
// render() paints only what the scene draws; what it leaves blank is
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.
buffer.fill(Qt::white);
m_buffer_painter.fillRect(exposed, Qt::white);
m_buffer_painter.setClipRect(exposed);
m_buffer_painter.setRenderHints(renderHints());
m_buffer_painter.setWorldTransform(viewportTransform());
QPainter buffer_painter(&buffer);
buffer_painter.setRenderHints(renderHints());
m_inverting = true;
paintingInverted(true);
render(&buffer_painter, QRectF(QPointF(0, 0), QSizeF(rect.size())),
rect, Qt::IgnoreAspectRatio);
const OptimizationFlags flags = optimizationFlags();
setOptimizationFlag(QGraphicsView::IndirectPainting, true);
QGraphicsView::paintEvent(event);
setOptimizationFlags(flags);
paintingInverted(false);
buffer_painter.end();
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(buffer, application_palette.color(QPalette::Base),
QET::Palette::invertLightness(part, application_palette.color(QPalette::Base),
application_palette.color(QPalette::Text));
QPainter painter(viewport());
painter.drawImage(rect.topLeft(), buffer);
painter.drawImage(area.topLeft(), part);
drawRubberBand(painter);
}
/**
@brief PaletteGraphicsView::drawRubberBand
Draw the selection rubber band the way QGraphicsView::paintEvent does.
Rendering the view into an off-screen image skips it, so it is drawn
here instead, after the inversion, in the palette colors.
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)
+44 -22
View File
@@ -19,26 +19,39 @@
#define PALETTE_GRAPHICS_VIEW_H
#include <QGraphicsView>
class QPainter;
#include <QImage>
#include <QPainter>
/**
A QGraphicsView that shows its scene with inverted lightness while its
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.
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.
The view paints through QGraphicsView::render() into an image and blits
the inverted image. QGraphicsView delivers scene updates straight to
its viewport when nobody listens to QGraphicsScene::changed(), and in
that mode the scene clears its "update everything" flag only when the
items are painted straight onto the viewport, which never happens
here: from the second QGraphicsScene::update() on, every scene update
and every item update would wait for an unrelated repaint. So the view
listens to changed() on every scene it is given, which makes the scene
clear the flag before it emits. Set the scene through this class, not
through a QGraphicsView pointer.
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
{
@@ -48,14 +61,17 @@ class PaletteGraphicsView : public QGraphicsView
explicit PaletteGraphicsView(QWidget *parent = nullptr);
explicit PaletteGraphicsView(QGraphicsScene *scene, QWidget *parent = nullptr);
void setScene(QGraphicsScene *scene);
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 rendered for an
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.
@@ -63,11 +79,17 @@ class PaletteGraphicsView : public QGraphicsView
virtual void paintingInverted(bool inverted);
private:
void listenToScene(QGraphicsScene *scene);
void paintInverted(const QRect &area);
void paintInverted(QPaintEvent *event);
void blitInverted(const QRect &area);
void drawRubberBand(QPainter &painter);
QMetaObject::Connection m_scene_connection;
/// 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
+54 -50
View File
@@ -271,84 +271,88 @@ QString QET::ElementsAndConductorsSentence(
int tables_count,
int terminal_strip_count)
{
QString text;
QStringList parts;
if (elements_count) {
text += QObject::tr(
"%n élément(s)",
"part of a sentence listing the content of a diagram",
elements_count
parts.append(
QObject::tr(
"%n élément(s)",
"part of a enumerative partial sentence listing the content of a diagram",
elements_count
)
);
}
if (conductors_count) {
if (!text.isEmpty()) text += ", ";
text += QObject::tr(
"%n conducteur(s)",
"part of a sentence listing the content of a diagram",
conductors_count
parts.append(
QObject::tr(
"%n conducteur(s)",
"part of a enumerative partial sentence listing the content of a diagram",
conductors_count
)
);
}
if (texts_count) {
if (!text.isEmpty()) text += ", ";
text += QObject::tr(
"%n champ(s) de texte",
"part of a sentence listing the content of a diagram",
texts_count
parts.append(
QObject::tr(
"%n champ(s) de texte",
"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",
images_count
);
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(
"%n forme(s)",
"part of a sentence listing the content of a diagram",
shapes_count
parts.append(
QObject::tr(
"%n forme(s)",
"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(
"%n texte(s) d'élément",
"part of a sentence listing the content of a diagram",
element_text_count);
parts.append(
QObject::tr(
"%n texte(s) d'élément",
"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(
"%n tableau(s)",
"part of a sentence listing the content of diagram",
tables_count);
parts.append(
QObject::tr(
"%n tableau(s)",
"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);
}
/**
+6
View File
@@ -235,6 +235,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);
@@ -1808,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();
}
/**
@@ -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);
+2
View File
@@ -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");
+1
View File
@@ -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;
+13 -1
View File
@@ -17,10 +17,11 @@
*/
#include "qetpalette.h"
#include <QApplication>
#include <QColor>
#include <QImage>
#include <QStyle>
#include <QWidget>
#include <cmath>
namespace {
@@ -296,3 +297,14 @@ QPixmap QET::Palette::forPalette(const QPixmap &pixmap, const QPalette &palette)
result.setDevicePixelRatio(pixmap.devicePixelRatio());
return result;
}
void QET::Palette::refreshStyleSheets()
{
// Setting the same sheet again is not a no-op: QWidget::setStyleSheet()
// asks QStyleSheetStyle to repolish the widget, which recomputes its
// palette from the application palette now in force.
const QWidgetList widgets = QApplication::allWidgets();
for (QWidget *widget : widgets)
if (!widget->styleSheet().isEmpty())
widget->setStyleSheet(widget->styleSheet());
}
+12
View File
@@ -135,6 +135,18 @@ namespace QET {
left alone either way.
*/
QPixmap forPalette(const QPixmap &pixmap, const QPalette &palette);
/**
Make every widget that carries a style sheet take the current
application palette. QApplication::setPalette() reaches plain
widgets, but a widget with a style sheet keeps the palette
QStyleSheetStyle resolved when the sheet was applied, so after a
live light/dark switch it is drawn in the old colors (the folio
tab bar and its buttons, the element info widgets, several
configuration pages). Re-applying each widget's own sheet makes
QStyleSheetStyle resolve it again. Call after setPalette().
*/
void refreshStyleSheets();
}
}
+24
View File
@@ -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()
+11 -4
View File
@@ -67,20 +67,27 @@ bool QETStyle::isLineArt(const QImage &image)
/**
@brief QETStyle::hoverColor
The palette's highlight color is the accent users already know from
selections. On a dark palette it is too dark to read on a hovered
button face, so it is lightened, a step at a time, until it reaches
selections, moved away from the hovered button face until it reaches
3:1 (WCAG 1.4.11) against the Light role: Fusion paints a hovered
auto-raise button with a gradient that runs from Button up to about
that color, and the icon has to read on the lightest part of it.
that color, and the icon has to read on the lightest part of it. On a
dark face the accent is lightened, a step at a time; on a light face it
is darkened, which keeps a pale accent (macOS's green or yellow
selection color, which comes with black selection text) from being
pushed to white. Should twenty steps not get there, the button text
color serves, which reads on the face by construction.
*/
QColor QETStyle::hoverColor(const QPalette &palette)
{
const QColor face = palette.color(QPalette::Active, QPalette::Light);
const bool light_face = face.lightnessF() > 0.5;
QColor ink = palette.color(QPalette::Active, QPalette::Highlight);
// 3.5 rather than 3.0: the top of Fusion's hover gradient is a shade
// lighter than the Light role, so the icon needs some headroom there.
for (int step = 0; step < 20 && QET::Palette::contrastRatio(ink, face) < 3.5; ++step)
ink = ink.lighter(110);
ink = light_face ? ink.darker(110) : ink.lighter(110);
if (QET::Palette::contrastRatio(ink, face) < 3.5)
ink = palette.color(QPalette::Active, QPalette::ButtonText);
return ink;
}
+173
View File
@@ -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
+21
View File
@@ -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;
}
+7 -16
View File
@@ -55,21 +55,13 @@ m_diagram(diagram)
{
openDialog();
QString text;
if(texts_list.count())
text.append(QObject::tr("Pivoter %1 textes").arg(texts_list.count()));
if(groups_list.count())
{
if(text.isEmpty())
text.append(QObject::tr("Pivoter"));
else
text.append(QObject::tr(" et"));
text.append(QObject::tr(" %1 groupes de textes").arg(groups_list.count()));
}
if(!text.isNull())
setText(text);
QStringList parts;
if (texts_list.count())
parts << QObject::tr("%n texte(s)", "", texts_list.count());
if (groups_list.count())
parts << QObject::tr("%n groupe(s) de textes", "", groups_list.count());
setText(QObject::tr("Pivoter %1").arg(QLocale().createSeparatedList(parts)));
for(DiagramTextItem *dti : texts_list)
setupAnimation(dti, "rotation", dti->rotation(), m_rotation);
for(ElementTextItemGroup *grp : groups_list)
@@ -77,7 +69,6 @@ m_diagram(diagram)
}
else
setObsolete(true);
}
void RotateTextsCommand::undo()
+12 -2
View File
@@ -157,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
@@ -176,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
+2
View File
@@ -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));
}
+79
View File
@@ -63,10 +63,13 @@ class tst_qeticons : public QObject
void toolbarIconIsReadable();
void hoverChangesTheIcon_data();
void hoverChangesTheIcon();
void hoverInkReadsWithAnyAccent_data();
void hoverInkReadsWithAnyAccent();
void coloredIconKeepsItsColorsOnHover();
void menuIconReadsOnHighlight_data();
void menuIconReadsOnHighlight();
void panelProjectIconStaysSmall();
void configPageIconsComeAtPageSize();
};
namespace {
@@ -106,6 +109,17 @@ namespace {
}
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;
}
}
void tst_qeticons::initTestCase()
@@ -333,6 +347,12 @@ void tst_qeticons::hoverChangesTheIcon_data()
// 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;
}
/**
@@ -472,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"))
+202
View File
@@ -29,11 +29,13 @@
#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"
@@ -84,6 +86,11 @@ class tst_qetpalette : public QObject
void paletteViewDrawsTheRubberBand();
void paletteViewFollowsTheApplicationUnderAStyleSheet();
void paletteViewFillsWhatTheSceneLeavesBlank();
void paletteViewErasesMovedChildren_data();
void paletteViewErasesMovedChildren();
void paletteViewErasesChildrenMovedWhilePainting_data();
void paletteViewErasesChildrenMovedWhilePainting();
void styleSheetWidgetsFollowPaletteChange();
private:
static void addPaletteRows();
@@ -571,6 +578,37 @@ namespace {
}
};
/**
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.
@@ -801,6 +839,112 @@ void tst_qetpalette::paletteViewFillsWhatTheSceneLeavesBlank()
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
@@ -815,6 +959,64 @@ void tst_qetpalette::invertLightnessSpeed()
}
}
/**
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