Compare commits

..

37 Commits

Author SHA1 Message Date
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
Laurent Trinques 3394c1246c Merge pull request #954 from jp2images/feature-dark-canvas
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m12s
Draw the folio with inverted lightness on a dark palette
2026-09-20 06:44:09 +02:00
Jeff Patterson 3e34a6d55f Fill the dark canvas buffer and keep one scene connection
QGraphicsView::render() paints only what the scene draws, so a scene
without a background brush left the off-screen buffer uninitialized
and the inversion turned that memory into noise. The buffer is now
filled white first, which the inversion turns into the Base color.

listenToScene() connected a new receiver on every setScene() call and
never dropped the previous scene's. It now keeps a single connection
and replaces it.

Test in tst_qetpalette: paletteViewFillsWhatTheSceneLeavesBlank.
2026-09-19 18:27:00 -05:00
Jeff Patterson 50792ba1ad Repaint the whole dark canvas when the application palette changes
After a live light/dark switch only the sheet changed colors; the
viewport around it kept the previous palette. Qt repaints a widget on
an application palette change only when the widget's own palette
changed with it, and under the folio tab widget's style sheet it does
not, so the only repaints came from the scene and covered the scene
rectangle alone. PaletteGraphicsView now watches the application
object for ApplicationPaletteChange, the one receiver Qt always
notifies, and repaints its whole viewport.

The test paletteViewFollowsTheApplicationUnderAStyleSheet now also
requires a full-viewport repaint after each switch, before anything
asks the view for a rendering.
2026-09-19 18:27:00 -05:00
Jeff Patterson 6404612014 Follow the application palette, not the view's, on the dark canvas
The folio tab widget carries a style sheet, and QStyleSheetStyle pins
the palette of every widget under it to the application palette in
force when the sheet was applied. After a live light/dark switch the
view's own palette() is therefore stale: the folio kept its dark sheet
after a switch to light, and kept its white sheet after a switch to
dark. PaletteGraphicsView now reads the application palette both for
the decision to invert and for the sheet and ink colors.

Test in tst_qetpalette: paletteViewFollowsTheApplicationUnderAStyleSheet
puts the view in a tab widget with a style sheet and switches the
application palette to dark and back.
2026-09-19 18:27:00 -05:00
Jeff Patterson eaf15faaa3 Move the dark canvas into PaletteGraphicsView and test it directly
The inverted painting, the rubber band replay and the changed()
receiver lived in DiagramView, which the unit tests cannot link, so the
update-flag regression was only covered through a stand-in view. They
now live in PaletteGraphicsView, a QGraphicsView subclass with no
other dependency, and DiagramView derives from it. The view tells a
subclass through paintingInverted(bool) when it renders for an
inverted display; DiagramView forwards that to the diagram. The grid
dot rule moves out of Diagram::drawBackground into
QET::Palette::gridDotColor().

tst_qetpalette now links the real class: gridDotColorSoftensInvertedDots,
paletteViewFollowsThePalette (light sheet, dark sheet at text contrast
with a red box still red and the paintingInverted calls in order, back
to light), paletteViewKeepsSceneUpdatesFlowing (three whole-scene
updates and a selection each repaint, scene set after construction),
paletteViewDrawsTheRubberBand.
2026-09-19 18:27:00 -05:00
Jeff Patterson b8c9e670c4 Keep scene updates flowing on the dark canvas and soften its grid
On a dark palette the folio view paints through QGraphicsView::render()
instead of onto its viewport. In that case QGraphicsView never clears
the scene's "update everything" flag, and while the flag is set every
further QGraphicsScene::update() and item update is dropped: from the
second Diagram::update() on, the grid toggle, the white/gray toggle and
even a selection waited for an unrelated repaint. With a receiver on
QGraphicsScene::changed() the scene clears the flag before it emits, so
DiagramView now connects an empty receiver in its constructor.

While the view paints for inversion, Diagram draws the grid dots a
third of the way from the sheet color to black, so they come out as a
soft gray on the dark sheet instead of as bright as the ink. Printing
and export never take that path.

Test in tst_qetpalette: sceneUpdatesReachARenderedView.
2026-09-19 18:26:45 -05:00
Jeff Patterson 85dc638a42 Draw the folio with inverted lightness on a dark palette
On a dark palette the folio stayed a white sheet with black ink, and
the white/gray toggle only darkened the sheet while the ink stayed
black. DiagramView now renders each repaint into an image and inverts
its lightness before blitting it: white becomes the palette's Base,
black becomes its Text, and colored conductors and elements keep their
hue. The document, printing and export are untouched; only the screen
rendering changes, and only while the palette is dark.

QET::Palette::invertLightness does the inversion in one integer pass
(adding 255 - max - min to the three channels inverts the HSL lightness
and keeps hue and saturation), then stretches the result between the
sheet and ink colors through three lookup tables. A 4K viewport costs
about 9 ms in a release build. QGraphicsView::render() skips the
selection rubber band, so the view draws it again after the inversion.

Tests in tst_qetpalette: invertLightnessMapsSheetAndInk,
invertedViewReadsOnDarkSheet, invertLightnessSpeed.
2026-09-19 18:26:45 -05:00
Laurent Trinques dfe56cddbd Merge pull request #928 from ispyisail/fix/903-escape-cancels-text-tool
Let Escape cancel the text tool, like every other placement (#903)
2026-09-19 23:43:31 +02:00
Laurent Trinques ef1fbd79b4 Merge pull request #942 from elevatormind/gitignore-ide-addition
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m1s
chore: ignore IDE settings and cache files
2026-09-19 20:25:12 +02:00
Laurent Trinques eb7abdfbc6 Merge pull request #946 from jp2images/feature-panels-follow-palette
Let the Collections and Projects panels follow the palette
2026-09-19 20:24:05 +02:00
Laurent Trinques f574d28340 Merge pull request #947 from jp2images/fix-dark-theme-light-icons
Redraw the Add PDF and folio icons as SVGs, stop inverting page-shaped icons in the dark theme
2026-09-19 20:22:29 +02:00
Laurent Trinques 31f90e2c46 Merge pull request #949 from jp2images/fix-menubar-test-macos
Make the menu bar keyboard test pass on macOS
2026-09-19 20:20:58 +02:00
Laurent Trinques 45bd8256b3 Merge pull request #950 from jp2images/feature-icon-hover
Give icons a hover state through a proxy style
2026-09-19 20:18:20 +02:00
Jeff Patterson 4e43cde063 Give icons a hover state through a proxy style
Hovering a tool button changed only its frame, and on the dark palette
Fusion's hover frame is too faint to notice, so nothing told the user
which button was under the pointer (GitHub #870, PR 3 of the plan).

QETStyle wraps the running style. For QIcon::Active it returns line-art
icons tinted in the palette's highlight color, lightened until the tint
reads at 3:1 on the Light role, which is the top of Fusion's hover
gradient. Colored icons keep their colors, using the same line-art rule
as misc/make_icon_themes.py. Every other mode goes 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 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 in the HighlightedText color instead.

QETApp::initStyle() installs the proxy on every platform, keeping the
base style's object name so the Fusion checks still match.

tests/qttest/tst_qeticons: hovering changes the icon ink to the tint and
stays at 3:1 on both palettes, on a raised button and on a checked one
drawn sunken, where the frame gives no hover cue at all; a colored icon is unchanged on hover; a
highlighted menu item's icon reads at 3:1 on the highlight bar.
inkcontrast.h gains background() and ink() helpers for those checks.
2026-09-19 12:26:06 -05:00
Jeff Patterson aef56fe41d Make the menu bar keyboard test pass on macOS
tst_menubarkeyboard's control case presses Alt+F and expects the File
menu to open. On macOS it never did, for two reasons unrelated to the
F10 shortcut it guards: a QMenuBar is native there, so its menus live
in the system menu bar where QTest key events do not reach them, and Qt
does not turn "&File" into an Alt+F mnemonic on macOS at all. The test
has failed on every Mac build since bd6bed8d6 added it.

On macOS the test now uses an in-window menu bar and switches auto
mnemonics on for its own process, which runs the same QMenuBar code the
other platforms exercise. What it still does not prove on macOS is the
native bar: QETMainWindow::activateMenuBar() calls setActiveAction() on
a bar the system draws, and only F10 in the running application can
say what that does there. Other platforms are unchanged.

Fixes #948.
2026-09-19 12:24:57 -05:00
Jeff Patterson 0395f90f75 Let the Collections and Projects panels follow the palette
Both panels forced a light palette on themselves (white rows, black
text, their own selection blue) so that element previews, which are
black line art drawn for the white sheet, would stay visible on a dark
desktop (bugtracker 335). On a dark palette the two docks were the
only white windows left.

The forced palettes are gone. Element previews are now kept as drawn,
on a transparent background, and adapted where they are shown:
ElementPreviewDelegate, installed on the collection tree, hands the
view a copy with its lightness inverted when the palette is dark
(QET::Palette::forPalette), so black ink becomes the palette's light
gray while colored icons such as folders stay as they are; the drag
pixmap is adapted the same way. A light palette shows the previews
untouched. This fixes bugtracker 335 on every dark desktop rather than
masking it with a white panel.

The preview cache stored the old white-sheet pictures; it records the
format now and drops a cache written before this change once. The
amber "show this directory" highlight sets black text so it reads on
both palettes. The Projects panel only shows icons from the icon
theme, which has a dark variant, so nothing else changes there.

tests/qttest/tst_qetpalette: the line-art rule tells ink from color;
inversion keeps hue and alpha; a preview reads at 3:1 on the Base
color of both palettes; in a tree on the dark palette the delegate
inverts a line-art icon and leaves a colored one alone.

Fixes #945.
2026-09-19 12:22:24 -05:00
Jeff Patterson a4d5b7a12a Draw the folio icons as SVGs
The folio icons (Add, Remove, Properties, New folio, Title block
template) were anti-aliased gray page drawings, and the previous commit
left them untouched on the dark palette, where their soft gray fills
read blurry next to the line-art icons. They are now pixel-grid SVGs in
ico/scalable/ in the style of the Add PDF icon: a landscape sheet with
a title block line, a plus or minus badge in the corner, text lines for
properties, a filled title block for the template. Same 24 pixel canvas
as pdf-import.svg, same currentColor recoloring for the dark theme.

Only the 22 pixel PNGs go: five files leave ico/22x22 and both .qrc
files, and the alias list in misc/make_icon_themes.py that exposed
three of them under a second name is down to conductor2.png. The 16
pixel files stay, so menus and the projects panel keep their icons at
that size, and the 128 pixel diagram.png stays for the configuration
page list.

tests/qttest/tst_qeticons: every file in ico/scalable/ resolves in both
themes at 22, 24, 32 and 64 pixels, dark ink on light and light ink on
dark, with no 22 pixel PNG left beside it; the light-art check reads
the folio family at 16 pixels, where the page art remains.
2026-09-19 12:02:55 -05:00
Jeff Patterson ba29dc45a0 Draw the Add PDF icon as an SVG
The "Add PDF" action used ico/22x22/pdf-import.png, a white page with a
red PDF mark that sat apart from its neighbors "Add text" and "Add
image", both gray line art in a square frame with a plus. It is now
ico/scalable/pdf-import.svg, the same pixel design as insert-image.png:
a frame, the letters PDF, a plus in the corner. One file serves every
slot and stays sharp on high-DPI screens; the PNG is gone from both
.qrc files.

The canvas is 24 pixels with the art offset by one, like the Breeze
SVGs already in the theme. Fusion's toolbar slot is 24 pixels: a 22
pixel PNG is drawn unscaled inside it, but a scalable icon is rendered
at the slot size, and a 22 pixel grid stretched to 24 puts every one
pixel line between pixels and reads blurry.

The file uses currentColor like the Breeze SVGs already in the theme,
so misc/make_icon_themes.py produces the dark copy the same way. A new
ico/scalable/ folder holds QET's own vector icons; the direction
arummler asked for in #690.

tests/qttest/tst_qeticons: the icon resolves in both themes at 16, 22,
32 and 64 pixels, dark ink on the light theme and light ink on the dark
one, and no 22 pixel PNG remains.
2026-09-19 12:02:54 -05:00
Jeff Patterson d06c7be606 Leave light icons out of the dark theme
misc/make_icon_themes.py sorted icons by saturation alone, so a white
page with a small red mark counted as line art and its dark copy turned
the page black: the PDF import icon read black on black (#919,
Kellermorph), and the folio, diagram and label icons came out as dark
pages with a light border.

An icon whose visible pixels are at least 30% near white is now "light
art" and inherits from the qet theme untouched; it already reads on a
dark toolbar. The generator also removes dark files it no longer
produces, so a reclassified icon falls back to the light theme instead
of keeping a stale copy. Thirteen files leave ico/themes/qet-dark.

tests/qttest/tst_qeticons: every dark theme file, taken as its mean
visible color, reaches 3:1 on the dark palette's window color; the
lightest-pixel check it replaces let a black page with a light border
through. Asking the dark theme for pdf-import, diagram, label, the
folio icons and diagram_bg returns the light art.
2026-09-19 12:02:54 -05:00
Magnus Hellströmer 56cbd6e23d chore: ignore IDE settings and cache files 2026-09-19 18:10:37 +02:00
Laurent Trinques 8bbc2da5c7 Merge pull request #940 from Kellermorph/german-translation
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m17s
Update German translation
2026-09-19 14:11:15 +02:00
Kellermorph 4b8db6be0e Update German translation 2026-09-19 13:25:39 +02:00
Laurent Trinques a6b4c3c673 Merge pull request #930 from ispyisail/feat/923-forum-auto-conductor-shortcut
List auto conductor creation in the Shortcuts page
2026-09-19 13:04:44 +02:00
Laurent Trinques 8f0ee06a4f Merge pull request #929 from ispyisail/feat/461-conductor-color-quick-access
Add a one-click conductor colour to the toolbar (#461)
2026-09-19 13:00:14 +02:00
ispyisail 70599c4ae1 List auto conductor creation in the Shortcuts page
"Est il possible dans les raccourcis d'ajouter un pour création
automatique de conducteur ? Je n'utilise pas par défaut, mais
ponctuellement c'est très pratique." -- oc67, an electrician, on the
forum (viewtopic.php?pid=23296).

The action itself has existed for a long time: m_auto_conductor is a
checkable QAction in the Schéma toolbar and the Project menu. It was
simply never handed to ShortcutManager, so it did not appear in
Configuration > Raccourcis and there was no way to reach it from the
keyboard. This registers it.

No default sequence is set. That is the request read literally -- he
asked for it to be *in* the shortcuts list so he can bind it himself --
and it avoids spending one of the few free keys on a setting many people
never touch. The Shortcuts page already treats "no shortcut" as a normal
state: it renders an empty field and its quick filter can list actions
with and without a binding separately.

Verified on a virtual display. With shortcuts/diagrameditor.auto_conductor
set to Ctrl+Alt+A:

  Configuration > Raccourcis, filtered on "conducteur", lists
  "Création automatique de conducteur(s)" under "Éditeur de schémas"
  showing that binding.

  Mouse parked away from the toolbar, pressing it twice: the toolbar
  button changes on each press and returns to its starting appearance
  after the second, so the key toggles the setting exactly as clicking
  the button does.

Two things that misled the first run, recorded so the next person does
not repeat them: F7 is already registered to panel.move_diagram_downx100
in the elements panel, and a screenshot taken with the pointer resting on
the button shows its hover state, not its checked state.

ctest 12/12, Qt 6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CaKympWT3owLotCpEN2CFj
2026-09-19 09:18:04 +12:00
ispyisail e42ebdc861 Add a conductor colour button to the Schéma toolbar (#461)
An electrician on the forum draws 400 V and 24 V circuits in the same
folio and wants the colour to be one click away
(qelectrotech.org/forum, viewtopic pid=23296). Today the quickest route
is F2, which opens a colour dialog and refuses to act unless exactly one
conductor is selected, so colouring a run means one conductor, one
dialog, at a time.

This adds a swatch button beside the auto-conductor actions. Picking a
colour does two things:

  - recolours every conductor currently selected, as ONE undo step;
  - becomes the colour of the next conductor drawn, through the
    LastUsedStyle mechanism #888 already added and Conductor's
    constructor already reads.

Either half is useful alone: with nothing selected it just sets the pen
for what comes next.

The menu lists the colours the trade names -- the three phases, neutral,
earth, and the ones used for control and extra-low-voltage circuits --
then any custom colours picked this session, then the full colour
dialog. A colour already in the standard list is not repeated under
"recently used".

Nothing is written to the project or to QSettings. That is deliberate:
it is the same session-scoped "what did I just use" idea as
LastUsedStyle, so it adds no persisted state and no file-format change.
Named presets stored per project -- what #461 actually asks for -- are a
larger feature that needs a maintainer decision first; the question is
still open on that issue since 21 June.

Verified on a virtual display against examples/Habitat-Schemas_developpes.qet,
reading colours back from the saved project rather than the screen:

  select all on folio 1, pick Rouge
      21 conductors {none:1, #ff5500:2, #ff0000:6, #00aa00:5, #0000ff:7}
      -> all 21 #ff0000
  one Ctrl+Z
      -> back to the original five-colour mix, exactly
  pick Marron with nothing selected, then draw a conductor
      -> the new conductor is #7b3f00

ctest 12/12, Qt 6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CaKympWT3owLotCpEN2CFj
2026-09-19 09:02:58 +12:00
ispyisail 3c1fb2fe72 Let Escape cancel the text tool, like every other placement (#903)
Issue #903 reported that Escape stopped cancelling an in-progress
placement. PR #899 fixed that by letting Escape through to the active
tool whenever Diagram::eventInterfaceIsRunning(), and 863ac5f0a tightened
it to isRunning() so a second Escape cannot retrigger an abort already in
flight. That covers seven of the eight tools. It cannot cover the eighth.

eventInterfaceIsRunning() is m_event_interface && isRunning(), and
isRunning() returns m_running. DiagramEventAddText is the only class
under sources/diagramevent/ that never sets m_running, so the guard reads
false for the whole time the tool is armed and DiagramView::keyPressEvent
keeps swallowing Escape for its own selection/focus handling.

It is also the only one of the eight with no RightButton branch --
right-clicking the folio with the text tool armed opens the folio context
menu. So this tool currently has no way to cancel at all: the only way
out is to pick a different tool, and any stray click drops a text field
the user did not want.

The tool is armed from the moment it is attached, so m_running is set in
the constructor and cleared where the text is placed, before finish().

Measured on a virtual display against a fixture holding one free text
field, counting diagram-level text fields in the saved project:

  arm the text tool, click            2 fields   places, as it should
  arm it, press Escape, then click    2 fields   before  -- not cancelled
                                      1 field    after   -- cancelled

The same run on master's rectangle tool cancels correctly, which is what
made the text tool look fixed when it was not.

ctest 12/12, Qt 6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 08:32:58 +12:00
72 changed files with 2553 additions and 315 deletions
+8
View File
@@ -8,3 +8,11 @@ doc/*
QElectroTech.tag
!doc/doc-utils
lang/*.qm
# -- IDE settings/intermediate files --
# zed
.zed
.cache
# VS Code
.vscode
+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
+8
View File
@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

+2
View File
@@ -216,6 +216,8 @@ 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/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)
+12 -25
View File
@@ -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>
@@ -274,6 +265,12 @@
<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/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 +287,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 +327,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 +341,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 +373,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 +409,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 +419,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 +454,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>
+12
View File
@@ -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

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

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

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

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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 667 B

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

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

+195 -190
View File
File diff suppressed because it is too large Load Diff
+45 -9
View File
@@ -38,6 +38,13 @@ 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 +71,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,6 +97,8 @@ 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"
@@ -107,6 +121,16 @@ 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 invert_lightness(image):
"""Invert lightness, keeping hue, saturation and alpha.
@@ -177,7 +201,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 +212,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 +240,15 @@ def main():
changed += write_if_changed(target, text)
dark.append(f"themes/qet-dark/scalable/{name}")
# 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))
@@ -231,8 +267,8 @@ def main():
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__":
-6
View File
@@ -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
+4 -2
View File
@@ -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);
+14
View File
@@ -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.
+10 -1
View File
@@ -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);
}
+14 -2
View File
@@ -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)
{
+3 -2
View File
@@ -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;
+12
View File
@@ -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
+2 -18
View File
@@ -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);
+7 -6
View File
@@ -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);
+184
View File
@@ -0,0 +1,184 @@
/*
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 <QPainter>
#include <QPaintEvent>
#include <QStyleHintReturnMask>
#include <QStyleOptionRubberBand>
#include <QtMath>
PaletteGraphicsView::PaletteGraphicsView(QWidget *parent) :
QGraphicsView(parent)
{
qApp->installEventFilter(this);
setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
}
PaletteGraphicsView::PaletteGraphicsView(QGraphicsScene *scene, QWidget *parent) :
QGraphicsView(scene, parent)
{
qApp->installEventFilter(this);
setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
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);
}
/**
@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::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
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->rect());
else
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
*/
void PaletteGraphicsView::paintInverted(const QRect &area)
{
const QRect rect = area.intersected(viewport()->rect());
if (rect.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
// the white sheet, which the inversion turns into the Base color.
buffer.fill(Qt::white);
QPainter buffer_painter(&buffer);
buffer_painter.setRenderHints(renderHints());
paintingInverted(true);
render(&buffer_painter, QRectF(QPointF(0, 0), QSizeF(rect.size())),
rect, Qt::IgnoreAspectRatio);
paintingInverted(false);
buffer_painter.end();
// The application palette, for the reason given in invertsLightness().
const QPalette application_palette = QApplication::palette();
QET::Palette::invertLightness(buffer, application_palette.color(QPalette::Base),
application_palette.color(QPalette::Text));
QPainter painter(viewport());
painter.drawImage(rect.topLeft(), buffer);
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.
@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());
}
+85
View File
@@ -0,0 +1,85 @@
/*
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>
class 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.
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.
The constructor also forces QGraphicsView::FullViewportUpdate in place
of the default MinimalViewportUpdate. #954 shipped with the default
kept, and moving an item then left conductor-shaped ghosts behind on
both a light and a dark palette, so the cause is shared code, not
paintInverted(): most likely listening to changed() at all, above,
changes which of QGraphicsScene's two update paths a view is on, and
MinimalViewportUpdate's job of turning the scene's reported dirty
rects into the smallest correct viewport region is where that would
show up first. FullViewportUpdate removes the need to get that region
right by repainting the whole viewport on every update; the class's
own benchmark already shows that cost is small next to a frame budget.
*/
class PaletteGraphicsView : public QGraphicsView
{
Q_OBJECT
public:
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;
/**
Called with true right before the scene is rendered 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 listenToScene(QGraphicsScene *scene);
void paintInverted(const QRect &area);
void drawRubberBand(QPainter &painter);
QMetaObject::Connection m_scene_connection;
};
#endif
+8
View File
@@ -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"
@@ -2368,6 +2370,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
+16
View File
@@ -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();
}
}
/**
+4
View File
@@ -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);
+119
View File
@@ -17,7 +17,10 @@
*/
#include "qetpalette.h"
#include <QColor>
#include <QImage>
#include <QStyle>
#include <cmath>
namespace {
@@ -74,6 +77,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 +231,68 @@ 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;
}
+48
View File
@@ -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,29 @@ 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);
}
}
+180
View File
@@ -0,0 +1,180 @@
/*
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. 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
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.
*/
QColor QETStyle::hoverColor(const QPalette &palette)
{
const QColor face = palette.color(QPalette::Active, QPalette::Light);
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);
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, &copy, 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));
}
+73
View File
@@ -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
+265
View File
@@ -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);
}
+69
View File
@@ -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
+18 -3
View File
@@ -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 -12
View File
@@ -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)));
}
}
}
+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));
}
+26
View File
@@ -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
+288 -19
View File
@@ -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,9 +56,16 @@ 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 coloredIconKeepsItsColorsOnHover();
void menuIconReadsOnHighlight_data();
void menuIconReadsOnHighlight();
void panelProjectIconStaysSmall();
};
@@ -70,18 +86,26 @@ 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"};
}
void tst_qeticons::initTestCase()
@@ -111,27 +135,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 +284,169 @@ 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;
}
/**
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
+507
View File
@@ -19,16 +19,25 @@
#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 <QStandardItemModel>
#include <QTreeView>
#include <QStyleFactory>
#include <QTabWidget>
#include <QToolBar>
#include "inkcontrast.h"
#include "ElementsCollection/elementpreviewdelegate.h"
#include "palettegraphicsview.h"
#include "qetpalette.h"
using QET::Palette::contrastRatio;
@@ -62,6 +71,19 @@ 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();
private:
static void addPaletteRows();
@@ -308,6 +330,491 @@ 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 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);
}
/**
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));
}
}
int main(int argc, char **argv)
{
// Widgets are painted for real, on Qt's offscreen platform so the test