Skip to content

Commit

Permalink
Merge pull request #271 from ales-erjavec/color-styled-icons
Browse files Browse the repository at this point in the history
[ENH] Color styled widget icons
  • Loading branch information
PrimozGodec authored Jun 7, 2023
2 parents 9c7d676 + 43b52f2 commit 1c90038
Show file tree
Hide file tree
Showing 16 changed files with 473 additions and 107 deletions.
2 changes: 2 additions & 0 deletions orangecanvas/application/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import AnyQt
from AnyQt.QtWidgets import QApplication
from AnyQt.QtGui import QPixmapCache
from AnyQt.QtCore import (
Qt, QUrl, QEvent, QSettings, QLibraryInfo, pyqtSignal as Signal,
QT_VERSION_INFO
Expand Down Expand Up @@ -231,6 +232,7 @@ def configureStyle():
if theme and theme in styles.colorthemes:
palette = styles.colorthemes[theme]()
QApplication.setPalette(palette)
QPixmapCache.setCacheLimit(64 * (2 ** 10))


__restart_command: Optional[List[str]] = None
Expand Down
28 changes: 22 additions & 6 deletions orangecanvas/application/canvastooldock.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@

from AnyQt.QtWidgets import (
QWidget, QSplitter, QVBoxLayout, QAction, QSizePolicy, QApplication,
QToolButton, QTreeView)
from AnyQt.QtGui import QPalette, QBrush, QDrag, QResizeEvent, QHideEvent

QToolButton, QTreeView
)
from AnyQt.QtGui import (
QPalette, QBrush, QDrag, QResizeEvent, QHideEvent, QPaintEvent
)
from AnyQt.QtCore import (
Qt, QSize, QObject, QPropertyAnimation, QEvent, QRect, QPoint,
QAbstractItemModel, QModelIndex, QPersistentModelIndex, QEventLoop,
QMimeData
)
from AnyQt.QtCore import pyqtProperty as Property, pyqtSignal as Signal

from ..gui.toolgrid import ToolGrid
from .. import styles
from ..gui.iconengine import StyledIconEngine
from ..gui.toolgrid import ToolGrid, ToolGridButton
from ..gui.toolbar import DynamicResizeToolBar
from ..gui.quickhelp import QuickHelp
from ..gui.framelesswindow import FramelessWindow
Expand Down Expand Up @@ -299,6 +303,12 @@ def toogleQuickHelpAction(self):
return self.toggleQuickHelpAction()


class _ToolGridButton(ToolGridButton):
def paintEvent(self, event: QPaintEvent) -> None:
with StyledIconEngine.setOverridePalette(styles.breeze_light()):
super().paintEvent(event)


class QuickCategoryToolbar(ToolGrid):
"""A toolbar with category buttons."""
def __init__(self, parent=None, buttonSize=QSize(), iconSize=QSize(),
Expand Down Expand Up @@ -355,8 +365,14 @@ def createButtonForAction(self, action):
"""
Create a button for the action.
"""
button = super().createButtonForAction(action)

button = _ToolGridButton(
self,
toolButtonStyle=self.toolButtonStyle(),
iconSize=self.iconSize(),
)
button.setDefaultAction(action)
if self.buttonSize().isValid():
button.setFixedSize(self.buttonSize())
item = action.data() # QPersistentModelIndex
assert isinstance(item, QPersistentModelIndex)

Expand Down
5 changes: 2 additions & 3 deletions orangecanvas/application/widgettoolbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from ..gui.toolbox import ToolBox
from ..gui.toolgrid import ToolGrid
from ..gui.quickhelp import StatusTipPromoter
from ..gui.utils import create_gradient
from ..gui.utils import create_gradient_brush
from ..registry.qt import QtWidgetRegistry


Expand Down Expand Up @@ -425,8 +425,7 @@ def __insertItem(self, item, index):
if isinstance(highlight, QBrush) and highlight.style() != Qt.NoBrush:
if not highlight.gradient():
value = highlight.color().value()
gradient = create_gradient(highlight.color())
highlight = QBrush(gradient)
highlight = create_gradient_brush(highlight.color())
highlight_foreground = Qt.black if value > 128 else Qt.white

palette = button.palette()
Expand Down
9 changes: 8 additions & 1 deletion orangecanvas/canvas/items/nodeitem.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
from .utils import (
saturated, radial_gradient, linspace, qpainterpath_sub_path, clip
)
from ... import styles
from ...gui.iconengine import StyledIconEngine
from ...gui.utils import disconnected

from ...scheme.node import UserMessage
Expand Down Expand Up @@ -1152,7 +1154,10 @@ def paint(self, painter, option, widget=None):
QPainter.SmoothPixmapTransform,
self.__transformationMode == Qt.SmoothTransformation
)
self.__icon.paint(painter, target, Qt.AlignCenter, mode)
palette = self.palette()
# assuming light-ish background color
with StyledIconEngine.setOverridePalette(palette):
self.__icon.paint(painter, target, Qt.AlignCenter, mode)


class NodeItem(QGraphicsWidget):
Expand Down Expand Up @@ -1335,6 +1340,8 @@ def setIcon(self, icon):
self.icon_item = GraphicsIconItem(
self.shapeItem, icon=icon, iconSize=QSize(36, 36)
)
# assuming light-ish color background
self.icon_item.setPalette(styles.breeze_light())
self.icon_item.setPos(-18, -18)

def setColor(self, color, selectedColor=None):
Expand Down
22 changes: 13 additions & 9 deletions orangecanvas/document/quickmenu.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@
from AnyQt.QtCore import pyqtSignal as Signal, pyqtProperty as Property

from .usagestatistics import UsageStatistics
from .. import styles
from ..gui.framelesswindow import FramelessWindow
from ..gui.lineedit import LineEdit
from ..gui.iconengine import StyledIconEngine
from ..gui.tooltree import ToolTree, FlattenedTreeItemModel
from ..gui.utils import StyledWidget_paintEvent, innerGlowBackgroundPixmap, innerShadowPixmap
from ..registry.qt import QtWidgetRegistry
Expand Down Expand Up @@ -90,7 +92,8 @@ def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIn
decRect = QRectF(decTl, decBr)

# draw icon pixmap
dec.paint(painter, decRect.toAlignedRect())
with StyledIconEngine.setOverridePalette(styles.breeze_light()):
dec.paint(painter, decRect.toAlignedRect())

# draw display
rect = QRect(opt.rect)
Expand Down Expand Up @@ -697,15 +700,16 @@ def paintEvent(self, event):
self.initStyleOption(opt)
if self.__showMenuIndicator and self.isChecked():
opt.features |= QStyleOptionToolButton.HasMenu
if self.__flat:
# Use default widget background/border styling.
StyledWidget_paintEvent(self, event)
with StyledIconEngine.setOverridePalette(styles.breeze_light()):
if self.__flat:
# Use default widget background/border styling.
StyledWidget_paintEvent(self, event)

p = QStylePainter(self)
p.drawControl(QStyle.CE_ToolButtonLabel, opt)
else:
p = QStylePainter(self)
p.drawComplexControl(QStyle.CC_ToolButton, opt)
p = QStylePainter(self)
p.drawControl(QStyle.CE_ToolButtonLabel, opt)
else:
p = QStylePainter(self)
p.drawComplexControl(QStyle.CC_ToolButton, opt)

# if checked, no shadow
if self.isChecked():
Expand Down
178 changes: 178 additions & 0 deletions orangecanvas/gui/iconengine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
from itertools import count
from contextlib import contextmanager
from typing import Optional

from AnyQt.QtCore import Qt, QObject, QSize, QRect, QT_VERSION_INFO
from AnyQt.QtGui import (
QIconEngine, QPalette, QIcon, QPixmap, QPixmapCache, QImage, QPainter
)
from AnyQt.QtWidgets import QApplication, QStyleOption

from orangecanvas.gui.utils import luminance
from orangecanvas.utils.image import grayscale_invert

__all__ = [
"StyledIconEngine",
"SymbolIconEngine",
]


_cache_id_gen = count()


class StyledIconEngine(QIconEngine):
"""
An abstract base class for icon engines that adapt to effective palette.
"""
__slots__ = ("__palette", "__styleObject")

def __init__(self, *args, palette: Optional[QPalette] = None,
styleObject: Optional[QObject] = None, **kwargs):
self.__palette = QPalette(palette) if palette is not None else None
self.__styleObject = styleObject
super().__init__(*args, **kwargs)

@staticmethod
def paletteFromStyleObject(obj: QObject) -> Optional[QPalette]:
palette = obj.property("palette")
if isinstance(palette, QPalette):
return palette
else:
return None

__paletteOverride = None

@staticmethod
@contextmanager
def setOverridePalette(palette: QPalette):
"""
Temporarily override used QApplication.palette() with this class.
This can be used when the icon is drawn on a non default background
and as such might not contrast with it when using the default palette,
and neither paint device nor styleObject can be used for this.
"""
old = StyledIconEngine.__paletteOverride
try:
StyledIconEngine.__paletteOverride = palette
yield
finally:
StyledIconEngine.__paletteOverride = old

@staticmethod
def paletteOverride() -> Optional[QPalette]:
return StyledIconEngine.__paletteOverride

def effectivePalette(self) -> QPalette:
if StyledIconEngine.__paletteOverride is not None:
return StyledIconEngine.__paletteOverride
if self.__palette is not None:
return self.__palette
elif self.__styleObject is not None:
palette = self.paletteFromStyleObject(self.__styleObject)
if palette is not None:
return palette
return QApplication.palette()


# shorthands for eliminating runtime attr load in hot path
_QIcon_Active_Modes = (QIcon.Active, QIcon.Selected)
_QIcon_Disabled = QIcon.Disabled

_QPalette_Active = QPalette.Active
_QPalette_WindowText = QPalette.WindowText
_QPalette_Disabled = QPalette.Disabled
_QPalette_HighlightedText = QPalette.HighlightedText


class SymbolIconEngine(StyledIconEngine):
"""
A *Symbolic* icon engine adapter for turning simple grayscale base icon
to current effective appearance.
Arguments
---------
base: QIcon
The base icon.
"""
def __init__(self, base: QIcon):
super().__init__()
self.__base = QIcon(base)
self.__cache_key = next(_cache_id_gen)

def paint(
self, painter: QPainter, rect: QRect, mode: QIcon.Mode,
state: QIcon.State
) -> None:
if not self.__base.isNull():
palette = self.effectivePalette()
size = rect.size()
dpr = painter.device().devicePixelRatioF()
size = size * dpr
pm = self.__renderStyledPixmap(size, mode, state, palette)
painter.drawPixmap(rect, pm)

def pixmap(self, size: QSize, mode: QIcon.Mode, state: QIcon.State) -> QPixmap:
return self.__renderStyledPixmap(size, mode, state, self.effectivePalette())

def __renderStyledPixmap(
self, size: QSize, mode: QIcon.Mode, state: QIcon.State,
palette: QPalette
) -> QPixmap:
active = mode in _QIcon_Active_Modes
disabled = mode == _QIcon_Disabled
cg = _QPalette_Disabled if disabled else _QPalette_Active
role = _QPalette_WindowText if active else _QPalette_HighlightedText
namespace = f"{__name__}:SymbolIconEngine/{self.__cache_key}"
cachekey = f"{size.width()}x{size.height()}"
style_key = f"{hex(palette.cacheKey())}-{cg}-{role}"
pmcachekey = f"{namespace}/{cachekey}/{style_key}"
pm = QPixmapCache.find(pmcachekey)
if pm is None or pm.isNull():
color = palette.color(QPalette.Text)
src = qicon_pixmap(self.__base, size, 1.0, mode, state)
src = src.toImage().convertToFormat(QImage.Format_ARGB32_Premultiplied)
if luminance(color) > 0.5:
dest = grayscale_invert(
src,
palette.color(QPalette.Text),
palette.color(QPalette.Base),
)
else:
dest = src
pm = QPixmap.fromImage(dest)
QPixmapCache.insert(pmcachekey, pm)

self.__style = style = QApplication.style()
if style is not None:
opt = QStyleOption()
opt.palette = palette
pm = style.generatedIconPixmap(mode, pm, opt)
return pm

def clone(self) -> 'QIconEngine':
return SymbolIconEngine(self.__base)


def qicon_pixmap(
base: QIcon, size: QSize, scale: float, mode: QIcon.Mode,
state: QIcon.State
) -> QPixmap:
"""
Like QIcon.pixmap(size: QSize, scale: float, ...) overload in Qt6.
On Qt 6 this directly calls the corresponding overload.
On Qt 5 this is emulated by painting on a suitable constructed pixmap.
"""
size = base.actualSize(size * scale, mode, state)
pm = QPixmap(size)
pm.setDevicePixelRatio(scale)
pm.fill(Qt.transparent)
p = QPainter(pm)
base.paint(p, 0, 0, size.width(), size.height(), Qt.AlignCenter, mode, state)
p.end()
return pm


if QT_VERSION_INFO >= (6, 0):
qicon_pixmap = QIcon.pixmap
Loading

0 comments on commit 1c90038

Please sign in to comment.