diff --git a/orangecanvas/application/widgettoolbox.py b/orangecanvas/application/widgettoolbox.py index 5f328fa88..ef266b4fa 100644 --- a/orangecanvas/application/widgettoolbox.py +++ b/orangecanvas/application/widgettoolbox.py @@ -25,7 +25,6 @@ from ..gui.toolbox import ToolBox from ..gui.toolgrid import ToolGrid from ..gui.quickhelp import StatusTipPromoter -from ..gui.utils import create_gradient from ..registry.qt import QtWidgetRegistry @@ -415,26 +414,7 @@ def __insertItem(self, item, index): self.insertItem(index, grid, text, icon, tooltip) button = self.tabButton(index) - # Set the 'highlight' color if applicable - highlight_foreground = None - highlight = item_background(item) - if highlight is None \ - and item.data(QtWidgetRegistry.BACKGROUND_ROLE) is not None: - highlight = item.data(QtWidgetRegistry.BACKGROUND_ROLE) - - 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_foreground = Qt.black if value > 128 else Qt.white - - palette = button.palette() - - if highlight is not None: - palette.setBrush(QPalette.Highlight, highlight) - if highlight_foreground is not None: - palette.setBrush(QPalette.HighlightedText, highlight_foreground) + palette = item.data(QtWidgetRegistry.BACKGROUND_ROLE) button.setPalette(palette) def __on_dataChanged(self, topLeft, bottomRight): diff --git a/orangecanvas/canvas/items/__init__.py b/orangecanvas/canvas/items/__init__.py index ef5a980d4..40d950085 100644 --- a/orangecanvas/canvas/items/__init__.py +++ b/orangecanvas/canvas/items/__init__.py @@ -3,7 +3,7 @@ """ -from .nodeitem import NodeItem, NodeAnchorItem, NodeBodyItem, SHADOW_COLOR +from .nodeitem import NodeItem, NodeAnchorItem, NodeBodyItem, DEFAULT_SHADOW_COLOR from .nodeitem import SourceAnchorItem, SinkAnchorItem, AnchorPoint from .linkitem import LinkItem, LinkCurveItem from .annotationitem import TextAnnotation, ArrowAnnotation diff --git a/orangecanvas/canvas/items/graphicstextitem.py b/orangecanvas/canvas/items/graphicstextitem.py index 253f4670e..59d431d4c 100644 --- a/orangecanvas/canvas/items/graphicstextitem.py +++ b/orangecanvas/canvas/items/graphicstextitem.py @@ -16,6 +16,7 @@ QGraphicsItem, QGraphicsSceneContextMenuEvent, QMenu, QAction, ) +from orangecanvas.gui.utils import foreground_for_background from orangecanvas.utils import set_flag @@ -63,11 +64,7 @@ def paint(self, painter, option, widget=None): if not window.isActiveWindow(): cg = QPalette.Inactive - color = palette.color( - cg, - QPalette.Highlight if state & QStyle.State_Selected - else QPalette.Light - ) + color = palette.color(cg, QPalette.Light) painter.save() painter.setPen(QPen(Qt.NoPen)) @@ -113,10 +110,13 @@ def __updateDefaultTextColor(self): # type: () -> None if self.__styleState & QStyle.State_Selected \ and not self.__styleState & QStyle.State_Editing: - role = QPalette.HighlightedText + bgRole = QPalette.Light + bgColor = self.palette().color(bgRole) + color = foreground_for_background(bgColor) else: role = QPalette.WindowText - self.setDefaultTextColor(self.palette().color(role)) + color = self.palette().color(role) + self.setDefaultTextColor(color) def setHtml(self, contents): # type: (str) -> None diff --git a/orangecanvas/canvas/items/linkitem.py b/orangecanvas/canvas/items/linkitem.py index c89494c3f..6ec22cf2f 100644 --- a/orangecanvas/canvas/items/linkitem.py +++ b/orangecanvas/canvas/items/linkitem.py @@ -13,14 +13,14 @@ from AnyQt.QtWidgets import ( QGraphicsItem, QGraphicsPathItem, QGraphicsWidget, QGraphicsDropShadowEffect, QGraphicsSceneHoverEvent, QStyle, - QGraphicsSceneMouseEvent -) + QGraphicsSceneMouseEvent, + QApplication) from AnyQt.QtGui import ( QPen, QBrush, QColor, QPainterPath, QTransform, QPalette, QFont, ) from AnyQt.QtCore import Qt, QPointF, QRectF, QLineF, QEvent, QPropertyAnimation, Signal, QTimer -from .nodeitem import AnchorPoint, SHADOW_COLOR +from .nodeitem import AnchorPoint, DEFAULT_SHADOW_COLOR from .graphicstextitem import GraphicsTextItem from .utils import stroke_path, qpainterpath_sub_path from ...registry import InputSignal, OutputSignal @@ -52,7 +52,7 @@ def __init__(self, parent): self.setPen(QPen(QBrush(QColor("#9CACB4")), 2.0)) self.shadow = QGraphicsDropShadowEffect( - blurRadius=5, color=QColor(SHADOW_COLOR), + blurRadius=5, color=QColor(DEFAULT_SHADOW_COLOR), offset=QPointF(0, 0) ) self.setGraphicsEffect(self.shadow) @@ -718,27 +718,46 @@ def __updatePen(self): # type: () -> None self.prepareGeometryChange() self.__boundingRect = None - if self.__dynamic: - if self.__dynamicEnabled: - color = QColor(0, 150, 0, 150) - else: - color = QColor(150, 0, 0, 150) - normal = QPen(QBrush(color), 2.0) - hover = QPen(QBrush(color.darker(120)), 2.0) - else: - normal = QPen(QBrush(QColor("#9CACB4")), 2.0) - hover = QPen(QBrush(QColor("#959595")), 2.0) + app = QApplication.instance() + darkMode = app.property('darkMode') + if self.__dynamic: + # TODO + pass + # if self.__dynamicEnabled: + # color = QColor(0, 150, 0, 150) + # else: + # color = QColor(150, 0, 0, 150) + # + # normal = QPen(QBrush(color), 2.0) + # hover = QPen(QBrush(color.darker(120)), 2.0) if self.__state & LinkItem.Empty: pen_style = Qt.DashLine else: pen_style = Qt.SolidLine + if darkMode: + brush = QBrush(QColor('#EEEEEE')) + hover = QPen( + QBrush(QColor('#FFFFFF')), + 3.0 if pen_style == Qt.SolidLine else 2.0 + ) + else: + brush = QBrush(QColor('#878787')) + hover = QPen( + brush.color().darker(105), + 3.0 if pen_style == Qt.SolidLine else 2.0 + ) + normal = QPen( + brush, + 3.0 if pen_style == Qt.SolidLine and self.__state else 2.0 + ) + normal.setStyle(pen_style) hover.setStyle(pen_style) - if self.hover or self.isSelected(): + if self.hover or self.isSelected() or self.__isSelectedImplicit(): pen = hover else: pen = normal diff --git a/orangecanvas/canvas/items/nodeitem.py b/orangecanvas/canvas/items/nodeitem.py index 0b22fb6d6..61090fdee 100644 --- a/orangecanvas/canvas/items/nodeitem.py +++ b/orangecanvas/canvas/items/nodeitem.py @@ -35,13 +35,13 @@ from .graphicspathobject import GraphicsPathObject from .graphicstextitem import GraphicsTextItem, GraphicsTextEdit from .utils import ( - saturated, radial_gradient, linspace, qpainterpath_sub_path, clip + radial_gradient, linspace, qpainterpath_sub_path, clip ) from ...gui.utils import disconnected - +from ...gui.palette_utils import create_palette, default_palette from ...scheme.node import UserMessage -from ...registry import NAMED_COLORS, WidgetDescription, CategoryDescription, \ - InputSignal, OutputSignal +from ...registry import WidgetDescription, CategoryDescription, InputSignal, \ + OutputSignal from ...resources import icon_loader from .utils import uniform_linear_layout_trunc from ...utils import set_flag @@ -50,39 +50,6 @@ from ...registry import WidgetDescription -def create_palette(light_color, color): - # type: (QColor, QColor) -> QPalette - """ - Return a new :class:`QPalette` from for the :class:`NodeBodyItem`. - """ - palette = QPalette() - - palette.setColor(QPalette.Inactive, QPalette.Light, - saturated(light_color, 50)) - palette.setColor(QPalette.Inactive, QPalette.Midlight, - saturated(light_color, 90)) - palette.setColor(QPalette.Inactive, QPalette.Button, - light_color) - - palette.setColor(QPalette.Active, QPalette.Light, - saturated(color, 50)) - palette.setColor(QPalette.Active, QPalette.Midlight, - saturated(color, 90)) - palette.setColor(QPalette.Active, QPalette.Button, - color) - palette.setColor(QPalette.ButtonText, QColor("#515151")) - return palette - - -def default_palette(): - # type: () -> QPalette - """ - Create and return a default palette for a node. - """ - return create_palette(QColor(NAMED_COLORS["light-yellow"]), - QColor(NAMED_COLORS["yellow"])) - - def animation_restart(animation): # type: (QPropertyAnimation) -> None if animation.state() == QPropertyAnimation.Running: @@ -90,8 +57,7 @@ def animation_restart(animation): animation.start() -SHADOW_COLOR = "#9CACB4" -SELECTED_SHADOW_COLOR = "#609ED7" +DEFAULT_SHADOW_COLOR = "#9CACB4" class NodeBodyItem(GraphicsPathObject): @@ -118,11 +84,9 @@ def __init__(self, parent=None): self.setPen(QPen(Qt.NoPen)) - self.setPalette(default_palette()) - self.shadow = QGraphicsDropShadowEffect( blurRadius=0, - color=QColor(SHADOW_COLOR), + color=QColor(DEFAULT_SHADOW_COLOR), offset=QPointF(0, 0), ) self.shadow.setEnabled(False) @@ -133,7 +97,7 @@ def __init__(self, parent=None): # non devicePixelRatio aware. shadowitem = GraphicsPathObject(self, objectName="shadow-shape-item") shadowitem.setPen(Qt.NoPen) - shadowitem.setBrush(QBrush(QColor(SHADOW_COLOR).lighter())) + shadowitem.setBrush(QBrush(QColor(DEFAULT_SHADOW_COLOR).lighter())) shadowitem.setGraphicsEffect(self.shadow) shadowitem.setFlag(QGraphicsItem.ItemStacksBehindParent) self.__shadow = shadowitem @@ -156,6 +120,16 @@ def __init__(self, parent=None): timeout=self.__progressTimeout ) + self.__bgAnimation = QPropertyAnimation(self, b"bgAnimPhase", + duration=250, loopCount=1) + self.__bgAnimation.setEasingCurve(QEasingCurve.InOutCubic) + self.__bgAnimPhase = 1 + self.__bgStart = (0.5, 0.5, 0.5) + self.__bgEnd = (0.5, 0.5, 0.5) + self.__grad = None + + self.setPalette(default_palette()) + # TODO: The body item should allow the setting of arbitrary painter # paths (for instance rounded rect, ...) def setShapeRect(self, rect): @@ -176,7 +150,7 @@ def setPalette(self, palette): Set the body color palette (:class:`QPalette`). """ self.palette = QPalette(palette) - self.__updateBrush() + self.__updateBrush(animate=False) def setAnimationEnabled(self, enabled): # type: (bool) -> None @@ -220,6 +194,7 @@ def ping(self): Trigger a 'ping' animation. """ animation_restart(self.__pingAnimation) + self.__updateBrush() def startSpinner(self): self.__spinnerAnimation.start() @@ -287,11 +262,7 @@ def __updateShadowState(self): if enabled and not self.shadow.isEnabled(): self.shadow.setEnabled(enabled) - if self.__isSelected: - color = QColor(SELECTED_SHADOW_COLOR) - else: - color = QColor(SHADOW_COLOR) - + color = self.palette.color(QPalette.Shadow) self.shadow.setColor(color) if self.__animationEnabled: @@ -304,8 +275,8 @@ def __updateShadowState(self): else: self.shadow.setBlurRadius(radius) - def __updateBrush(self): - # type: () -> None + def __updateBrush(self, animate=True): + # type: (bool) -> None palette = self.palette if self.__isSelected: cg = QPalette.Active @@ -314,9 +285,54 @@ def __updateBrush(self): palette.setCurrentColorGroup(cg) c1 = palette.color(QPalette.Light) - c2 = palette.color(QPalette.Button) - grad = radial_gradient(c2, c1) - self.setBrush(QBrush(grad)) + c2 = palette.color(QPalette.Midlight) + c3 = palette.color(QPalette.Button) + grad = radial_gradient(c3, c2, c1) + + # randomize position of gradient centre + def get_rand_factor(shift=0, scaling=5): + from random import random + return (random() - 0.5) / scaling + shift + + w = 0.5 + get_rand_factor() + h = 0.5 + get_rand_factor() + r = 0.5 + get_rand_factor() + + if not animate or not self.__animationEnabled: + grad.setFocalPoint(w, h) + grad.setCenter(w, h) + grad.setCenterRadius(r) + self.setBrush(QBrush(grad)) + return + + if self.__bgAnimation.state() == QPropertyAnimation.Running: + return + self.__grad = grad + self.__bgStart = self.__bgEnd + self.__bgEnd = (w, h, r) + self.__bgAnimation.setStartValue(0) + self.__bgAnimation.setEndValue(1) + self.__bgAnimation.start() + + @Property('float') + def bgAnimPhase(self): + if not hasattr(self, '_NodeBodyItem__bgAnimPhase'): + return 1 + return self.__bgAnimPhase + + @bgAnimPhase.setter + def bgAnimPhase(self, p): + sw, sh, sr = self.__bgStart + ew, eh, er = self.__bgEnd + + dw = sw + (ew - sw) * p + dh = sh + (eh - sh) * p + dr = sr + (er - sr) * p + self.__grad.setFocalPoint(dw, dh) + self.__grad.setCenter(dw, dh) + self.__grad.setCenterRadius(dr) + self.setBrush(QBrush(self.__grad)) + self.__bgAnimPhase = p # TODO: The selected state should be set using the # QStyle flags (State_Selected. State_HasFocus) @@ -332,7 +348,8 @@ def setSelected(self, selected): """ self.__isSelected = selected self.__updateShadowState() - self.__updateBrush() + if selected: + self.__updateBrush() def __on_finished(self): # type: () -> None @@ -354,8 +371,10 @@ def __init__(self, parent=None): self.setAcceptedMouseButtons(Qt.NoButton) self.setRect(-3.5, -3.5, 7., 7.) self.setPen(QPen(Qt.NoPen)) - self.setBrush(QBrush(QColor("#9CACB4"))) - self.hoverBrush = QBrush(QColor("#959595")) + + color = QColor("#9CACB4") + self.brush = QBrush(color) + self.hover_brush = QBrush(color.darker(115)) self.__hover = False @@ -380,7 +399,7 @@ def setLinkState(self, state: 'LinkItem.State'): def paint(self, painter, option, widget=None): # type: (QPainter, QStyleOptionGraphicsItem, Optional[QWidget]) -> None hover = self.__styleState & (QStyle.State_Selected | QStyle.State_MouseOver) - brush = self.hoverBrush if hover else self.brush() + brush = self.hover_brush if hover else self.brush if self.__linkState & (LinkItem.Pending | LinkItem.Invalidated): brush = QBrush(Qt.red) @@ -568,7 +587,7 @@ def __init__(self, parent, **kwargs): self.shadow = QGraphicsDropShadowEffect( blurRadius=0, - color=QColor(SHADOW_COLOR), + # color=QColor(DEFAULT_SHADOW_COLOR), offset=QPointF(0, 0), ) # self.setGraphicsEffect(self.shadow) @@ -576,7 +595,7 @@ def __init__(self, parent, **kwargs): shadowitem = GraphicsPathObject(self, objectName="shadow-shape-item") shadowitem.setPen(Qt.NoPen) - shadowitem.setBrush(QBrush(QColor(SHADOW_COLOR))) + # shadowitem.setBrush(QBrush(QColor(DEFAULT_SHADOW_COLOR))) shadowitem.setGraphicsEffect(self.shadow) shadowitem.setFlag(QGraphicsItem.ItemStacksBehindParent) self.__shadow = shadowitem @@ -1234,6 +1253,9 @@ def __init__(self, widget_description=None, parent=None, **kwargs): self.outputAnchorItem.setAnchorPath(output_path) self.outputAnchorItem.setAnimationEnabled(self.__animationEnabled) + self.widget_palette = None + self.__updateAnchorBrushes() + self.inputAnchorItem.hide() self.outputAnchorItem.hide() @@ -1321,11 +1343,37 @@ def setWidgetCategory(self, desc): Set the widget category. """ self.category_description = desc - if desc and desc.background: - background = NAMED_COLORS.get(desc.background, desc.background) - color = QColor(background) - if color.isValid(): - self.setColor(color) + + background = desc.background + if desc.background: + palette = create_palette(background) + else: + palette = default_palette() + self.widget_palette = palette + self.setPalette(palette) + + def __updateAnchorBrushes(self): + app = QApplication.instance() + darkMode = app.property('darkMode') + + deactivated_brush = QBrush(QColor('#878787')) + if darkMode: + connected_brush = QBrush(QColor('#EEEEEE')) + connected_hover_brush = QBrush(QColor('#FFFFFF')) + else: + connected_brush = QBrush(QColor('#878787')) + connected_hover_brush = QBrush(connected_brush.color().darker(105)) + hover_brush = connected_brush + + for anchor in (self.outputAnchorItem, self.inputAnchorItem): + anchor.connectedBrush = connected_brush + anchor.normalBrush = deactivated_brush + anchor.connectedHoverBrush = connected_hover_brush + anchor.normalHoverBrush = hover_brush + for point in anchor.anchorPoints(): + point.indicator.brush = connected_brush + point.indicator.hover_brush = hover_brush + anchor.update() def setIcon(self, icon): # type: (QIcon) -> None @@ -1337,18 +1385,8 @@ def setIcon(self, icon): ) self.icon_item.setPos(-18, -18) - def setColor(self, color, selectedColor=None): - # type: (QColor, Optional[QColor]) -> None - """ - Set the widget color. - """ - if selectedColor is None: - selectedColor = saturated(color, 150) - palette = create_palette(color, selectedColor) - self.shapeItem.setPalette(palette) - def setTitle(self, title): - # type: (str) -> None + # type: (str) -> Nne """ Set the node title. The title text is displayed at the bottom of the node. @@ -1546,8 +1584,11 @@ def newInputAnchor(self, signal=None): if not (self.widget_description and self.widget_description.inputs): raise ValueError("Widget has no inputs.") + palette = self.palette() + anchor = AnchorPoint(self, signal=signal) self.inputAnchorItem.addAnchor(anchor) + self.__updateAnchorBrushes() return anchor @@ -1566,8 +1607,11 @@ def newOutputAnchor(self, signal=None): if not (self.widget_description and self.widget_description.outputs): raise ValueError("Widget has no outputs.") + palette = self.palette() + anchor = AnchorPoint(self, signal=signal) self.outputAnchorItem.addAnchor(anchor) + self.__updateAnchorBrushes() return anchor @@ -1760,8 +1804,14 @@ def itemChange(self, change, value): def __updatePalette(self): # type: () -> None - palette = self.palette() + if self.widget_palette: + palette = self.widget_palette + else: + palette = self.palette() + + self.shapeItem.setPalette(palette) self.captionTextItem.setPalette(palette) + self.__updateAnchorBrushes() def __updateFont(self): # type: () -> None diff --git a/orangecanvas/canvas/items/utils.py b/orangecanvas/canvas/items/utils.py index 3b4753906..3a23606cd 100644 --- a/orangecanvas/canvas/items/utils.py +++ b/orangecanvas/canvas/items/utils.py @@ -11,7 +11,7 @@ QColor, QRadialGradient, QPainterPathStroker, QPainterPath, QPen ) -from AnyQt.QtWidgets import QGraphicsItem +from orangecanvas.gui.utils import saturated if typing.TYPE_CHECKING: T = typing.TypeVar("T") @@ -112,7 +112,7 @@ def saturated(color, factor=150): return QColor.fromHsvF(h, s, v, a).convertTo(color.spec()) -def radial_gradient(color, color_light=50): +def radial_gradient(color, color_light=50, color_lightest=50): # type: (QColor, Union[int, QColor]) -> QRadialGradient """ radial_gradient(QColor, QColor) @@ -125,9 +125,11 @@ def radial_gradient(color, color_light=50): """ if not isinstance(color_light, QColor): color_light = saturated(color, color_light) + if not isinstance(color_lightest, QColor): + color_lightest = saturated(color, color_lightest) gradient = QRadialGradient(0.5, 0.5, 0.5) - gradient.setColorAt(0.0, color_light) - gradient.setColorAt(0.5, color_light) + gradient.setColorAt(0.0, color_lightest) + gradient.setColorAt(0.8, color_light) gradient.setColorAt(1.0, color) gradient.setCoordinateMode(QRadialGradient.ObjectBoundingMode) return gradient diff --git a/orangecanvas/document/quickmenu.py b/orangecanvas/document/quickmenu.py index 2d5d1a85f..a13de53cf 100644 --- a/orangecanvas/document/quickmenu.py +++ b/orangecanvas/document/quickmenu.py @@ -62,14 +62,12 @@ def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIn """ Draw icon background """ # get category color - brush = as_qbrush(index.data(QtWidgetRegistry.BACKGROUND_ROLE)) - if brush is not None: - color = brush.color() - else: - color = QColor("FFA840") # orange! + palette: QPalette = index.data(QtWidgetRegistry.BACKGROUND_ROLE) + light_color = palette.color(QPalette.Light) + dark_color = palette.color(QPalette.Midlight) # (get) cache(d) pixmap - bg = innerGlowBackgroundPixmap(color, + bg = innerGlowBackgroundPixmap(light_color, dark_color, QSize(rect.height(), rect.height())) # draw background @@ -1228,7 +1226,10 @@ def update_from_settings(self): def as_qbrush(value): # type: (Any) -> Optional[QBrush] - if isinstance(value, QBrush): + app = QApplication.instance() + if isinstance(value, QPalette): + return value.button() + elif isinstance(value, QBrush): return value else: return None @@ -1464,18 +1465,20 @@ def __on_dataChanged(self, topLeft, bottomRight): # Note: the tab buttons are offest by 1 (to accommodate # the Suggest Page). button = self.__pages.tabButton(row + 1) - brush = as_qbrush(index.data(QtWidgetRegistry.BACKGROUND_ROLE)) - if brush is not None: - base_color = brush.color() - shadow_color = base_color.fromHsv(base_color.hsvHue(), - base_color.hsvSaturation(), - 100) - button.setStyleSheet( - TAB_BUTTON_STYLE_TEMPLATE.format - (base_color.darker(110).name(), - base_color.name(), - shadow_color.name()) - ) + + palette = index.data(QtWidgetRegistry.BACKGROUND_ROLE) + base_color = palette.color(QPalette.Midlight) + off_color = palette.color(QPalette.Button) + shadow_color = base_color.fromHsv(base_color.hsvHue(), + base_color.hsvSaturation(), + 100) + + button.setStyleSheet( + TAB_BUTTON_STYLE_TEMPLATE.format + (off_color.name(), + base_color.name(), + shadow_color.name()) + ) def __on_rowsInserted(self, parent, start, end): # type: (QModelIndex, int, int) -> None @@ -1500,19 +1503,20 @@ def __insertPage(self, row, index): i = self.insertPage(row, page.title(), page) - brush = as_qbrush(index.data(QtWidgetRegistry.BACKGROUND_ROLE)) - if brush is not None: - base_color = brush.color() - shadow_color = base_color.fromHsv(base_color.hsvHue(), - base_color.hsvSaturation(), - 100) - button = self.__pages.tabButton(i) - button.setStyleSheet( - TAB_BUTTON_STYLE_TEMPLATE.format - (base_color.darker(110).name(), - base_color.name(), - shadow_color.name()) - ) + palette = index.data(QtWidgetRegistry.BACKGROUND_ROLE) + base_color = palette.color(QPalette.Midlight) + off_color = palette.color(QPalette.Button) + shadow_color = base_color.fromHsv(base_color.hsvHue(), + base_color.hsvSaturation(), + 100) + + button = self.__pages.tabButton(i) + button.setStyleSheet( + TAB_BUTTON_STYLE_TEMPLATE.format + (off_color.name(), + base_color.name(), + shadow_color.name()) + ) def __removePage(self, row): # type: (int) -> None diff --git a/orangecanvas/gui/palette_utils.py b/orangecanvas/gui/palette_utils.py new file mode 100644 index 000000000..6b86352a8 --- /dev/null +++ b/orangecanvas/gui/palette_utils.py @@ -0,0 +1,78 @@ +from AnyQt.QtWidgets import ( + QApplication +) +from AnyQt.QtGui import ( + QColor, QPalette +) + +from orangecanvas.registry import NAMED_COLORS, DEFAULT_COLOR +from orangecanvas.gui.utils import saturated, foreground_for_background + + +def create_palette(background): + # type: (Any) -> QPalette + """ + Return a new :class:`QPalette` from for the :class:`NodeBodyItem`. + """ + app = QApplication.instance() + darkMode = app.property('darkMode') + + defaults = { + # Canvas widget background radial gradient + QPalette.Light: + lambda color: saturated(color, 50), + QPalette.Midlight: + lambda color: saturated(color, 90), + QPalette.Button: + lambda color: color, + # Canvas widget shadow + QPalette.Shadow: + lambda color: saturated(color, 125) if darkMode else + saturated(color, 150), + + # Category tab color + QPalette.Highlight: + lambda color: color, + + QPalette.HighlightedText: + lambda color: foreground_for_background(color), + } + + palette = QPalette() + + if isinstance(background, dict): + if app.property('darkMode'): + background = background.get('dark', next(iter(background.values()))) + else: + background = background.get('light', next(iter(background.values()))) + base_color = background[QPalette.Button] + base_color = QColor(base_color) + else: + color = NAMED_COLORS.get(background, background) + color = QColor(background) + if color.isValid(): + base_color = color + else: + color = NAMED_COLORS[DEFAULT_COLOR] + base_color = QColor(color) + + for role, default in defaults.items(): + if isinstance(background, dict) and role in background: + v = background[role] + color = NAMED_COLORS.get(v, v) + color = QColor(color) + if color.isValid(): + palette.setColor(role, color) + continue + color = default(base_color) + palette.setColor(role, color) + + return palette + + +def default_palette(): + # type: () -> QPalette + """ + Create and return a default palette for a node. + """ + return create_palette(DEFAULT_COLOR) diff --git a/orangecanvas/gui/tests/test_utils.py b/orangecanvas/gui/tests/test_utils.py new file mode 100644 index 000000000..3fa5aaa34 --- /dev/null +++ b/orangecanvas/gui/tests/test_utils.py @@ -0,0 +1,19 @@ +from AnyQt.QtCore import Qt +from AnyQt.QtGui import QColor + +from orangecanvas.gui.test import QAppTestCase +from orangecanvas.gui.utils import foreground_for_background + + +class TestUtils(QAppTestCase): + def test_fg_for_bg(self): + w = QColor(Qt.white) + self.assertEqual( + foreground_for_background(w).name(), + '#000000' + ) + b = QColor(Qt.black) + self.assertEqual( + foreground_for_background(b).name(), + '#ffffff' + ) diff --git a/orangecanvas/gui/toolbox.py b/orangecanvas/gui/toolbox.py index c9517080f..eaed126c9 100644 --- a/orangecanvas/gui/toolbox.py +++ b/orangecanvas/gui/toolbox.py @@ -126,18 +126,15 @@ def __paintEventNoStyle(self): # highlight brush is used as the background for the icon and background # when the tab is expanded and as mouse hover color (lighter). - brush_highlight = palette.highlight() - foregroundrole = QPalette.ButtonText + brush_highlight = palette.midlight() + foregroundrole = QPalette.HighlightedText if opt.state & QStyle.State_Sunken: # State 'down' pressed during a mouse press (slightly darker). - background_brush = brush_darker(brush_highlight, 110) - foregroundrole = QPalette.HighlightedText + background_brush = palette.highlight() elif opt.state & QStyle.State_MouseOver: - background_brush = brush_darker(brush_highlight, 95) - foregroundrole = QPalette.HighlightedText - elif opt.state & QStyle.State_On: background_brush = brush_highlight - foregroundrole = QPalette.HighlightedText + elif opt.state & QStyle.State_On: + background_brush = palette.midlight() else: # The default button brush. background_brush = palette.button() diff --git a/orangecanvas/gui/utils.py b/orangecanvas/gui/utils.py index 1f9ac143f..b1b2c36f8 100644 --- a/orangecanvas/gui/utils.py +++ b/orangecanvas/gui/utils.py @@ -15,12 +15,13 @@ from typing import Optional, Union from AnyQt.QtWidgets import ( - QWidget, QMessageBox, QStyleOption, QStyle, QTextEdit, QScrollBar + QWidget, QMessageBox, QStyleOption, QStyle, QTextEdit, QScrollBar, + QApplication ) from AnyQt.QtGui import ( QGradient, QLinearGradient, QRadialGradient, QBrush, QPainter, QPaintEvent, QColor, QPixmap, QPixmapCache, QTextOption, QGuiApplication, - QTextCharFormat, QFont + QTextCharFormat, QFont, QPalette ) from AnyQt.QtCore import Qt, QPointF, QPoint, QRect, QRectF, Signal, QEvent from AnyQt import sip @@ -293,6 +294,64 @@ def brush_darker(brush: QBrush, factor: bool) -> QBrush: return brush +def relative_luminance(color: QColor): + vR = color.redF() + vG = color.greenF() + vB = color.blueF() + + def sRGBtoLin(c): + if c <= 0.04045: + return c / 12.92 + else: + return pow(((c + 0.055) / 1.055), 2.4) + + Y = (0.2126 * sRGBtoLin(vR) + + 0.7152 * sRGBtoLin(vG) + + 0.0722 * sRGBtoLin(vB)) + + return Y + + +def lstar(color): + Y = relative_luminance(color) + if Y <= 216 / 24389: + Lstar = Y * (24389 / 27) + else: + Lstar = pow(Y, (1 / 3)) * 116 - 16 + + return Lstar + + +def contrast_ratio(c1, c2): + v1 = relative_luminance(c1) + v2 = relative_luminance(c2) + if v1 < v2: + _ = v2 + v2 = v1 + v1 = _ + return (v1 + 0.05) / (v2 + 0.05) + + +def foreground_for_background(bg: QColor): + if lstar(bg) < 50: + return QColor(Qt.white) + else: + return QColor(Qt.black) + + +def saturated(color, factor=150): + # type: (QColor, int) -> QColor + """Return a saturated color. + """ + h = color.hsvHueF() + s = color.hsvSaturationF() + v = color.valueF() + a = color.alphaF() + s = factor * s / 100.0 + s = max(min(1.0, s), 0.0) + return QColor.fromHsvF(h, s, v, a).convertTo(color.spec()) + + def create_gradient(base_color: QColor, stop=QPointF(0, 0), finalStop=QPointF(0, 1)) -> QLinearGradient: """ @@ -447,28 +506,25 @@ def message(icon, text, title=None, informative_text=None, details=None, return mbox.exec() -def innerGlowBackgroundPixmap(color, size, radius=5): +def innerGlowBackgroundPixmap(light_color, dark_color, size, radius=10): """ Draws radial gradient pixmap, then uses that to draw a rounded-corner gradient rectangle pixmap. Args: - color (QColor): used as outer color (lightness 245 used for inner) + light_color (QColor): used as inner color + dark_color (QColor): used as outer color size (QSize): size of output pixmap radius (int): radius of inner glow rounded corners """ key = "InnerGlowBackground " + \ - color.name() + " " + \ + light_color.name() + " " + \ + dark_color.name() + " " + \ str(radius) bg = QPixmapCache.find(key) if bg: return bg - # set background colors for gradient - color = color.toHsl() - light_color = color.fromHsl(color.hslHue(), color.hslSaturation(), 245) - dark_color = color - # initialize radial gradient center = QPoint(radius, radius) pixRect = QRect(0, 0, radius * 2, radius * 2) diff --git a/orangecanvas/registry/__init__.py b/orangecanvas/registry/__init__.py index 48237afbf..5db6db1d5 100644 --- a/orangecanvas/registry/__init__.py +++ b/orangecanvas/registry/__init__.py @@ -41,7 +41,7 @@ # default color when the category does not provide it -DEFAULT_COLOR = "light-yellow" +DEFAULT_COLOR = "blue" from .description import ( diff --git a/orangecanvas/registry/description.py b/orangecanvas/registry/description.py index d76150fb8..f3099ddad 100644 --- a/orangecanvas/registry/description.py +++ b/orangecanvas/registry/description.py @@ -361,7 +361,7 @@ def __init__(self, name, id, category=None, version=None, maintainer=None, maintainer_email=None, help=None, help_ref=None, url=None, keywords=None, priority=sys.maxsize, - icon=None, background=None, + icon=None, background=None, dark_background=None, replaces=None, short_name=None, ): if inputs is None: @@ -415,6 +415,7 @@ def __init__(self, name, id, category=None, version=None, self.priority = priority self.icon = icon self.background = background + self.dark_background = dark_background self.replaces = list(replaces) def __str__(self): @@ -477,7 +478,8 @@ def __init__(self, name=None, version=None, maintainer=None, maintainer_email=None, url=None, help=None, keywords=None, widgets=None, priority=sys.maxsize, - icon=None, background=None, hidden=False + icon=None, background=None, dark_background=None, + hidden=False ): self.name = name @@ -498,6 +500,7 @@ def __init__(self, name=None, version=None, self.priority = priority self.icon = icon self.background = background + self.dark_background = dark_background self.hidden = hidden def __str__(self): diff --git a/orangecanvas/registry/qt.py b/orangecanvas/registry/qt.py index 200710711..945176e82 100644 --- a/orangecanvas/registry/qt.py +++ b/orangecanvas/registry/qt.py @@ -15,6 +15,7 @@ from AnyQt.QtCore import QObject, Qt from AnyQt.QtCore import pyqtSignal as Signal +from ..gui.palette_utils import create_palette, default_palette from ..utils import type_str from .discovery import WidgetDiscovery from .description import WidgetDescription, CategoryDescription @@ -234,14 +235,11 @@ def _cat_desc_to_std_item(self, desc): item.setIcon(icon) if desc.background: - background = desc.background + palette = create_palette(desc.background) else: - background = DEFAULT_COLOR - - background = NAMED_COLORS.get(background, background) + palette = default_palette() - brush = QBrush(QColor(background)) - item.setData(brush, self.BACKGROUND_ROLE) + item.setData(palette, self.BACKGROUND_ROLE) tooltip = desc.description if desc.description else desc.name @@ -267,18 +265,15 @@ def _widget_desc_to_std_item(self, desc, category): item.setIcon(icon) # This should be inherited from the category. - background = None if desc.background: background = desc.background + palette = create_palette(background) elif category.background: background = category.background + palette = create_palette(background) else: - background = DEFAULT_COLOR - - if background is not None: - background = NAMED_COLORS.get(background, background) - brush = QBrush(QColor(background)) - item.setData(brush, self.BACKGROUND_ROLE) + palette = default_palette() + item.setData(palette, self.BACKGROUND_ROLE) tooltip = tooltip_helper(desc) style = "ul { margin-top: 1px; margin-bottom: 1px; }" diff --git a/orangecanvas/registry/utils.py b/orangecanvas/registry/utils.py index e78d9f879..b1d6500c8 100644 --- a/orangecanvas/registry/utils.py +++ b/orangecanvas/registry/utils.py @@ -73,6 +73,7 @@ def widget_from_module_globals(module): priority = getattr(module, "PRIORITY", sys.maxsize) keywords = getattr(module, "KEYWORDS", None) background = getattr(module, "BACKGROUND", None) + dark_background = getattr(module, "DARK_BACKGROUND", None) replaces = getattr(module, "REPLACES", None) inputs = list(map(input_channel_from_args, inputs)) @@ -100,6 +101,7 @@ def widget_from_module_globals(module): priority=priority, icon=icon, background=background, + dark_background=dark_background, replaces=replaces) @@ -138,6 +140,7 @@ def category_from_package_globals(package): priority = getattr(package, "PRIORITY", sys.maxsize - 1) icon = getattr(package, "ICON", None) background = getattr(package, "BACKGROUND", None) + dark_background = getattr(package, "DARK_BACKGROUND", None) hidden = getattr(package, "HIDDEN", None) if priority == sys.maxsize - 1 and name.lower() == "prototypes": @@ -159,4 +162,5 @@ def category_from_package_globals(package): priority=priority, icon=icon, background=background, + dark_background=dark_background, hidden=hidden)