484 lines
16 KiB
Python
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)
|
||
|
|
|
||
|
|
|