# coding:utf-8 from typing import Union from PySide6.QtCore import Signal, QUrl, Qt, QRectF, QSize, QPoint, Property from PySide6.QtGui import QDesktopServices, QIcon, QPainter, QFont from PySide6.QtWidgets import QHBoxLayout, QPushButton, QRadioButton, QToolButton, QApplication, QWidget, QSizePolicy from ...common.animation import TranslateYAnimation from ...common.icon import FluentIconBase, drawIcon, isDarkTheme, Theme, toQIcon from ...common.icon import FluentIcon as FIF from ...common.font import setFont from ...common.style_sheet import FluentStyleSheet from ...common.overload import singledispatchmethod from .menu import RoundMenu class PushButton(QPushButton): """ push button """ @singledispatchmethod def __init__(self, parent: QWidget = None): super().__init__(parent) FluentStyleSheet.BUTTON.apply(self) self.isPressed = False self.isHover = False self.setIconSize(QSize(16, 16)) self.setIcon(None) setFont(self) self._postInit() @__init__.register def _(self, text: str, parent: QWidget = None, icon: Union[QIcon, str, FluentIconBase] = None): self.__init__(parent=parent) self.setText(text) self.setIcon(icon) def _postInit(self): pass def setIcon(self, icon: Union[QIcon, str, FluentIconBase]): self.setProperty('hasIcon', icon is not None) self.setStyle(QApplication.style()) self._icon = icon or QIcon() self.update() def icon(self): return toQIcon(self._icon) def setProperty(self, name: str, value) -> bool: if name != 'icon': return super().setProperty(name, value) self.setIcon(value) return True def mousePressEvent(self, e): self.isPressed = True super().mousePressEvent(e) def mouseReleaseEvent(self, e): self.isPressed = False super().mouseReleaseEvent(e) def enterEvent(self, e): self.isHover = True self.update() def leaveEvent(self, e): self.isHover = False self.update() def _drawIcon(self, icon, painter, rect): """ draw icon """ drawIcon(icon, painter, rect) def paintEvent(self, e): super().paintEvent(e) if self.icon().isNull(): return painter = QPainter(self) painter.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) if not self.isEnabled(): painter.setOpacity(0.3628) elif self.isPressed: painter.setOpacity(0.786) w, h = self.iconSize().width(), self.iconSize().height() y = (self.height() - h) / 2 mw = self.minimumSizeHint().width() if mw > 0: self._drawIcon(self._icon, painter, QRectF( 12+(self.width()-mw)//2, y, w, h)) else: self._drawIcon(self._icon, painter, QRectF(12, y, w, h)) class PrimaryPushButton(PushButton): """ Primary color push button """ def _drawIcon(self, icon, painter, rect): if isinstance(icon, FluentIconBase) and self.isEnabled(): # reverse icon color theme = Theme.DARK if not isDarkTheme() else Theme.LIGHT icon = icon.icon(theme) elif not self.isEnabled(): painter.setOpacity(0.786 if isDarkTheme() else 0.9) icon = icon.icon(Theme.DARK) PushButton._drawIcon(self, icon, painter, rect) class ToggleButton(PushButton): def _postInit(self): self.setCheckable(True) self.setChecked(False) def _drawIcon(self, icon, painter, rect): if not self.isChecked(): return PushButton._drawIcon(self, icon, painter, rect) PrimaryPushButton._drawIcon(self, icon, painter, rect) class HyperlinkButton(QPushButton): """ Hyperlink button """ @singledispatchmethod def __init__(self, parent: QWidget = None): super().__init__(parent) self._url = QUrl() FluentStyleSheet.BUTTON.apply(self) self.setCursor(Qt.PointingHandCursor) setFont(self) self.clicked.connect(lambda i: QDesktopServices.openUrl(self.getUrl())) @__init__.register def _(self, url: str, text: str, parent: QWidget = None): self.__init__(parent) self.setText(text) self.url.setUrl(url) def getUrl(self): return self._url def setUrl(self, url: Union[str, QUrl]): self._url = QUrl(url) url = Property(QUrl, getUrl, setUrl) class RadioButton(QRadioButton): """ Radio button """ @singledispatchmethod def __init__(self, parent: QWidget = None): super().__init__(parent) FluentStyleSheet.BUTTON.apply(self) @__init__.register def _(self, text: str, parent: QWidget = None): self.__init__(parent) self.setText(text) class ToolButton(QToolButton): """ Tool button """ @singledispatchmethod def __init__(self, parent: QWidget = None): super().__init__(parent) FluentStyleSheet.BUTTON.apply(self) self.isPressed = False self.isHover = False self.setIconSize(QSize(16, 16)) self.setIcon(QIcon()) setFont(self) self._postInit() @__init__.register def _(self, icon: FluentIconBase, parent: QWidget = None): self.__init__(parent) self.setIcon(icon) @__init__.register def _(self, icon: QIcon, parent: QWidget = None): self.__init__(parent) self.setIcon(icon) @__init__.register def _(self, icon: str, parent: QWidget = None): self.__init__(parent) self.setIcon(icon) def _postInit(self): pass def setIcon(self, icon: Union[QIcon, str, FluentIconBase]): self._icon = icon self.update() def icon(self): return toQIcon(self._icon) def setProperty(self, name: str, value) -> bool: if name != 'icon': return super().setProperty(name, value) self.setIcon(value) return True def mousePressEvent(self, e): self.isPressed = True super().mousePressEvent(e) def mouseReleaseEvent(self, e): self.isPressed = False super().mouseReleaseEvent(e) def enterEvent(self, e): self.isHover = True self.update() def leaveEvent(self, e): self.isHover = False self.update() def _drawIcon(self, icon, painter: QPainter, rect: QRectF): """ draw icon """ drawIcon(icon, painter, rect) def paintEvent(self, e): super().paintEvent(e) if self._icon is None: return painter = QPainter(self) painter.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) if not self.isEnabled(): painter.setOpacity(0.43) elif self.isPressed: painter.setOpacity(0.63) w, h = self.iconSize().width(), self.iconSize().height() y = (self.height() - h) / 2 x = (self.width() - w) / 2 self._drawIcon(self._icon, painter, QRectF(x, y, w, h)) class TransparentToolButton(ToolButton): """ Transparent background tool button """ class PrimaryToolButton(ToolButton): """ Primary color tool button """ def _drawIcon(self, icon, painter: QPainter, rect: QRectF): if isinstance(icon, FluentIconBase) and self.isEnabled(): # reverse icon color theme = Theme.DARK if not isDarkTheme() else Theme.LIGHT icon = icon.icon(theme) elif not self.isEnabled(): painter.setOpacity(0.786 if isDarkTheme() else 0.9) icon = icon.icon(Theme.DARK) return drawIcon(icon, painter, rect) class DropDownButtonBase: """ Drop down button base class """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._menu = None self.arrowAni = TranslateYAnimation(self) def setMenu(self, menu: RoundMenu): self._menu = menu def menu(self) -> RoundMenu: return self._menu def _showMenu(self): if not self.menu(): return menu = self.menu() if menu.view.width() < self.width(): menu.view.setMinimumWidth(self.width()) menu.adjustSize() # show menu x = -menu.width()//2 + menu.layout().contentsMargins().left() + self.width()//2 y = self.height() menu.exec(self.mapToGlobal(QPoint(x, y))) def _hideMenu(self): if self.menu(): self.menu().hide() def _drawDropDownIcon(self, painter, rect): if isDarkTheme(): FIF.ARROW_DOWN.render(painter, rect) else: FIF.ARROW_DOWN.render(painter, rect, fill="#646464") def paintEvent(self, e): painter = QPainter(self) painter.setRenderHints(QPainter.Antialiasing) if self.isHover: painter.setOpacity(0.8) elif self.isPressed: painter.setOpacity(0.7) rect = QRectF(self.width()-22, self.height()/2-5+self.arrowAni.y, 10, 10) self._drawDropDownIcon(painter, rect) class DropDownPushButton(DropDownButtonBase, PushButton): """ Drop down push button """ def mouseReleaseEvent(self, e): PushButton.mouseReleaseEvent(self, e) self._showMenu() def paintEvent(self, e): PushButton.paintEvent(self, e) DropDownButtonBase.paintEvent(self, e) class DropDownToolButton(DropDownButtonBase, ToolButton): """ Drop down tool button """ def mouseReleaseEvent(self, e): ToolButton.mouseReleaseEvent(self, e) self._showMenu() def _drawIcon(self, icon, painter, rect: QRectF): rect.moveLeft(12) return super()._drawIcon(icon, painter, rect) def paintEvent(self, e): ToolButton.paintEvent(self, e) DropDownButtonBase.paintEvent(self, e) class PrimaryDropDownButtonBase(DropDownButtonBase): """ Primary color drop down button base class """ def _drawDropDownIcon(self, painter, rect): theme = Theme.DARK if not isDarkTheme() else Theme.LIGHT FIF.ARROW_DOWN.render(painter, rect, theme) class PrimaryDropDownPushButton(PrimaryDropDownButtonBase, PrimaryPushButton): """ Primary color drop down push button """ def mouseReleaseEvent(self, e): PrimaryPushButton.mouseReleaseEvent(self, e) self._showMenu() def paintEvent(self, e): PrimaryPushButton.paintEvent(self, e) PrimaryDropDownButtonBase.paintEvent(self, e) class PrimaryDropDownToolButton(PrimaryDropDownButtonBase, PrimaryToolButton): """ Primary drop down tool button """ def mouseReleaseEvent(self, e): PrimaryToolButton.mouseReleaseEvent(self, e) self._showMenu() def _drawIcon(self, icon, painter, rect: QRectF): rect.moveLeft(12) return super()._drawIcon(icon, painter, rect) def paintEvent(self, e): PrimaryToolButton.paintEvent(self, e) PrimaryDropDownButtonBase.paintEvent(self, e) class SplitDropButton(ToolButton): def _postInit(self): self.arrowAni = TranslateYAnimation(self) self.setIconSize(QSize(10, 10)) self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) def _drawIcon(self, icon, painter, rect): rect.translate(0, self.arrowAni.y) if self.isPressed: painter.setOpacity(0.5) elif self.isHover: painter.setOpacity(1) else: painter.setOpacity(0.63) super()._drawIcon(FIF.ARROW_DOWN, painter, rect) class PrimarySplitDropButton(PrimaryToolButton): def _postInit(self): self.arrowAni = TranslateYAnimation(self) self.setIconSize(QSize(10, 10)) self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) def _drawIcon(self, icon, painter, rect): rect.translate(0, self.arrowAni.y) if self.isPressed: painter.setOpacity(0.7) elif self.isHover: painter.setOpacity(0.9) else: painter.setOpacity(1) theme = Theme.DARK if not isDarkTheme() else Theme.LIGHT super()._drawIcon(FIF.ARROW_DOWN.icon(theme), painter, rect) class SplitWidgetBase(QWidget): """ Split widget base class """ dropDownClicked = Signal() def __init__(self, parent=None): super().__init__(parent=parent) self.flyout = None # type: QWidget self.dropButton = SplitDropButton(self) self.hBoxLayout = QHBoxLayout(self) self.hBoxLayout.setSpacing(0) self.hBoxLayout.setContentsMargins(0, 0, 0, 0) self.hBoxLayout.addWidget(self.dropButton) self.dropButton.clicked.connect(self.dropDownClicked) self.dropButton.clicked.connect(self.showFlyout) self.setAttribute(Qt.WA_TranslucentBackground) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) def setWidget(self, widget: QWidget): """ set the widget on left side """ self.hBoxLayout.insertWidget(0, widget, 1, Qt.AlignLeft) def setDropButton(self, button): """ set drop dow button """ self.hBoxLayout.removeWidget(self.dropButton) self.dropButton.deleteLater() self.dropButton = button self.dropButton.clicked.connect(self.dropDownClicked) self.dropButton.clicked.connect(self.showFlyout) self.hBoxLayout.addWidget(button) def setFlyout(self, flyout): """ set the widget pops up when drop down button is clicked Parameters ---------- flyout: QWidget the widget pops up when drop down button is clicked. It should contain the `exec` method, whose first parameter type is `QPoint` """ self.flyout = flyout def showFlyout(self): """ show flyout """ if not self.flyout: return w = self.flyout if isinstance(w, RoundMenu) and w.view.width() < self.width(): w.view.setMinimumWidth(self.width()) w.adjustSize() dx = w.layout().contentsMargins().left() if isinstance(w, RoundMenu) else 0 x = -w.width()//2 + dx + self.width()//2 y = self.height() w.exec(self.mapToGlobal(QPoint(x, y))) class SplitPushButton(SplitWidgetBase): """ Split push button """ clicked = Signal() @singledispatchmethod def __init__(self, parent: QWidget = None): super().__init__(parent=parent) self.button = PushButton(self) self.button.setObjectName('splitPushButton') self.button.clicked.connect(self.clicked) self.setWidget(self.button) self._postInit() @__init__.register def _(self, text: str, parent: QWidget = None, icon: Union[QIcon, str, FluentIconBase] = None): self.__init__(parent) self.setText(text) self.setIcon(icon) def _postInit(self): pass def text(self): return self.button.text() def setText(self, text: str): self.button.setText(text) self.adjustSize() def icon(self): return self.button.icon() def setIcon(self, icon: Union[QIcon, FluentIconBase, str]): self.button.setIcon(icon) def setIconSize(self, size: QSize): self.button.setIconSize(size) text_ = Property(str, text, setText) icon_ = Property(QIcon, icon, setIcon) class PrimarySplitPushButton(SplitPushButton): """ Primary split push button """ def _postInit(self): self.setDropButton(PrimarySplitDropButton(self)) self.hBoxLayout.removeWidget(self.button) self.button.deleteLater() self.button = PrimaryPushButton(self) self.button.setObjectName('primarySplitPushButton') self.button.clicked.connect(self.clicked) self.setWidget(self.button) class SplitToolButton(SplitWidgetBase): """ Split tool button """ clicked = Signal() @singledispatchmethod def __init__(self, parent: QWidget = None): super().__init__(parent=parent) self.button = ToolButton(self) self.button.setObjectName('splitToolButton') self.button.clicked.connect(self.clicked) self.setWidget(self.button) self._postInit() @__init__.register def _(self, icon: FluentIconBase, parent: QWidget = None): self.__init__(parent) self.setIcon(icon) @__init__.register def _(self, icon: QIcon, parent: QWidget = None): self.__init__(parent) self.setIcon(icon) @__init__.register def _(self, icon: str, parent: QWidget = None): self.__init__(parent) self.setIcon(icon) def _postInit(self): pass def icon(self): return self.button.icon() def setIcon(self, icon: Union[QIcon, FluentIconBase, str]): self.button.setIcon(icon) def setIconSize(self, size: QSize): self.button.setIconSize(size) icon_ = Property(QIcon, icon, setIcon) class PrimarySplitToolButton(SplitToolButton): """ Primary split push button """ def _postInit(self): self.setDropButton(PrimarySplitDropButton(self)) self.hBoxLayout.removeWidget(self.button) self.button.deleteLater() self.button = PrimaryToolButton(self) self.button.setObjectName('primarySplitToolButton') self.button.clicked.connect(self.clicked) self.setWidget(self.button)