554 lines
15 KiB
Python
554 lines
15 KiB
Python
# coding:utf-8
|
|
|
|
from typing import List, Union
|
|
|
|
from PySide6.QtCore import Qt, Property, QPoint, Signal, QSize, QRectF, QUrl
|
|
from PySide6.QtGui import (QPainter, QPixmap, QPalette, QColor, QFont, QImage, QPainterPath,
|
|
QImageReader, QBrush, QMovie, QDesktopServices)
|
|
from PySide6.QtWidgets import QLabel, QWidget, QPushButton, QApplication
|
|
|
|
from ...common.exception_handler import exceptionHandler
|
|
from ...common.overload import singledispatchmethod
|
|
from ...common.font import setFont, getFont
|
|
from ...common.style_sheet import FluentStyleSheet, setCustomStyleSheet, setCustomStyleSheet
|
|
from ...common.config import qconfig, isDarkTheme
|
|
from .menu import LabelContextMenu
|
|
|
|
|
|
class PixmapLabel(QLabel):
|
|
""" Label for high dpi pixmap """
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.__pixmap = QPixmap()
|
|
|
|
def setPixmap(self, pixmap: QPixmap):
|
|
self.__pixmap = pixmap
|
|
self.setFixedSize(pixmap.size())
|
|
self.update()
|
|
|
|
def pixmap(self):
|
|
return self.__pixmap
|
|
|
|
def paintEvent(self, e):
|
|
if self.__pixmap.isNull():
|
|
return super().paintEvent(e)
|
|
|
|
painter = QPainter(self)
|
|
painter.setRenderHints(QPainter.Antialiasing |
|
|
QPainter.SmoothPixmapTransform)
|
|
painter.setPen(Qt.NoPen)
|
|
painter.drawPixmap(self.rect(), self.__pixmap)
|
|
|
|
|
|
class FluentLabelBase(QLabel):
|
|
""" Fluent label base class
|
|
|
|
Constructors
|
|
------------
|
|
* FluentLabelBase(`parent`: QWidget = None)
|
|
* FluentLabelBase(`text`: str, `parent`: QWidget = None)
|
|
"""
|
|
|
|
@singledispatchmethod
|
|
def __init__(self, parent: QWidget = None):
|
|
super().__init__(parent)
|
|
self._init()
|
|
|
|
@__init__.register
|
|
def _(self, text: str, parent: QWidget = None):
|
|
self.__init__(parent)
|
|
self.setText(text)
|
|
|
|
def _init(self):
|
|
FluentStyleSheet.LABEL.apply(self)
|
|
self.setFont(self.getFont())
|
|
self.setTextColor()
|
|
qconfig.themeChanged.connect(lambda: self.setTextColor(self.lightColor, self.darkColor))
|
|
|
|
self.customContextMenuRequested.connect(self._onContextMenuRequested)
|
|
return self
|
|
|
|
def getFont(self):
|
|
raise NotImplementedError
|
|
|
|
@exceptionHandler()
|
|
def setTextColor(self, light=QColor(0, 0, 0), dark=QColor(255, 255, 255)):
|
|
""" set the text color of label
|
|
|
|
Parameters
|
|
----------
|
|
light, dark: QColor | Qt.GlobalColor | str
|
|
text color in light/dark mode
|
|
"""
|
|
self._lightColor = QColor(light)
|
|
self._darkColor = QColor(dark)
|
|
|
|
setCustomStyleSheet(
|
|
self,
|
|
f"FluentLabelBase{{color:{self.lightColor.name(QColor.NameFormat.HexArgb)}}}",
|
|
f"FluentLabelBase{{color:{self.darkColor.name(QColor.NameFormat.HexArgb)}}}"
|
|
)
|
|
|
|
@Property(QColor)
|
|
def lightColor(self):
|
|
return self._lightColor
|
|
|
|
@lightColor.setter
|
|
def lightColor(self, color: QColor):
|
|
self.setTextColor(color, self.darkColor)
|
|
|
|
@Property(QColor)
|
|
def darkColor(self):
|
|
return self._darkColor
|
|
|
|
@darkColor.setter
|
|
def darkColor(self, color: QColor):
|
|
self.setTextColor(self.lightColor, color)
|
|
|
|
@Property(int)
|
|
def pixelFontSize(self):
|
|
return self.font().pixelSize()
|
|
|
|
@pixelFontSize.setter
|
|
def pixelFontSize(self, size: int):
|
|
font = self.font()
|
|
font.setPixelSize(size)
|
|
self.setFont(font)
|
|
|
|
@Property(bool)
|
|
def strikeOut(self):
|
|
return self.font().strikeOut()
|
|
|
|
@strikeOut.setter
|
|
def strikeOut(self, isStrikeOut: bool):
|
|
font = self.font()
|
|
font.setStrikeOut(isStrikeOut)
|
|
self.setFont(font)
|
|
|
|
@Property(bool)
|
|
def underline(self):
|
|
return self.font().underline()
|
|
|
|
@underline.setter
|
|
def underline(self, isUnderline: bool):
|
|
font = self.font()
|
|
font.setStyle()
|
|
font.setUnderline(isUnderline)
|
|
self.setFont(font)
|
|
|
|
def _onContextMenuRequested(self, pos):
|
|
menu = LabelContextMenu(parent=self)
|
|
menu.exec(self.mapToGlobal(pos))
|
|
|
|
|
|
class CaptionLabel(FluentLabelBase):
|
|
""" Caption text label
|
|
|
|
Constructors
|
|
------------
|
|
* CaptionLabel(`parent`: QWidget = None)
|
|
* CaptionLabel(`text`: str, `parent`: QWidget = None)
|
|
"""
|
|
|
|
def getFont(self):
|
|
return getFont(12)
|
|
|
|
|
|
class BodyLabel(FluentLabelBase):
|
|
""" Body text label
|
|
|
|
Constructors
|
|
------------
|
|
* BodyLabel(`parent`: QWidget = None)
|
|
* BodyLabel(`text`: str, `parent`: QWidget = None)
|
|
"""
|
|
|
|
def getFont(self):
|
|
return getFont(14)
|
|
|
|
|
|
class StrongBodyLabel(FluentLabelBase):
|
|
""" Strong body text label
|
|
|
|
Constructors
|
|
------------
|
|
* StrongBodyLabel(`parent`: QWidget = None)
|
|
* StrongBodyLabel(`text`: str, `parent`: QWidget = None)
|
|
"""
|
|
|
|
def getFont(self):
|
|
return getFont(14, QFont.DemiBold)
|
|
|
|
|
|
class SubtitleLabel(FluentLabelBase):
|
|
""" Subtitle text label
|
|
|
|
Constructors
|
|
------------
|
|
* SubtitleLabel(`parent`: QWidget = None)
|
|
* SubtitleLabel(`text`: str, `parent`: QWidget = None)
|
|
"""
|
|
|
|
def getFont(self):
|
|
return getFont(20, QFont.DemiBold)
|
|
|
|
|
|
class TitleLabel(FluentLabelBase):
|
|
""" Sub title text label
|
|
|
|
Constructors
|
|
------------
|
|
* TitleLabel(`parent`: QWidget = None)
|
|
* TitleLabel(`text`: str, `parent`: QWidget = None)
|
|
"""
|
|
|
|
def getFont(self):
|
|
return getFont(28, QFont.DemiBold)
|
|
|
|
|
|
class LargeTitleLabel(FluentLabelBase):
|
|
""" Large title text label
|
|
|
|
Constructors
|
|
------------
|
|
* LargeTitleLabel(`parent`: QWidget = None)
|
|
* LargeTitleLabel(`text`: str, `parent`: QWidget = None)
|
|
"""
|
|
|
|
def getFont(self):
|
|
return getFont(40, QFont.DemiBold)
|
|
|
|
|
|
class DisplayLabel(FluentLabelBase):
|
|
""" Display text label
|
|
|
|
Constructors
|
|
------------
|
|
* DisplayLabel(`parent`: QWidget = None)
|
|
* DisplayLabel(`text`: str, `parent`: QWidget = None)
|
|
"""
|
|
|
|
def getFont(self):
|
|
return getFont(68, QFont.DemiBold)
|
|
|
|
|
|
class ImageLabel(QLabel):
|
|
""" Image label
|
|
|
|
Constructors
|
|
------------
|
|
* ImageLabel(`parent`: QWidget = None)
|
|
* ImageLabel(`image`: str | QImage | QPixmap, `parent`: QWidget = None)
|
|
"""
|
|
|
|
clicked = Signal()
|
|
|
|
@singledispatchmethod
|
|
def __init__(self, parent: QWidget = None):
|
|
super().__init__(parent)
|
|
self.image = QImage()
|
|
self.setBorderRadius(0, 0, 0, 0)
|
|
self._postInit()
|
|
|
|
@__init__.register
|
|
def _(self, image: str, parent=None):
|
|
self.__init__(parent)
|
|
self.setImage(image)
|
|
|
|
@__init__.register
|
|
def _(self, image: QImage, parent=None):
|
|
self.__init__(parent)
|
|
self.setImage(image)
|
|
|
|
@__init__.register
|
|
def _(self, image: QPixmap, parent=None):
|
|
self.__init__(parent)
|
|
self.setImage(image)
|
|
|
|
def _postInit(self):
|
|
pass
|
|
|
|
def _onFrameChanged(self, index: int):
|
|
self.image = self.movie().currentImage()
|
|
self.update()
|
|
|
|
def setBorderRadius(self, topLeft: int, topRight: int, bottomLeft: int, bottomRight: int):
|
|
""" set the border radius of image """
|
|
self._topLeftRadius = topLeft
|
|
self._topRightRadius = topRight
|
|
self._bottomLeftRadius = bottomLeft
|
|
self._bottomRightRadius = bottomRight
|
|
self.update()
|
|
|
|
def setImage(self, image: Union[str, QPixmap, QImage] = None):
|
|
""" set the image of label """
|
|
self.image = image or QImage()
|
|
|
|
if isinstance(image, str):
|
|
reader = QImageReader(image)
|
|
if reader.supportsAnimation():
|
|
self.setMovie(QMovie(image))
|
|
else:
|
|
self.image = reader.read()
|
|
elif isinstance(image, QPixmap):
|
|
self.image = image.toImage()
|
|
|
|
self.setFixedSize(self.image.size())
|
|
self.update()
|
|
|
|
def scaledToWidth(self, width: int):
|
|
if self.isNull():
|
|
return
|
|
|
|
h = int(width / self.image.width() * self.image.height())
|
|
self.setFixedSize(width, h)
|
|
|
|
if self.movie():
|
|
self.movie().setScaledSize(QSize(width, h))
|
|
|
|
def scaledToHeight(self, height: int):
|
|
if self.isNull():
|
|
return
|
|
|
|
w = int(height / self.image.height() * self.image.width())
|
|
self.setFixedSize(w, height)
|
|
|
|
if self.movie():
|
|
self.movie().setScaledSize(QSize(w, height))
|
|
|
|
def setScaledSize(self, size: QSize):
|
|
if self.isNull():
|
|
return
|
|
|
|
self.setFixedSize(size)
|
|
|
|
if self.movie():
|
|
self.movie().setScaledSize(size)
|
|
|
|
def isNull(self):
|
|
return self.image.isNull()
|
|
|
|
def mouseReleaseEvent(self, e):
|
|
super().mouseReleaseEvent(e)
|
|
self.clicked.emit()
|
|
|
|
def setPixmap(self, pixmap: QPixmap):
|
|
self.setImage(pixmap)
|
|
|
|
def pixmap(self) -> QPixmap:
|
|
return QPixmap.fromImage(self.image)
|
|
|
|
def setMovie(self, movie: QMovie):
|
|
super().setMovie(movie)
|
|
self.movie().start()
|
|
self.image = self.movie().currentImage()
|
|
self.movie().frameChanged.connect(self._onFrameChanged)
|
|
|
|
def paintEvent(self, e):
|
|
if self.isNull():
|
|
return
|
|
|
|
painter = QPainter(self)
|
|
painter.setRenderHints(QPainter.Antialiasing)
|
|
|
|
path = QPainterPath()
|
|
w, h = self.width(), self.height()
|
|
|
|
# top line
|
|
path.moveTo(self.topLeftRadius, 0)
|
|
path.lineTo(w - self.topRightRadius, 0)
|
|
|
|
# top right arc
|
|
d = self.topRightRadius * 2
|
|
path.arcTo(w - d, 0, d, d, 90, -90)
|
|
|
|
# right line
|
|
path.lineTo(w, h - self.bottomRightRadius)
|
|
|
|
# bottom right arc
|
|
d = self.bottomRightRadius * 2
|
|
path.arcTo(w - d, h - d, d, d, 0, -90)
|
|
|
|
# bottom line
|
|
path.lineTo(self.bottomLeftRadius, h)
|
|
|
|
# bottom left arc
|
|
d = self.bottomLeftRadius * 2
|
|
path.arcTo(0, h - d, d, d, -90, -90)
|
|
|
|
# left line
|
|
path.lineTo(0, self.topLeftRadius)
|
|
|
|
# top left arc
|
|
d = self.topLeftRadius * 2
|
|
path.arcTo(0, 0, d, d, -180, -90)
|
|
|
|
# draw image
|
|
image = self.image.scaled(
|
|
self.size()*self.devicePixelRatioF(), Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
|
|
|
painter.setPen(Qt.NoPen)
|
|
painter.setClipPath(path)
|
|
painter.drawImage(self.rect(), image)
|
|
|
|
@Property(int)
|
|
def topLeftRadius(self):
|
|
return self._topLeftRadius
|
|
|
|
@topLeftRadius.setter
|
|
def topLeftRadius(self, radius: int):
|
|
self.setBorderRadius(radius, self.topRightRadius, self.bottomLeftRadius, self.bottomRightRadius)
|
|
|
|
@Property(int)
|
|
def topRightRadius(self):
|
|
return self._topRightRadius
|
|
|
|
@topRightRadius.setter
|
|
def topRightRadius(self, radius: int):
|
|
self.setBorderRadius(self.topLeftRadius, radius, self.bottomLeftRadius, self.bottomRightRadius)
|
|
|
|
@Property(int)
|
|
def bottomLeftRadius(self):
|
|
return self._bottomLeftRadius
|
|
|
|
@bottomLeftRadius.setter
|
|
def bottomLeftRadius(self, radius: int):
|
|
self.setBorderRadius(self.topLeftRadius, self.topRightRadius, radius, self.bottomRightRadius)
|
|
|
|
@Property(int)
|
|
def bottomRightRadius(self):
|
|
return self._bottomRightRadius
|
|
|
|
@bottomRightRadius.setter
|
|
def bottomRightRadius(self, radius: int):
|
|
self.setBorderRadius(
|
|
self.topLeftRadius, self.topRightRadius, self.bottomLeftRadius, radius)
|
|
|
|
|
|
class AvatarWidget(ImageLabel):
|
|
""" Avatar widget
|
|
|
|
Constructors
|
|
------------
|
|
* AvatarWidget(`parent`: QWidget = None)
|
|
* AvatarWidget(`image`: str | QImage | QPixmap, `parent`: QWidget = None)
|
|
"""
|
|
|
|
def _postInit(self):
|
|
self.setRadius(48)
|
|
self.lightBackgroundColor = QColor(0, 0, 0, 50)
|
|
self.darkBackgroundColor = QColor(255, 255, 255, 50)
|
|
|
|
def getRadius(self):
|
|
return self._radius
|
|
|
|
def setRadius(self, radius: int):
|
|
self._radius = radius
|
|
setFont(self, radius)
|
|
self.setFixedSize(2*radius, 2*radius)
|
|
self.update()
|
|
|
|
def setImage(self, image: Union[str, QPixmap, QImage] = None):
|
|
super().setImage(image)
|
|
self.setRadius(self.radius)
|
|
|
|
def setBackgroundColor(self, light: QColor, dark: QColor):
|
|
self.lightBackgroundColor = QColor(light)
|
|
self.darkBackgroundColor = QColor(light)
|
|
self.update()
|
|
|
|
def paintEvent(self, e):
|
|
painter = QPainter(self)
|
|
painter.setRenderHints(QPainter.Antialiasing)
|
|
|
|
if not self.isNull():
|
|
self._drawImageAvatar(painter)
|
|
else:
|
|
self._drawTextAvatar(painter)
|
|
|
|
def _drawImageAvatar(self, painter: QPainter):
|
|
# center crop image
|
|
image = self.image.scaled(
|
|
self.size()*self.devicePixelRatioF(), Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation) # type: QImage
|
|
|
|
iw, ih = image.width(), image.height()
|
|
d = self.getRadius() * 2 * self.devicePixelRatioF()
|
|
x, y = (iw - d) / 2, (ih - d) / 2
|
|
image = image.copy(int(x), int(y), int(d), int(d))
|
|
|
|
# draw image
|
|
path = QPainterPath()
|
|
path.addEllipse(QRectF(self.rect()))
|
|
|
|
painter.setPen(Qt.NoPen)
|
|
painter.setClipPath(path)
|
|
painter.drawImage(self.rect(), image)
|
|
|
|
def _drawTextAvatar(self, painter: QPainter):
|
|
if not self.text():
|
|
return
|
|
|
|
painter.setBrush(self.darkBackgroundColor if isDarkTheme() else self.lightBackgroundColor)
|
|
painter.setPen(Qt.NoPen)
|
|
painter.drawEllipse(QRectF(self.rect()))
|
|
|
|
painter.setFont(self.font())
|
|
painter.setPen(Qt.white if isDarkTheme() else Qt.black)
|
|
painter.drawText(self.rect(), Qt.AlignCenter, self.text()[0].upper())
|
|
|
|
radius = Property(int, getRadius, setRadius)
|
|
|
|
|
|
class HyperlinkLabel(QPushButton):
|
|
""" Hyperlink label
|
|
|
|
Constructors
|
|
------------
|
|
* HyperlinkLabel(`parent`: QWidget = None)
|
|
* HyperlinkLabel(`text`: str, `parent`: QWidget = None)
|
|
* HyperlinkLabel(`url`: QUrl, `parent`: QWidget = None)
|
|
"""
|
|
|
|
@singledispatchmethod
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent=parent)
|
|
self._url = QUrl()
|
|
|
|
setFont(self, 14)
|
|
self.setUnderlineVisible(False)
|
|
FluentStyleSheet.LABEL.apply(self)
|
|
self.setCursor(Qt.PointingHandCursor)
|
|
self.clicked.connect(self._onClicked)
|
|
|
|
@__init__.register
|
|
def _(self, text: str, parent=None):
|
|
self.__init__(parent)
|
|
self.setText(text)
|
|
|
|
@__init__.register
|
|
def _(self, url: QUrl, text: str, parent=None):
|
|
self.__init__(parent)
|
|
self.setText(text)
|
|
self._url = url
|
|
|
|
def getUrl(self) -> QUrl:
|
|
return self._url
|
|
|
|
def setUrl(self, url: Union[QUrl, str]):
|
|
self._url = QUrl(url)
|
|
|
|
def isUnderlineVisible(self):
|
|
return self._isUnderlineVisible
|
|
|
|
def setUnderlineVisible(self, isVisible: bool):
|
|
self._isUnderlineVisible = isVisible
|
|
self.setProperty('underline', isVisible)
|
|
self.setStyle(QApplication.style())
|
|
|
|
def _onClicked(self):
|
|
if self.getUrl().isValid():
|
|
QDesktopServices.openUrl(self.getUrl())
|
|
|
|
url = Property(QUrl, getUrl, setUrl)
|
|
underlineVisible = Property(bool, isUnderlineVisible, setUnderlineVisible) |