Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 88 additions & 3 deletions src/silx/gui/icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@
__date__ = "07/01/2019"


import os
import logging
import os
import re
import weakref
from . import qt
import silx.resources
Expand Down Expand Up @@ -70,6 +71,86 @@ def cleanIconCache():
"""Order of file format extension to check"""


class _SvgIconEngine(qt.QIconEngine):
"""Icon engine for SVG icons which tweaks color according to color scheme."""

def __init__(self, name: str):
super().__init__()
self._lightSvgPath = self._readSVGPath(name)
self._isDarkColorScheme = False
self._renderer = qt.QSvgRenderer(self._lightSvgPath)

@staticmethod
def _readSVGPath(name: str) -> bytes:
filename = silx.resources._resource_filename(
f"{name}.svg", default_directory="gui/icons"
)
qfile = qt.QFile(filename)
if not qfile.exists():
raise ValueError(f"SVG icon resource not found: {name}")

if not qfile.open(qt.QIODevice.ReadOnly | qt.QIODevice.Text):
raise ValueError(f"Cannot open SVG icon resource: {name}")

svgpath = bytes(qfile.readAll())
qfile.close()
return svgpath

@property
def _darkSvgPath(self) -> bytes:
palette = qt.QApplication.palette()
foregroundQColor = palette.color(qt.QPalette.Active, qt.QPalette.Text)
foreground = foregroundQColor.name()

svgpath = self._lightSvgPath.replace(
b"<svg", f'<svg fill="{foreground}"'.encode()
)
svgpath = re.sub(
b'fill="(#000000|#000)"', f'fill="{foreground}"'.encode(), svgpath
)
Comment on lines +108 to +110

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

svg default fill is black and this can be overridden in with fill declared in the <svg> tag.
To complete this PR, the idea is to patch existing icons to remove unnecessary fill="#000" so this substitution is not needed. This would allow to explicitly set a fill color to black even in dark mode.

svgpath = re.sub(
b'stroke="(#000000|#000)"', f'stroke="{foreground}"'.encode(), svgpath
)
Comment on lines +111 to +113

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

svg default stroke is "none" so overriding the default in <svg> does not work as for fill....

return svgpath

def _currentRenderer(self) -> qt.QSvgRenderer:
if qt.BINDING != "PyQt5":
isDark = (
qt.QApplication.styleHints().colorScheme() == qt.Qt.ColorScheme.Dark
)
else:
isDark = False

if isDark != self._isDarkColorScheme:
self._renderer.load(self._darkSvgPath if isDark else self._lightSvgPath)
self._isDarkColorScheme = isDark

Comment on lines +124 to +127

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reload icon only when needed

return self._renderer

def paint(
self,
painter: qt.QPainter,
rect: qt.QRect,
mode: qt.QIcon.Mode,
state: qt.QIcon.State,
):
renderer = self._currentRenderer()
if renderer.isValid():
renderer.render(painter, rect)

def pixmap(self, size: qt.QSize, mode: qt.QIcon.Mode, state: qt.QIcon.State):
pixmap = qt.QPixmap(size)
pixmap.fill(qt.Qt.transparent)

painter = qt.QPainter(pixmap)
self.paint(painter, qt.QRect(0, 0, size.width(), size.height()), mode, state)
painter.end()
return pixmap

def clone(self):
return _SvgIconEngine(self._lightSvgPath)


class AbstractAnimatedIcon(qt.QObject):
"""Store an animated icon.

Expand Down Expand Up @@ -333,8 +414,12 @@ def getQIcon(name):
"""
cached_icons = getIconCache()
if name not in cached_icons:
qfile = getQFile(name)
icon = qt.QIcon(qfile.fileName())
try:
icon = qt.QIcon(_SvgIconEngine(name))
except ValueError:
_logger.warning("Failed to load SVG icon")
qfile = getQFile(name)
icon = qt.QIcon(qfile.fileName())
Comment on lines +421 to +422

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept previous implementation for other application using silx icon mechanism but in silx we can consider completely remove .png icons.

cached_icons[name] = icon
else:
icon = cached_icons[name]
Expand Down
Loading