initial fluent-widgets ui
This commit is contained in:
521
qfluentwidgets/components/widgets/flyout.py
Normal file
521
qfluentwidgets/components/widgets/flyout.py
Normal file
@ -0,0 +1,521 @@
|
||||
# coding:utf-8
|
||||
from enum import Enum
|
||||
import sys
|
||||
from typing import Union
|
||||
|
||||
from PySide6.QtCore import (Qt, QPropertyAnimation, QPoint, QParallelAnimationGroup, QEasingCurve, QMargins,
|
||||
QRectF, QObject, QSize, Signal, QEvent)
|
||||
from PySide6.QtGui import QPixmap, QPainter, QColor, QCursor, QIcon, QImage, QPainterPath, QBrush, QMovie, QImageReader
|
||||
from PySide6.QtWidgets import QWidget, QGraphicsDropShadowEffect, QLabel, QHBoxLayout, QVBoxLayout, QApplication
|
||||
|
||||
from ...common.auto_wrap import TextWrap
|
||||
from ...common.style_sheet import isDarkTheme, FluentStyleSheet
|
||||
from ...common.icon import FluentIconBase, drawIcon, FluentIcon
|
||||
from ...common.screen import getCurrentScreenGeometry
|
||||
from .button import TransparentToolButton
|
||||
from .label import ImageLabel
|
||||
|
||||
|
||||
class FlyoutAnimationType(Enum):
|
||||
""" Flyout animation type """
|
||||
PULL_UP = 0
|
||||
DROP_DOWN = 1
|
||||
SLIDE_LEFT = 2
|
||||
SLIDE_RIGHT = 3
|
||||
FADE_IN = 4
|
||||
NONE = 5
|
||||
|
||||
|
||||
class IconWidget(QWidget):
|
||||
|
||||
def __init__(self, icon, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.setFixedSize(36, 54)
|
||||
self.icon = icon
|
||||
|
||||
def paintEvent(self, e):
|
||||
if not self.icon:
|
||||
return
|
||||
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHints(QPainter.Antialiasing |
|
||||
QPainter.SmoothPixmapTransform)
|
||||
|
||||
rect = QRectF(8, (self.height()-20)/2, 20, 20)
|
||||
drawIcon(self.icon, painter, rect)
|
||||
|
||||
|
||||
class FlyoutViewBase(QWidget):
|
||||
""" Flyout view base class """
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
def addWidget(self, widget: QWidget, stretch=0, align=Qt.AlignLeft):
|
||||
raise NotImplementedError
|
||||
|
||||
def backgroundColor(self):
|
||||
return QColor(40, 40, 40) if isDarkTheme() else QColor(248, 248, 248)
|
||||
|
||||
def borderColor(self):
|
||||
return QColor(0, 0, 0, 45) if isDarkTheme() else QColor(0, 0, 0, 17)
|
||||
|
||||
def paintEvent(self, e):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHints(QPainter.Antialiasing)
|
||||
|
||||
painter.setBrush(self.backgroundColor())
|
||||
painter.setPen(self.borderColor())
|
||||
|
||||
rect = self.rect().adjusted(1, 1, -1, -1)
|
||||
painter.drawRoundedRect(rect, 8, 8)
|
||||
|
||||
|
||||
class FlyoutView(FlyoutViewBase):
|
||||
""" Flyout view """
|
||||
|
||||
closed = Signal()
|
||||
|
||||
def __init__(self, title: str, content: str, icon: Union[FluentIconBase, QIcon, str] = None,
|
||||
image: Union[str, QPixmap, QImage] = None, isClosable=False, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
title: str
|
||||
the title of teaching tip
|
||||
|
||||
content: str
|
||||
the content of teaching tip
|
||||
|
||||
icon: InfoBarIcon | FluentIconBase | QIcon | str
|
||||
the icon of teaching tip
|
||||
|
||||
image: str | QPixmap | QImage
|
||||
the image of teaching tip
|
||||
|
||||
isClosable: bool
|
||||
whether to show the close button
|
||||
|
||||
parent: QWidget
|
||||
parent widget
|
||||
"""
|
||||
self.icon = icon
|
||||
self.title = title
|
||||
self.image = image
|
||||
self.content = content
|
||||
self.isClosable = isClosable
|
||||
|
||||
self.vBoxLayout = QVBoxLayout(self)
|
||||
self.viewLayout = QHBoxLayout()
|
||||
self.widgetLayout = QVBoxLayout()
|
||||
|
||||
self.titleLabel = QLabel(title, self)
|
||||
self.contentLabel = QLabel(content, self)
|
||||
self.iconWidget = IconWidget(icon, self)
|
||||
self.imageLabel = ImageLabel(self)
|
||||
self.closeButton = TransparentToolButton(FluentIcon.CLOSE, self)
|
||||
|
||||
self.__initWidgets()
|
||||
|
||||
def __initWidgets(self):
|
||||
self.imageLabel.setImage(self.image)
|
||||
|
||||
self.closeButton.setFixedSize(32, 32)
|
||||
self.closeButton.setIconSize(QSize(12, 12))
|
||||
self.closeButton.setVisible(self.isClosable)
|
||||
self.titleLabel.setVisible(bool(self.title))
|
||||
self.contentLabel.setVisible(bool(self.content))
|
||||
self.iconWidget.setHidden(self.icon is None)
|
||||
|
||||
self.closeButton.clicked.connect(self.closed)
|
||||
|
||||
self.titleLabel.setObjectName('titleLabel')
|
||||
self.contentLabel.setObjectName('contentLabel')
|
||||
FluentStyleSheet.TEACHING_TIP.apply(self)
|
||||
|
||||
self.__initLayout()
|
||||
|
||||
def __initLayout(self):
|
||||
self.vBoxLayout.setContentsMargins(1, 1, 1, 1)
|
||||
self.widgetLayout.setContentsMargins(0, 8, 0, 8)
|
||||
self.viewLayout.setSpacing(4)
|
||||
self.widgetLayout.setSpacing(0)
|
||||
self.vBoxLayout.setSpacing(0)
|
||||
|
||||
# add icon widget
|
||||
if not self.title or not self.content:
|
||||
self.iconWidget.setFixedHeight(36)
|
||||
|
||||
self.vBoxLayout.addLayout(self.viewLayout)
|
||||
self.viewLayout.addWidget(self.iconWidget, 0, Qt.AlignTop)
|
||||
|
||||
# add text
|
||||
self._adjustText()
|
||||
self.widgetLayout.addWidget(self.titleLabel)
|
||||
self.widgetLayout.addWidget(self.contentLabel)
|
||||
self.viewLayout.addLayout(self.widgetLayout)
|
||||
|
||||
# add close button
|
||||
self.closeButton.setVisible(self.isClosable)
|
||||
self.viewLayout.addWidget(
|
||||
self.closeButton, 0, Qt.AlignRight | Qt.AlignTop)
|
||||
|
||||
# adjust content margins
|
||||
margins = QMargins(6, 5, 6, 5)
|
||||
margins.setLeft(20 if not self.icon else 5)
|
||||
margins.setRight(20 if not self.isClosable else 6)
|
||||
self.viewLayout.setContentsMargins(margins)
|
||||
|
||||
# add image
|
||||
self._adjustImage()
|
||||
self._addImageToLayout()
|
||||
|
||||
def addWidget(self, widget: QWidget, stretch=0, align=Qt.AlignLeft):
|
||||
""" add widget to view """
|
||||
self.widgetLayout.addSpacing(8)
|
||||
self.widgetLayout.addWidget(widget, stretch, align)
|
||||
|
||||
def _addImageToLayout(self):
|
||||
self.imageLabel.setBorderRadius(8, 8, 0, 0)
|
||||
self.imageLabel.setHidden(self.imageLabel.isNull())
|
||||
self.vBoxLayout.insertWidget(0, self.imageLabel)
|
||||
|
||||
def _adjustText(self):
|
||||
w = min(900, QApplication.screenAt(
|
||||
QCursor.pos()).geometry().width() - 200)
|
||||
|
||||
# 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])
|
||||
|
||||
def _adjustImage(self):
|
||||
w = self.vBoxLayout.sizeHint().width() - 2
|
||||
self.imageLabel.scaledToWidth(w)
|
||||
|
||||
def showEvent(self, e):
|
||||
super().showEvent(e)
|
||||
self._adjustImage()
|
||||
self.adjustSize()
|
||||
|
||||
|
||||
class Flyout(QWidget):
|
||||
""" Flyout """
|
||||
|
||||
closed = Signal()
|
||||
|
||||
def __init__(self, view: FlyoutViewBase, parent=None, isDeleteOnClose=True, isMacInputMethodEnabled=False):
|
||||
super().__init__(parent=parent)
|
||||
self.view = view
|
||||
self.hBoxLayout = QHBoxLayout(self)
|
||||
self.aniManager = None # type: FlyoutAnimationManager
|
||||
self.isDeleteOnClose = isDeleteOnClose
|
||||
self.isMacInputMethodEnabled = isMacInputMethodEnabled
|
||||
|
||||
self.hBoxLayout.setContentsMargins(15, 8, 15, 20)
|
||||
self.hBoxLayout.addWidget(self.view)
|
||||
self.setShadowEffect()
|
||||
|
||||
self.setAttribute(Qt.WA_TranslucentBackground)
|
||||
|
||||
if sys.platform != "darwin" or not isMacInputMethodEnabled:
|
||||
self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint |
|
||||
Qt.NoDropShadowWindowHint)
|
||||
else:
|
||||
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint | Qt.NoDropShadowWindowHint)
|
||||
QApplication.instance().installEventFilter(self)
|
||||
|
||||
def eventFilter(self, watched, event):
|
||||
if sys.platform == "darwin" and self.isMacInputMethodEnabled:
|
||||
if self.isVisible() and event.type() == QEvent.MouseButtonPress:
|
||||
if not self.rect().contains(self.mapFromGlobal(event.globalPos())):
|
||||
self.close()
|
||||
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
def setShadowEffect(self, blurRadius=35, offset=(0, 8)):
|
||||
""" add shadow to dialog """
|
||||
color = QColor(0, 0, 0, 80 if isDarkTheme() else 30)
|
||||
self.shadowEffect = QGraphicsDropShadowEffect(self.view)
|
||||
self.shadowEffect.setBlurRadius(blurRadius)
|
||||
self.shadowEffect.setOffset(*offset)
|
||||
self.shadowEffect.setColor(color)
|
||||
self.view.setGraphicsEffect(None)
|
||||
self.view.setGraphicsEffect(self.shadowEffect)
|
||||
|
||||
def closeEvent(self, e):
|
||||
if self.isDeleteOnClose:
|
||||
self.deleteLater()
|
||||
|
||||
super().closeEvent(e)
|
||||
self.closed.emit()
|
||||
|
||||
def showEvent(self, e):
|
||||
# fixes #780
|
||||
self.activateWindow()
|
||||
super().showEvent(e)
|
||||
|
||||
def exec(self, pos: QPoint, aniType=FlyoutAnimationType.PULL_UP):
|
||||
""" show calendar view """
|
||||
self.aniManager = FlyoutAnimationManager.make(aniType, self)
|
||||
self.show()
|
||||
self.aniManager.exec(pos)
|
||||
|
||||
@classmethod
|
||||
def make(cls, view: FlyoutViewBase, target: Union[QWidget, QPoint] = None, parent=None,
|
||||
aniType=FlyoutAnimationType.PULL_UP, isDeleteOnClose=True, isMacInputMethodEnabled=False):
|
||||
""" create and show a flyout
|
||||
|
||||
Parameters
|
||||
----------
|
||||
view: FlyoutViewBase
|
||||
flyout view
|
||||
|
||||
target: QWidget | QPoint
|
||||
the target widget or position to show flyout
|
||||
|
||||
parent: QWidget
|
||||
parent window
|
||||
|
||||
aniType: FlyoutAnimationType
|
||||
flyout animation type
|
||||
|
||||
isDeleteOnClose: bool
|
||||
whether delete flyout automatically when flyout is closed
|
||||
"""
|
||||
w = cls(view, parent, isDeleteOnClose, isMacInputMethodEnabled)
|
||||
|
||||
if target is None:
|
||||
return w
|
||||
|
||||
# show flyout first so that we can get the correct size
|
||||
w.show()
|
||||
|
||||
# move flyout to the top of target
|
||||
if isinstance(target, QWidget):
|
||||
target = FlyoutAnimationManager.make(aniType, w).position(target)
|
||||
|
||||
w.exec(target, aniType)
|
||||
return w
|
||||
|
||||
@classmethod
|
||||
def create(cls, title: str, content: str, icon: Union[FluentIconBase, QIcon, str] = None,
|
||||
image: Union[str, QPixmap, QImage] = None, isClosable=False, target: Union[QWidget, QPoint] = None,
|
||||
parent=None, aniType=FlyoutAnimationType.PULL_UP, isDeleteOnClose=True, isMacInputMethodEnabled=False):
|
||||
""" create and show a flyout using the default view
|
||||
|
||||
Parameters
|
||||
----------
|
||||
title: str
|
||||
the title of teaching tip
|
||||
|
||||
content: str
|
||||
the content of teaching tip
|
||||
|
||||
icon: InfoBarIcon | FluentIconBase | QIcon | str
|
||||
the icon of teaching tip
|
||||
|
||||
image: str | QPixmap | QImage
|
||||
the image of teaching tip
|
||||
|
||||
isClosable: bool
|
||||
whether to show the close button
|
||||
|
||||
target: QWidget | QPoint
|
||||
the target widget or position to show flyout
|
||||
|
||||
parent: QWidget
|
||||
parent window
|
||||
|
||||
aniType: FlyoutAnimationType
|
||||
flyout animation type
|
||||
|
||||
isDeleteOnClose: bool
|
||||
whether delete flyout automatically when flyout is closed
|
||||
"""
|
||||
view = FlyoutView(title, content, icon, image, isClosable)
|
||||
w = cls.make(view, target, parent, aniType, isDeleteOnClose, isMacInputMethodEnabled)
|
||||
view.closed.connect(w.close)
|
||||
return w
|
||||
|
||||
def fadeOut(self):
|
||||
self.fadeOutAni = QPropertyAnimation(self, b'windowOpacity', self)
|
||||
self.fadeOutAni.finished.connect(self.close)
|
||||
self.fadeOutAni.setStartValue(1)
|
||||
self.fadeOutAni.setEndValue(0)
|
||||
self.fadeOutAni.setDuration(120)
|
||||
self.fadeOutAni.start()
|
||||
|
||||
|
||||
class FlyoutAnimationManager(QObject):
|
||||
""" Flyout animation manager """
|
||||
|
||||
managers = {}
|
||||
|
||||
def __init__(self, flyout: Flyout):
|
||||
super().__init__()
|
||||
self.flyout = flyout
|
||||
self.aniGroup = QParallelAnimationGroup(self)
|
||||
self.slideAni = QPropertyAnimation(flyout, b'pos', self)
|
||||
self.opacityAni = QPropertyAnimation(flyout, b'windowOpacity', self)
|
||||
|
||||
self.slideAni.setDuration(187)
|
||||
self.opacityAni.setDuration(187)
|
||||
|
||||
self.opacityAni.setStartValue(0)
|
||||
self.opacityAni.setEndValue(1)
|
||||
|
||||
self.slideAni.setEasingCurve(QEasingCurve.OutQuad)
|
||||
self.opacityAni.setEasingCurve(QEasingCurve.OutQuad)
|
||||
self.aniGroup.addAnimation(self.slideAni)
|
||||
self.aniGroup.addAnimation(self.opacityAni)
|
||||
|
||||
@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
|
||||
|
||||
def exec(self, pos: QPoint):
|
||||
""" start animation """
|
||||
raise NotImplementedError
|
||||
|
||||
def _adjustPosition(self, pos):
|
||||
rect = getCurrentScreenGeometry()
|
||||
w, h = self.flyout.sizeHint().width() + 5, self.flyout.sizeHint().height()
|
||||
x = max(rect.left(), min(pos.x(), rect.right() - w))
|
||||
y = max(rect.top(), min(pos.y() - 4, rect.bottom() - h + 5))
|
||||
return QPoint(x, y)
|
||||
|
||||
def position(self, target: QWidget):
|
||||
""" return the top left position relative to the target """
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def make(cls, aniType: FlyoutAnimationType, flyout: Flyout) -> "FlyoutAnimationManager":
|
||||
""" mask animation manager """
|
||||
if aniType not in cls.managers:
|
||||
raise ValueError(f'`{aniType}` is an invalid animation type.')
|
||||
|
||||
return cls.managers[aniType](flyout)
|
||||
|
||||
|
||||
@FlyoutAnimationManager.register(FlyoutAnimationType.PULL_UP)
|
||||
class PullUpFlyoutAnimationManager(FlyoutAnimationManager):
|
||||
""" Pull up flyout animation manager """
|
||||
|
||||
def position(self, target: QWidget):
|
||||
w = self.flyout
|
||||
pos = target.mapToGlobal(QPoint())
|
||||
x = pos.x() + target.width()//2 - w.sizeHint().width()//2
|
||||
y = pos.y() - w.sizeHint().height() + w.layout().contentsMargins().bottom()
|
||||
return QPoint(x, y)
|
||||
|
||||
def exec(self, pos: QPoint):
|
||||
pos = self._adjustPosition(pos)
|
||||
self.slideAni.setStartValue(pos+QPoint(0, 8))
|
||||
self.slideAni.setEndValue(pos)
|
||||
self.aniGroup.start()
|
||||
|
||||
|
||||
@FlyoutAnimationManager.register(FlyoutAnimationType.DROP_DOWN)
|
||||
class DropDownFlyoutAnimationManager(FlyoutAnimationManager):
|
||||
""" Drop down flyout animation manager """
|
||||
|
||||
def position(self, target: QWidget):
|
||||
w = self.flyout
|
||||
pos = target.mapToGlobal(QPoint(0, target.height()))
|
||||
x = pos.x() + target.width()//2 - w.sizeHint().width()//2
|
||||
y = pos.y() - w.layout().contentsMargins().top() + 8
|
||||
return QPoint(x, y)
|
||||
|
||||
def exec(self, pos: QPoint):
|
||||
pos = self._adjustPosition(pos)
|
||||
self.slideAni.setStartValue(pos-QPoint(0, 8))
|
||||
self.slideAni.setEndValue(pos)
|
||||
self.aniGroup.start()
|
||||
|
||||
|
||||
@FlyoutAnimationManager.register(FlyoutAnimationType.SLIDE_LEFT)
|
||||
class SlideLeftFlyoutAnimationManager(FlyoutAnimationManager):
|
||||
""" Slide left flyout animation manager """
|
||||
|
||||
def position(self, target: QWidget):
|
||||
w = self.flyout
|
||||
pos = target.mapToGlobal(QPoint(0, 0))
|
||||
x = pos.x() - w.sizeHint().width() + 8
|
||||
y = pos.y() - w.sizeHint().height()//2 + target.height()//2 + \
|
||||
w.layout().contentsMargins().top()
|
||||
return QPoint(x, y)
|
||||
|
||||
def exec(self, pos: QPoint):
|
||||
pos = self._adjustPosition(pos)
|
||||
self.slideAni.setStartValue(pos+QPoint(8, 0))
|
||||
self.slideAni.setEndValue(pos)
|
||||
self.aniGroup.start()
|
||||
|
||||
|
||||
@FlyoutAnimationManager.register(FlyoutAnimationType.SLIDE_RIGHT)
|
||||
class SlideRightFlyoutAnimationManager(FlyoutAnimationManager):
|
||||
""" Slide right flyout animation manager """
|
||||
|
||||
def position(self, target: QWidget):
|
||||
w = self.flyout
|
||||
pos = target.mapToGlobal(QPoint(0, 0))
|
||||
x = pos.x() + target.width() - 8
|
||||
y = pos.y() - w.sizeHint().height()//2 + target.height()//2 + \
|
||||
w.layout().contentsMargins().top()
|
||||
return QPoint(x, y)
|
||||
|
||||
def exec(self, pos: QPoint):
|
||||
pos = self._adjustPosition(pos)
|
||||
self.slideAni.setStartValue(pos-QPoint(8, 0))
|
||||
self.slideAni.setEndValue(pos)
|
||||
self.aniGroup.start()
|
||||
|
||||
|
||||
@FlyoutAnimationManager.register(FlyoutAnimationType.FADE_IN)
|
||||
class FadeInFlyoutAnimationManager(FlyoutAnimationManager):
|
||||
""" Fade in flyout animation manager """
|
||||
|
||||
def position(self, target: QWidget):
|
||||
w = self.flyout
|
||||
pos = target.mapToGlobal(QPoint())
|
||||
x = pos.x() + target.width()//2 - w.sizeHint().width()//2
|
||||
y = pos.y() - w.sizeHint().height() + w.layout().contentsMargins().bottom()
|
||||
return QPoint(x, y)
|
||||
|
||||
def exec(self, pos: QPoint):
|
||||
self.flyout.move(self._adjustPosition(pos))
|
||||
self.aniGroup.removeAnimation(self.slideAni)
|
||||
self.aniGroup.start()
|
||||
|
||||
|
||||
|
||||
@FlyoutAnimationManager.register(FlyoutAnimationType.NONE)
|
||||
class DummyFlyoutAnimationManager(FlyoutAnimationManager):
|
||||
""" Dummy flyout animation manager """
|
||||
|
||||
def exec(self, pos: QPoint):
|
||||
""" start animation """
|
||||
self.flyout.move(self._adjustPosition(pos))
|
||||
|
||||
def position(self, target: QWidget):
|
||||
""" return the top left position relative to the target """
|
||||
m = self.flyout.hBoxLayout.contentsMargins()
|
||||
return target.mapToGlobal(QPoint(-m.left(), -self.flyout.sizeHint().height()+m.bottom()-8))
|
||||
Reference in New Issue
Block a user