You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

553 lines
18 KiB

# coding:utf-8
from enum import Enum
from typing import Union
import weakref
from PySide6.QtCore import (Qt, QEvent, QSize, QRectF, QObject, QPropertyAnimation,
QEasingCurve, QTimer, Signal, QParallelAnimationGroup, QPoint)
from PySide6.QtGui import QPainter, QIcon, QColor
from PySide6.QtWidgets import (QWidget, QFrame, QLabel, QHBoxLayout, QVBoxLayout,
QToolButton, QGraphicsOpacityEffect)
from ...common.auto_wrap import TextWrap
from ...common.style_sheet import FluentStyleSheet, themeColor
from ...common.icon import FluentIconBase, Theme, isDarkTheme, writeSvg, drawSvgIcon, drawIcon
from ...common.icon import FluentIcon as FIF
from .button import TransparentToolButton
class InfoBarIcon(FluentIconBase, Enum):
""" Info bar icon """
INFORMATION = "Info"
SUCCESS = "Success"
WARNING = "Warning"
ERROR = "Error"
def path(self, theme=Theme.AUTO):
if theme == Theme.AUTO:
color = "dark" if isDarkTheme() else "light"
else:
color = theme.value.lower()
return f':/qfluentwidgets/images/info_bar/{self.value}_{color}.svg'
class InfoBarPosition(Enum):
""" Info bar position """
TOP = 0
BOTTOM = 1
TOP_LEFT = 2
TOP_RIGHT = 3
BOTTOM_LEFT = 4
BOTTOM_RIGHT = 5
NONE = 6
class InfoIconWidget(QWidget):
""" Icon widget """
def __init__(self, icon: InfoBarIcon, parent=None):
super().__init__(parent=parent)
self.setFixedSize(36, 36)
self.icon = icon
def paintEvent(self, e):
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing |
QPainter.SmoothPixmapTransform)
rect = QRectF(10, 10, 15, 15)
if self.icon != InfoBarIcon.INFORMATION:
drawIcon(self.icon, painter, rect)
else:
drawIcon(self.icon, painter, rect, indexes=[0], fill=themeColor().name())
class InfoBar(QFrame):
""" Information bar """
closedSignal = Signal()
def __init__(self, icon: Union[InfoBarIcon, FluentIconBase, QIcon, str], title: str, content: str,
orient=Qt.Horizontal, isClosable=True, duration=1000, position=InfoBarPosition.TOP_RIGHT,
parent=None):
"""
Parameters
----------
icon: InfoBarIcon | FluentIconBase | QIcon | str
the icon of info bar
title: str
the title of info bar
content: str
the content of info bar
orient: Qt.Orientation
the layout direction of info bar, use `Qt.Horizontal` for short content
isClosable: bool
whether to show the close button
duraction: int
the time for info bar to display in milliseconds. If duration is less than zero,
info bar will never disappear.
parent: QWidget
parent widget
"""
super().__init__(parent=parent)
self.title = title
self.content = content
self.orient = orient
self.icon = icon
self.duration = duration
self.isClosable = isClosable
self.position = position
self.titleLabel = QLabel(self)
self.contentLabel = QLabel(self)
self.closeButton = TransparentToolButton(FIF.CLOSE, self)
self.iconWidget = InfoIconWidget(icon)
self.hBoxLayout = QHBoxLayout(self)
self.textLayout = QHBoxLayout() if self.orient == Qt.Horizontal else QVBoxLayout()
self.widgetLayout = QHBoxLayout() if self.orient == Qt.Horizontal else QVBoxLayout()
self.opacityEffect = QGraphicsOpacityEffect(self)
self.opacityAni = QPropertyAnimation(
self.opacityEffect, b'opacity', self)
self.lightBackgroundColor = None
self.darkBackgroundColor = None
self.__initWidget()
def __initWidget(self):
self.opacityEffect.setOpacity(1)
self.setGraphicsEffect(self.opacityEffect)
self.closeButton.setFixedSize(36, 36)
self.closeButton.setIconSize(QSize(12, 12))
self.closeButton.setCursor(Qt.PointingHandCursor)
self.closeButton.setVisible(self.isClosable)
self.__setQss()
self.__initLayout()
self.closeButton.clicked.connect(self.close)
def __initLayout(self):
self.hBoxLayout.setContentsMargins(6, 6, 6, 6)
self.hBoxLayout.setSizeConstraint(QVBoxLayout.SetMinimumSize)
self.textLayout.setSizeConstraint(QHBoxLayout.SetMinimumSize)
self.textLayout.setAlignment(Qt.AlignTop)
self.textLayout.setContentsMargins(1, 8, 0, 8)
self.hBoxLayout.setSpacing(0)
self.textLayout.setSpacing(5)
# add icon to layout
self.hBoxLayout.addWidget(self.iconWidget, 0, Qt.AlignTop | Qt.AlignLeft)
# add title to layout
self.textLayout.addWidget(self.titleLabel, 1, Qt.AlignTop)
self.titleLabel.setVisible(bool(self.title))
# add content label to layout
if self.orient == Qt.Horizontal:
self.textLayout.addSpacing(7)
self.textLayout.addWidget(self.contentLabel, 1, Qt.AlignTop)
self.contentLabel.setVisible(bool(self.content))
self.hBoxLayout.addLayout(self.textLayout)
# add widget layout
if self.orient == Qt.Horizontal:
self.hBoxLayout.addLayout(self.widgetLayout)
self.widgetLayout.setSpacing(10)
else:
self.textLayout.addLayout(self.widgetLayout)
# add close button to layout
self.hBoxLayout.addSpacing(12)
self.hBoxLayout.addWidget(self.closeButton, 0, Qt.AlignTop | Qt.AlignLeft)
self._adjustText()
def __setQss(self):
self.titleLabel.setObjectName('titleLabel')
self.contentLabel.setObjectName('contentLabel')
if isinstance(self.icon, Enum):
self.setProperty('type', self.icon.value)
FluentStyleSheet.INFO_BAR.apply(self)
def __fadeOut(self):
""" fade out """
self.opacityAni.setDuration(200)
self.opacityAni.setStartValue(1)
self.opacityAni.setEndValue(0)
self.opacityAni.finished.connect(self.close)
self.opacityAni.start()
def _adjustText(self):
w = 900 if not self.parent() else (self.parent().width() - 50)
# adjust title
chars = max(min(w / 10, 120), 30)
self.titleLabel.setText(TextWrap.wrap(self.title, chars, False)[0])
# adjust content
chars = max(min(w / 9, 120), 30)
self.contentLabel.setText(TextWrap.wrap(self.content, chars, False)[0])
self.adjustSize()
def addWidget(self, widget: QWidget, stretch=0):
""" add widget to info bar """
self.widgetLayout.addSpacing(6)
align = Qt.AlignTop if self.orient == Qt.Vertical else Qt.AlignVCenter
self.widgetLayout.addWidget(widget, stretch, Qt.AlignLeft | align)
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 eventFilter(self, obj, e: QEvent):
if obj is self.parent():
if e.type() in [QEvent.Resize, QEvent.WindowStateChange]:
self._adjustText()
return super().eventFilter(obj, e)
def closeEvent(self, e):
self.closedSignal.emit()
self.deleteLater()
def showEvent(self, e):
self._adjustText()
super().showEvent(e)
if self.duration >= 0:
QTimer.singleShot(self.duration, self.__fadeOut)
if self.position != InfoBarPosition.NONE:
manager = InfoBarManager.make(self.position)
manager.add(self)
if self.parent():
self.parent().installEventFilter(self)
def paintEvent(self, e):
super().paintEvent(e)
if self.lightBackgroundColor is None:
return
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing)
painter.setPen(Qt.NoPen)
if isDarkTheme():
painter.setBrush(self.darkBackgroundColor)
else:
painter.setBrush(self.lightBackgroundColor)
rect = self.rect().adjusted(1, 1, -1, -1)
painter.drawRoundedRect(rect, 6, 6)
@classmethod
def new(cls, icon, title, content, orient=Qt.Horizontal, isClosable=True, duration=1000,
position=InfoBarPosition.TOP_RIGHT, parent=None):
w = InfoBar(icon, title, content, orient,
isClosable, duration, position, parent)
w.show()
return w
@classmethod
def info(cls, title, content, orient=Qt.Horizontal, isClosable=True, duration=1000,
position=InfoBarPosition.TOP_RIGHT, parent=None):
return cls.new(InfoBarIcon.INFORMATION, title, content, orient, isClosable, duration, position, parent)
@classmethod
def success(cls, title, content, orient=Qt.Horizontal, isClosable=True, duration=1000,
position=InfoBarPosition.TOP_RIGHT, parent=None):
return cls.new(InfoBarIcon.SUCCESS, title, content, orient, isClosable, duration, position, parent)
@classmethod
def warning(cls, title, content, orient=Qt.Horizontal, isClosable=True, duration=1000,
position=InfoBarPosition.TOP_RIGHT, parent=None):
return cls.new(InfoBarIcon.WARNING, title, content, orient, isClosable, duration, position, parent)
@classmethod
def error(cls, title, content, orient=Qt.Horizontal, isClosable=True, duration=1000,
position=InfoBarPosition.TOP_RIGHT, parent=None):
return cls.new(InfoBarIcon.ERROR, title, content, orient, isClosable, duration, position, parent)
class InfoBarManager(QObject):
""" Info bar manager """
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(InfoBarManager, cls).__new__(
cls, *args, **kwargs)
cls._instance.__initialized = False
return cls._instance
def __init__(self):
if self.__initialized:
return
super().__init__()
self.spacing = 16
self.margin = 24
self.infoBars = weakref.WeakKeyDictionary()
self.aniGroups = weakref.WeakKeyDictionary()
self.slideAnis = []
self.dropAnis = []
self.__initialized = True
def add(self, infoBar: InfoBar):
""" add info bar """
p = infoBar.parent() # type:QWidget
if not p:
return
if p not in self.infoBars:
p.installEventFilter(self)
self.infoBars[p] = []
self.aniGroups[p] = QParallelAnimationGroup(self)
if infoBar in self.infoBars[p]:
return
# add drop animation
if self.infoBars[p]:
dropAni = QPropertyAnimation(infoBar, b'pos')
dropAni.setDuration(200)
self.aniGroups[p].addAnimation(dropAni)
self.dropAnis.append(dropAni)
infoBar.setProperty('dropAni', dropAni)
# add slide animation
self.infoBars[p].append(infoBar)
slideAni = self._createSlideAni(infoBar)
self.slideAnis.append(slideAni)
infoBar.setProperty('slideAni', slideAni)
infoBar.closedSignal.connect(lambda: self.remove(infoBar))
slideAni.start()
def remove(self, infoBar: InfoBar):
""" remove info bar """
p = infoBar.parent()
if p not in self.infoBars:
return
if infoBar not in self.infoBars[p]:
return
self.infoBars[p].remove(infoBar)
# remove drop animation
dropAni = infoBar.property('dropAni') # type: QPropertyAnimation
if dropAni:
self.aniGroups[p].removeAnimation(dropAni)
self.dropAnis.remove(dropAni)
# remove slider animation
slideAni = infoBar.property('slideAni')
if slideAni:
self.slideAnis.remove(slideAni)
# adjust the position of the remaining info bars
self._updateDropAni(p)
self.aniGroups[p].start()
def _createSlideAni(self, infoBar: InfoBar):
slideAni = QPropertyAnimation(infoBar, b'pos')
slideAni.setEasingCurve(QEasingCurve.OutQuad)
slideAni.setDuration(200)
slideAni.setStartValue(self._slideStartPos(infoBar))
slideAni.setEndValue(self._pos(infoBar))
return slideAni
def _updateDropAni(self, parent):
for bar in self.infoBars[parent]:
ani = bar.property('dropAni')
if not ani:
continue
ani.setStartValue(bar.pos())
ani.setEndValue(self._pos(bar))
def _pos(self, infoBar: InfoBar, parentSize=None) -> QPoint:
raise NotImplementedError
def _slideStartPos(self, infoBar: InfoBar) -> QPoint:
raise NotImplementedError
def eventFilter(self, obj, e: QEvent):
if obj not in self.infoBars:
return False
if e.type() in [QEvent.Resize, QEvent.WindowStateChange]:
size = e.size() if e.type() == QEvent.Resize else None
for bar in self.infoBars[obj]:
bar.move(self._pos(bar, size))
return super().eventFilter(obj, e)
@staticmethod
def make(position: InfoBarPosition):
""" mask info bar manager according to the display position """
managers = {
InfoBarPosition.TOP: TopInfoBarManager,
InfoBarPosition.BOTTOM: BottomInfoBarManager,
InfoBarPosition.TOP_RIGHT: TopRightInfoBarManager,
InfoBarPosition.BOTTOM_RIGHT: BottomRightInfoBarManager,
InfoBarPosition.TOP_LEFT: TopLeftInfoBarManager,
InfoBarPosition.BOTTOM_LEFT: BottomLeftInfoBarManager,
}
if position not in managers:
raise ValueError(f'`{position}` is an invalid info bar position.')
return managers[position]()
class TopInfoBarManager(InfoBarManager):
""" Top position info bar manager """
def _pos(self, infoBar: InfoBar, parentSize=None):
p = infoBar.parent()
parentSize = parentSize or p.size()
x = (infoBar.parent().width() - infoBar.width()) // 2
y = self.margin
index = self.infoBars[p].index(infoBar)
for bar in self.infoBars[p][0:index]:
y += (bar.height() + self.spacing)
return QPoint(x, y)
def _slideStartPos(self, infoBar: InfoBar):
pos = self._pos(infoBar)
return QPoint(pos.x(), pos.y() - 16)
class TopRightInfoBarManager(InfoBarManager):
""" Top right position info bar manager """
def _pos(self, infoBar: InfoBar, parentSize=None):
p = infoBar.parent()
parentSize = parentSize or p.size()
x = parentSize.width() - infoBar.width() - self.margin
y = self.margin
index = self.infoBars[p].index(infoBar)
for bar in self.infoBars[p][0:index]:
y += (bar.height() + self.spacing)
return QPoint(x, y)
def _slideStartPos(self, infoBar: InfoBar):
return QPoint(infoBar.parent().width(), self._pos(infoBar).y())
class BottomRightInfoBarManager(InfoBarManager):
""" Bottom right position info bar manager """
def _pos(self, infoBar: InfoBar, parentSize=None) -> QPoint:
p = infoBar.parent()
parentSize = parentSize or p.size()
x = parentSize.width() - infoBar.width() - self.margin
y = parentSize.height() - infoBar.height() - self.margin
index = self.infoBars[p].index(infoBar)
for bar in self.infoBars[p][0:index]:
y -= (bar.height() + self.spacing)
return QPoint(x, y)
def _slideStartPos(self, infoBar: InfoBar):
return QPoint(infoBar.parent().width(), self._pos(infoBar).y())
class TopLeftInfoBarManager(InfoBarManager):
""" Top left position info bar manager """
def _pos(self, infoBar: InfoBar, parentSize=None) -> QPoint:
p = infoBar.parent()
parentSize = parentSize or p.size()
y = self.margin
index = self.infoBars[p].index(infoBar)
for bar in self.infoBars[p][0:index]:
y += (bar.height() + self.spacing)
return QPoint(self.margin, y)
def _slideStartPos(self, infoBar: InfoBar):
return QPoint(-infoBar.width(), self._pos(infoBar).y())
class BottomLeftInfoBarManager(InfoBarManager):
""" Bottom left position info bar manager """
def _pos(self, infoBar: InfoBar, parentSize: QSize = None) -> QPoint:
p = infoBar.parent()
parentSize = parentSize or p.size()
y = parentSize.height() - infoBar.height() - self.margin
index = self.infoBars[p].index(infoBar)
for bar in self.infoBars[p][0:index]:
y -= (bar.height() + self.spacing)
return QPoint(self.margin, y)
def _slideStartPos(self, infoBar: InfoBar):
return QPoint(-infoBar.width(), self._pos(infoBar).y())
class BottomInfoBarManager(InfoBarManager):
""" Bottom position info bar manager """
def _pos(self, infoBar: InfoBar, parentSize: QSize = None) -> QPoint:
p = infoBar.parent()
parentSize = parentSize or p.size()
x = (parentSize.width() - infoBar.width()) // 2
y = parentSize.height() - infoBar.height() - self.margin
index = self.infoBars[p].index(infoBar)
for bar in self.infoBars[p][0:index]:
y -= (bar.height() + self.spacing)
return QPoint(x, y)
def _slideStartPos(self, infoBar: InfoBar):
pos = self._pos(infoBar)
return QPoint(pos.x(), pos.y() + 16)