Files
2025-08-14 18:45:16 +08:00

484 lines
16 KiB
Python

# coding:utf-8
from enum import Enum
from typing import Union
from PySide6.QtCore import Qt, QEvent, QRectF, QPoint, QObject, QSize
from PySide6.QtGui import QPixmap, QPainter, QColor, QIcon
from PySide6.QtWidgets import QLabel, QWidget, QSizePolicy
from ...common.font import setFont
from ...common.icon import drawIcon, FluentIconBase, toQIcon
from ...common.overload import singledispatchmethod
from ...common.style_sheet import themeColor, FluentStyleSheet, isDarkTheme, Theme
class InfoLevel(Enum):
""" Info level """
INFOAMTION = 'Info'
SUCCESS = 'Success'
ATTENTION = 'Attension'
WARNING = "Warning"
ERROR = "Error"
class InfoBadgePosition(Enum):
""" Info badge position """
TOP_RIGHT = 0
BOTTOM_RIGHT = 1
RIGHT = 2
TOP_LEFT = 3
BOTTOM_LEFT = 4
LEFT = 5
NAVIGATION_ITEM = 6
class InfoBadge(QLabel):
""" Information badge
Constructors
------------
* InfoBadge(`parent`: QWidget = None, `level`=InfoLevel.ATTENTION)
* InfoBadge(`text`: str, `parent`: QWidget = None, `level`=InfoLevel.ATTENTION)
* InfoBadge(`num`: int, `parent`: QWidget = None, `level`=InfoLevel.ATTENTION)
* InfoBadge(`num`: float, `parent`: QWidget = None, `level`=InfoLevel.ATTENTION)
"""
@singledispatchmethod
def __init__(self, parent: QWidget = None, level=InfoLevel.ATTENTION):
super().__init__(parent=parent)
self.level = InfoLevel.INFOAMTION
self.lightBackgroundColor = None
self.darkBackgroundColor = None
self.manager = None # type: InfoBadgeManager
self.setLevel(level)
setFont(self, 11)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setAttribute(Qt.WA_TransparentForMouseEvents)
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
FluentStyleSheet.INFO_BADGE.apply(self)
@__init__.register
def _(self, text: str, parent: QWidget = None, level=InfoLevel.ATTENTION):
self.__init__(parent, level)
self.setText(text)
@__init__.register
def _(self, num: int, parent: QWidget = None, level=InfoLevel.ATTENTION):
self.__init__(parent, level)
self.setNum(num)
@__init__.register
def _(self, num: float, parent: QWidget = None, level=InfoLevel.ATTENTION):
self.__init__(parent, level)
self.setNum(num)
def setLevel(self, level: InfoLevel):
""" set infomation level """
if level == self.level:
return
self.level = level
self.setProperty('level', level.value)
self.update()
def setProperty(self, name: str, value):
super().setProperty(name, value)
if name != "level":
return
values = [i.value for i in InfoLevel._member_map_.values()]
if value in values:
self.level = InfoLevel(value)
def setCustomBackgroundColor(self, light, dark):
""" set the custom background color
Parameters
----------
light, dark: str | Qt.GlobalColor | QColor
background color in light/dark theme mode
"""
self.lightBackgroundColor = QColor(light)
self.darkBackgroundColor = QColor(dark)
self.update()
def paintEvent(self, e):
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing)
painter.setPen(Qt.NoPen)
painter.setBrush(self._backgroundColor())
r = self.height() / 2
painter.drawRoundedRect(self.rect(), r, r)
super().paintEvent(e)
def _backgroundColor(self):
isDark = isDarkTheme()
if self.lightBackgroundColor:
color = self.darkBackgroundColor if isDark else self.lightBackgroundColor
elif self.level == InfoLevel.INFOAMTION:
color = QColor(157, 157, 157) if isDark else QColor(138, 138, 138)
elif self.level == InfoLevel.SUCCESS:
color = QColor(108, 203, 95) if isDark else QColor(15, 123, 15)
elif self.level == InfoLevel.ATTENTION:
color = themeColor()
elif self.level == InfoLevel.WARNING:
color = QColor(255, 244, 206) if isDark else QColor(157, 93, 0)
else:
color = QColor(255, 153, 164) if isDark else QColor(196, 43, 28)
return color
@classmethod
def make(cls, text: Union[str, float], parent=None, level=InfoLevel.INFOAMTION, target: QWidget = None,
position=InfoBadgePosition.TOP_RIGHT):
w = InfoBadge(text, parent, level)
w.adjustSize()
if target:
w.manager = InfoBadgeManager.make(position, target, w)
w.move(w.manager.position())
return w
@classmethod
def info(cls, text: Union[str, float], parent=None, target: QWidget = None, position=InfoBadgePosition.TOP_RIGHT):
return cls.make(text, parent, InfoLevel.INFOAMTION, target, position)
@classmethod
def success(cls, text: Union[str, float], parent=None, target: QWidget = None, position=InfoBadgePosition.TOP_RIGHT):
return cls.make(text, parent, InfoLevel.SUCCESS, target, position)
@classmethod
def attension(cls, text: Union[str, float], parent=None, target: QWidget = None, position=InfoBadgePosition.TOP_RIGHT):
return cls.make(text, parent, InfoLevel.ATTENTION, target, position)
@classmethod
def warning(cls, text: Union[str, float], parent=None, target: QWidget = None, position=InfoBadgePosition.TOP_RIGHT):
return cls.make(text, parent, InfoLevel.WARNING, target, position)
@classmethod
def error(cls, text: Union[str, float], parent=None, target: QWidget = None, position=InfoBadgePosition.TOP_RIGHT):
return cls.make(text, parent, InfoLevel.ERROR, target, position)
@classmethod
def custom(cls, text: Union[str, float], light: QColor, dark: QColor, parent=None, target: QWidget = None,
position=InfoBadgePosition.TOP_RIGHT):
""" create a badge with custom background color
Parameters
----------
text: str | float
the text of badge
light, dark: str | Qt.GlobalColor | QColor
background color in light/dark theme mode
parent: QWidget
parent widget
target: QWidget
target widget to show the badge
pos: InfoBadgePosition
the position relative to target
"""
w = cls.make(text, parent, target=target, position=position)
w.setCustomBackgroundColor(light, dark)
return w
class DotInfoBadge(InfoBadge):
""" Dot info badge """
def __init__(self, parent=None, level=InfoLevel.ATTENTION):
super().__init__(parent, level)
self.setFixedSize(4, 4)
def paintEvent(self, e):
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing)
painter.setPen(Qt.NoPen)
painter.setBrush(self._backgroundColor())
painter.drawEllipse(self.rect())
@classmethod
def make(cls, parent=None, level=InfoLevel.INFOAMTION, target: QWidget = None,
position=InfoBadgePosition.TOP_RIGHT):
w = DotInfoBadge(parent, level)
if target:
w.manager = InfoBadgeManager.make(position, target, w)
w.move(w.manager.position())
return w
@classmethod
def info(cls, parent=None, target: QWidget = None, position=InfoBadgePosition.TOP_RIGHT):
return cls.make(parent, InfoLevel.INFOAMTION, target, position)
@classmethod
def success(cls, parent=None, target: QWidget = None, position=InfoBadgePosition.TOP_RIGHT):
return cls.make(parent, InfoLevel.SUCCESS, target, position)
@classmethod
def attension(cls, parent=None, target: QWidget = None, position=InfoBadgePosition.TOP_RIGHT):
return cls.make(parent, InfoLevel.ATTENTION, target, position)
@classmethod
def warning(cls, parent=None, target: QWidget = None, position=InfoBadgePosition.TOP_RIGHT):
return cls.make(parent, InfoLevel.WARNING, target, position)
@classmethod
def error(cls, parent=None, target: QWidget = None, position=InfoBadgePosition.TOP_RIGHT):
return cls.make(parent, InfoLevel.ERROR, target, position)
@classmethod
def custom(cls, light: QColor, dark: QColor, parent=None, target: QWidget = None,
position=InfoBadgePosition.TOP_RIGHT):
""" create a badge with custom background color
Parameters
----------
light, dark: str | Qt.GlobalColor | QColor
background color in light/dark theme mode
parent: QWidget
parent widget
"""
w = cls.make(parent, target=target, position=position)
w.setCustomBackgroundColor(light, dark)
return w
class IconInfoBadge(InfoBadge):
""" Icon icon badge
Constructors
------------
* IconInfoBadge(`parent`: QWidget = None, `level`=InfoLevel.ATTENTION)
* IconInfoBadge(`icon`: QIcon | str | FluentIconBase, `parent`: QWidget = None, `level`=InfoLevel.ATTENTION)
"""
@singledispatchmethod
def __init__(self, parent: QWidget = None, level=InfoLevel.ATTENTION):
super().__init__(parent=parent, level=level)
self._icon = QIcon()
self._iconSize = QSize(8, 8)
self.setFixedSize(16, 16)
@__init__.register
def _(self, icon: FluentIconBase, parent: QWidget = None, level=InfoLevel.ATTENTION):
self.__init__(parent, level)
self.setIcon(icon)
@__init__.register
def _(self, icon: QIcon, parent: QWidget = None, level=InfoLevel.ATTENTION):
self.__init__(parent, level)
self.setIcon(icon)
def setIcon(self, icon: Union[QIcon, FluentIconBase, str]):
""" set the icon of info badge """
self._icon = icon
self.update()
def icon(self):
return toQIcon(self._icon)
def iconSize(self):
return self._iconSize
def setIconSize(self, size: QSize):
self._iconSize = size
self.update()
def paintEvent(self, e):
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing)
painter.setPen(Qt.NoPen)
painter.setBrush(self._backgroundColor())
painter.drawEllipse(self.rect())
iw, ih = self.iconSize().width(), self.iconSize().height()
x, y = (self.width() - iw) / 2, (self.width() - ih) / 2
rect = QRectF(x, y, iw, ih)
if isinstance(self._icon, FluentIconBase):
theme = Theme.DARK if not isDarkTheme() else Theme.LIGHT
self._icon.render(painter, rect, theme)
else:
drawIcon(self._icon, painter, rect)
@classmethod
def make(cls, icon: Union[QIcon, FluentIconBase], parent=None, level=InfoLevel.INFOAMTION, target: QWidget = None,
position=InfoBadgePosition.TOP_RIGHT):
w = IconInfoBadge(icon, parent, level)
if target:
w.manager = InfoBadgeManager.make(position, target, w)
w.move(w.manager.position())
return w
@classmethod
def info(cls, icon: Union[QIcon, FluentIconBase], parent=None, target: QWidget = None,
position=InfoBadgePosition.TOP_RIGHT):
return cls.make(icon, parent, InfoLevel.INFOAMTION, target, position)
@classmethod
def success(cls, icon: Union[QIcon, FluentIconBase], parent=None, target: QWidget = None,
position=InfoBadgePosition.TOP_RIGHT):
return cls.make(icon, parent, InfoLevel.SUCCESS, target, position)
@classmethod
def attension(cls, icon: Union[QIcon, FluentIconBase], parent=None, target: QWidget = None,
position=InfoBadgePosition.TOP_RIGHT):
return cls.make(icon, parent, InfoLevel.ATTENTION, target, position)
@classmethod
def warning(cls, icon: Union[QIcon, FluentIconBase], parent=None, target: QWidget = None,
position=InfoBadgePosition.TOP_RIGHT):
return cls.make(icon, parent, InfoLevel.WARNING, target, position)
@classmethod
def error(cls, icon: Union[QIcon, FluentIconBase], parent=None, target: QWidget = None,
position=InfoBadgePosition.TOP_RIGHT):
return cls.make(icon, parent, InfoLevel.ERROR, target, position)
@classmethod
def custom(cls, icon: Union[QIcon, FluentIconBase], light: QColor, dark: QColor, parent=None,
target: QWidget = None, position=InfoBadgePosition.TOP_RIGHT):
""" create a badge with custom background color
Parameters
----------
icon: QIcon | FluentIconBase
the icon of badge
light, dark: str | Qt.GlobalColor | QColor
background color in light/dark theme mode
parent: QWidget
parent widget
"""
w = cls.make(icon, parent, target=target, position=position)
w.setCustomBackgroundColor(light, dark)
return w
class InfoBadgeManager(QObject):
""" Info badge manager """
managers = {}
def __init__(self, target: QWidget, badge: InfoBadge):
super().__init__()
self.target = target
self.badge = badge
self.target.installEventFilter(self)
def eventFilter(self, obj, e: QEvent):
if obj is self.target:
if e.type() in [QEvent.Resize, QEvent.Move]:
self.badge.move(self.position())
return super().eventFilter(obj, e)
@classmethod
def register(cls, name):
""" register menu animation manager
Parameters
----------
name: Any
the name of manager, it should be unique
"""
def wrapper(Manager):
if name not in cls.managers:
cls.managers[name] = Manager
return Manager
return wrapper
@classmethod
def make(cls, position: InfoBadgePosition, target: QWidget, badge: InfoBadge):
""" mask info badge manager """
if position not in cls.managers:
raise ValueError(f'`{position}` is an invalid animation type.')
return cls.managers[position](target, badge)
def position(self):
""" return the position of info badge """
return QPoint()
@InfoBadgeManager.register(InfoBadgePosition.TOP_RIGHT)
class TopRightInfoBadgeManager(InfoBadgeManager):
""" Top right info badge manager """
def position(self):
pos = self.target.geometry().topRight()
x = pos.x() - self.badge.width() // 2
y = pos.y() - self.badge.height() // 2
return QPoint(x, y)
@InfoBadgeManager.register(InfoBadgePosition.RIGHT)
class RightInfoBadgeManager(InfoBadgeManager):
""" Right info badge manager """
def position(self):
x = self.target.geometry().right() - self.badge.width() // 2
y = self.target.geometry().center().y() - self.badge.height() // 2
return QPoint(x, y)
@InfoBadgeManager.register(InfoBadgePosition.BOTTOM_RIGHT)
class BottomRightInfoBadgeManager(InfoBadgeManager):
""" Bottom right info badge manager """
def position(self):
pos = self.target.geometry().bottomRight()
x = pos.x() - self.badge.width() // 2
y = pos.y() - self.badge.height() // 2
return QPoint(x, y)
@InfoBadgeManager.register(InfoBadgePosition.TOP_LEFT)
class TopLeftInfoBadgeManager(InfoBadgeManager):
""" Top left info badge manager """
def position(self):
x = self.target.x() - self.badge.width() // 2
y = self.target.y() - self.badge.height() // 2
return QPoint(x, y)
@InfoBadgeManager.register(InfoBadgePosition.LEFT)
class LeftInfoBadgeManager(InfoBadgeManager):
""" Top left info badge manager """
def position(self):
x = self.target.x() - self.badge.width() // 2
y = self.target.geometry().center().y() - self.badge.height() // 2
return QPoint(x, y)
@InfoBadgeManager.register(InfoBadgePosition.BOTTOM_LEFT)
class BottomLeftInfoBadgeManager(InfoBadgeManager):
""" Bottom left info badge manager """
def position(self):
pos = self.target.geometry().bottomLeft()
x = pos.x() - self.badge.width() // 2
y = pos.y() - self.badge.height() // 2
return QPoint(x, y)