A fluent design widgets library based on C++ Qt/PyQt/PySide. Make Qt Great Again.
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.

631 lines
18 KiB

# coding:utf-8
from typing import Iterable, List, Tuple, Union
from PyQt5.QtCore import Qt, pyqtSignal, QSize, QRectF, QRect, QPoint, QEvent
from PyQt5.QtGui import QPixmap, QPainter, QColor, QFont, QHoverEvent, QPainterPath
from PyQt5.QtWidgets import QAction, QLayoutItem, QWidget, QFrame, QHBoxLayout, QApplication
from ...common.font import setFont
from ...common.icon import FluentIcon, Icon, Action
from ...common.style_sheet import isDarkTheme
from .menu import RoundMenu, MenuAnimationType
from .button import TransparentToggleToolButton
from .tool_tip import ToolTipFilter
from .flyout import FlyoutViewBase, Flyout
class CommandButton(TransparentToggleToolButton):
""" Command button
Constructors
------------
* CommandButton(`parent`: QWidget = None)
* CommandButton(`icon`: QIcon | str | FluentIconBase = None, `parent`: QWidget = None)
"""
def _postInit(self):
super()._postInit()
self.setCheckable(False)
self.setToolButtonStyle(Qt.ToolButtonIconOnly)
setFont(self, 12)
self._text = ''
self._action = None
self._isTight = False
def setTight(self, isTight: bool):
self._isTight = isTight
self.update()
def isTight(self):
return self._isTight
def sizeHint(self) -> QSize:
if self.isIconOnly():
return QSize(36, 34) if self.isTight() else QSize(48, 34)
# get the width of text
tw = self.fontMetrics().width(self.text())
style = self.toolButtonStyle()
if style == Qt.ToolButtonTextBesideIcon:
return QSize(tw + 47, 34)
if style == Qt.ToolButtonTextOnly:
return QSize(tw + 32, 34)
return QSize(tw + 32, 50)
def isIconOnly(self):
if not self.text():
return True
return self.toolButtonStyle() in [Qt.ToolButtonIconOnly, Qt.ToolButtonFollowStyle]
def _drawIcon(self, icon, painter, rect):
pass
def text(self):
return self._text
def setText(self, text: str):
self._text = text
self.update()
def setAction(self, action: QAction):
self._action = action
self._onActionChanged()
self.clicked.connect(action.trigger)
action.toggled.connect(self.setChecked)
action.changed.connect(self._onActionChanged)
self.installEventFilter(CommandToolTipFilter(self, 700))
def _onActionChanged(self):
action = self.action()
self.setIcon(action.icon())
self.setText(action.text())
self.setToolTip(action.toolTip())
self.setEnabled(action.isEnabled())
self.setCheckable(action.isCheckable())
self.setChecked(action.isChecked())
def action(self):
return self._action
def paintEvent(self, e):
super().paintEvent(e)
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing |
QPainter.SmoothPixmapTransform)
if not self.isChecked():
painter.setPen(Qt.white if isDarkTheme() else Qt.black)
else:
painter.setPen(Qt.black if isDarkTheme() else Qt.white)
if not self.isEnabled():
painter.setOpacity(0.43)
elif self.isPressed:
painter.setOpacity(0.63)
# draw icon and text
style = self.toolButtonStyle()
iw, ih = self.iconSize().width(), self.iconSize().height()
if self.isIconOnly():
y = (self.height() - ih) / 2
x = (self.width() - iw) / 2
super()._drawIcon(self._icon, painter, QRectF(x, y, iw, ih))
elif style == Qt.ToolButtonTextOnly:
painter.drawText(self.rect(), Qt.AlignCenter, self.text())
elif style == Qt.ToolButtonTextBesideIcon:
y = (self.height() - ih) / 2
super()._drawIcon(self._icon, painter, QRectF(11, y, iw, ih))
rect = QRectF(26, 0, self.width() - 26, self.height())
painter.drawText(rect, Qt.AlignCenter, self.text())
elif style == Qt.ToolButtonTextUnderIcon:
x = (self.width() - iw) / 2
super()._drawIcon(self._icon, painter, QRectF(x, 9, iw, ih))
rect = QRectF(0, ih + 13, self.width(), self.height() - ih - 13)
painter.drawText(rect, Qt.AlignHCenter | Qt.AlignTop, self.text())
class CommandToolTipFilter(ToolTipFilter):
""" Command tool tip filter """
def _canShowToolTip(self) -> bool:
return super()._canShowToolTip() and self.parent().isIconOnly()
class MoreActionsButton(CommandButton):
""" More action button """
def _postInit(self):
super()._postInit()
self.setIcon(FluentIcon.MORE)
def sizeHint(self):
return QSize(40, 34)
def clearState(self):
self.setAttribute(Qt.WA_UnderMouse, False)
e = QHoverEvent(QEvent.HoverLeave, QPoint(-1, -1), QPoint())
QApplication.sendEvent(self, e)
class CommandSeparator(QWidget):
""" Command separator """
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedSize(9, 34)
def paintEvent(self, e):
painter = QPainter(self)
painter.setPen(QColor(255, 255, 255, 21)
if isDarkTheme() else QColor(0, 0, 0, 15))
painter.drawLine(5, 2, 5, self.height() - 2)
class CommandMenu(RoundMenu):
""" Command menu """
def __init__(self, parent=None):
super().__init__("", parent)
self.setItemHeight(32)
self.view.setIconSize(QSize(16, 16))
class CommandBar(QFrame):
""" Command bar """
def __init__(self, parent=None):
super().__init__(parent=parent)
self._widgets = [] # type: List[QWidget]
self._hiddenWidgets = [] # type: List[QWidget]
self._hiddenActions = [] # type: List[QAction]
self._menuAnimation = MenuAnimationType.DROP_DOWN
self._toolButtonStyle = Qt.ToolButtonIconOnly
self._iconSize = QSize(16, 16)
self._isButtonTight = False
self._spacing = 4
self.moreButton = MoreActionsButton(self)
self.moreButton.clicked.connect(self._showMoreActionsMenu)
self.moreButton.hide()
setFont(self, 12)
self.setAttribute(Qt.WA_TranslucentBackground)
def setSpaing(self, spacing: int):
if spacing == self._spacing:
return
self._spacing = spacing
self.updateGeometry()
def spacing(self):
return self._spacing
def addAction(self, action: QAction):
""" add action
Parameters
----------
action: QAction
the action to add
"""
if action in self.actions():
return
button = self._createButton(action)
self._insertWidgetToLayout(-1, button)
super().addAction(action)
return button
def addActions(self, actions: Iterable[QAction]):
for action in actions:
self.addAction(action)
def addHiddenAction(self, action: QAction):
""" add hidden action """
if action in self.actions():
return
self._hiddenActions.append(action)
self.updateGeometry()
super().addAction(action)
def addHiddenActions(self, actions: List[QAction]):
""" add hidden action """
for action in actions:
self.addHiddenAction(action)
def insertAction(self, before: QAction, action: QAction):
if before not in self.actions():
return
index = self.actions().index(before)
button = self._createButton(action)
self._insertWidgetToLayout(index, button)
super().insertAction(before, action)
return button
def addSeparator(self):
self.insertSeparator(-1)
def insertSeparator(self, index: int):
self._insertWidgetToLayout(index, CommandSeparator(self))
def addWidget(self, widget: QWidget):
""" add widget to command bar """
self._insertWidgetToLayout(-1, widget)
def removeAction(self, action: QAction):
if action not in self.actions():
return
for w in self.commandButtons:
if w.action() is action:
self._widgets.remove(w)
w.hide()
w.deleteLater()
break
self.updateGeometry()
def removeWidget(self, widget: QWidget):
if widget not in self._widgets:
return
self._widgets.remove(widget)
self.updateGeometry()
def removeHiddenAction(self, action: QAction):
if action in self._hiddenActions:
self._hiddenActions.remove(action)
def setToolButtonStyle(self, style: Qt.ToolButtonStyle):
""" set the style of tool button """
if self.toolButtonStyle() == style:
return
self._toolButtonStyle = style
for w in self.commandButtons:
w.setToolButtonStyle(style)
def toolButtonStyle(self):
return self._toolButtonStyle
def setButtonTight(self, isTight: bool):
if self.isButtonTight() == isTight:
return
self._isButtonTight = isTight
for w in self.commandButtons:
w.setTight(isTight)
self.updateGeometry()
def isButtonTight(self):
return self._isButtonTight
def setIconSize(self, size: QSize):
if size == self._iconSize:
return
self._iconSize = size
for w in self.commandButtons:
w.setIconSize(size)
def iconSize(self):
return self._iconSize
def resizeEvent(self, e):
self.updateGeometry()
def _createButton(self, action: QAction):
""" create command button """
button = CommandButton(self)
button.setAction(action)
button.setToolButtonStyle(self.toolButtonStyle())
button.setTight(self.isButtonTight())
button.setIconSize(self.iconSize())
button.setFont(self.font())
return button
def _insertWidgetToLayout(self, index: int, widget: QWidget):
""" add widget to layout """
widget.setParent(self)
widget.show()
if index < 0:
self._widgets.append(widget)
else:
self._widgets.insert(index, widget)
self.setFixedHeight(max(w.height() for w in self._widgets))
self.updateGeometry()
def minimumSizeHint(self) -> QSize:
return self.moreButton.size()
def updateGeometry(self):
self._hiddenWidgets.clear()
self.moreButton.hide()
visibles = self._visibleWidgets()
x = self.contentsMargins().left()
h = self.height()
for widget in visibles:
widget.show()
widget.move(x, (h - widget.height()) // 2)
x += (widget.width() + self.spacing())
# show more actions button
if self._hiddenActions or len(visibles) < len(self._widgets):
self.moreButton.show()
self.moreButton.move(x, (h - self.moreButton.height()) // 2)
for widget in self._widgets[len(visibles):]:
widget.hide()
self._hiddenWidgets.append(widget)
def _visibleWidgets(self) -> List[QWidget]:
""" return the visible widgets in layout """
# have enough spacing to show all widgets
if self.suitableWidth() <= self.width():
return self._widgets
w = self.moreButton.width()
for index, widget in enumerate(self._widgets):
w += widget.width()
if index > 0:
w += self.spacing()
if w > self.width():
break
return self._widgets[:index]
def suitableWidth(self):
widths = [w.width() for w in self._widgets]
if self._hiddenActions:
widths.append(self.moreButton.width())
return sum(widths) + self.spacing() * max(len(widths) - 1, 0)
def resizeToSuitableWidth(self):
self.setFixedWidth(self.suitableWidth())
def setFont(self, font: QFont):
super().setFont(font)
for button in self.commandButtons:
button.setFont(font)
@property
def commandButtons(self):
return [w for w in self._widgets if isinstance(w, CommandButton)]
def setMenuDropDown(self, down: bool):
""" set the animation direction of more actions menu """
if down:
self._menuAnimation = MenuAnimationType.DROP_DOWN
else:
self._menuAnimation = MenuAnimationType.PULL_UP
def isMenuDropDown(self):
return self._menuAnimation == MenuAnimationType.DROP_DOWN
def _showMoreActionsMenu(self):
""" show more actions menu """
self.moreButton.clearState()
actions = self._hiddenActions.copy()
for w in reversed(self._hiddenWidgets):
if isinstance(w, CommandButton):
actions.insert(0, w.action())
menu = CommandMenu(self)
menu.addActions(actions)
x = -menu.width() + menu.layout().contentsMargins().right() + \
self.moreButton.width() + 18
if self._menuAnimation == MenuAnimationType.DROP_DOWN:
y = self.moreButton.height()
else:
y = -5
pos = self.moreButton.mapToGlobal(QPoint(x, y))
menu.exec(pos, aniType=self._menuAnimation)
class CommandViewMenu(CommandMenu):
""" Command view menu """
def __init__(self, parent=None):
super().__init__(parent)
self.view.setObjectName('commandListWidget')
def setDropDown(self, down: bool, long=False):
self.view.setProperty('dropDown', down)
self.view.setProperty('long', long)
self.view.setStyle(QApplication.style())
self.view.update()
class CommandViewBar(CommandBar):
""" Command view bar """
def __init__(self, parent=None):
super().__init__(parent)
self.setMenuDropDown(True)
def setMenuDropDown(self, down: bool):
""" set the animation direction of more actions menu """
if down:
self._menuAnimation = MenuAnimationType.FADE_IN_DROP_DOWN
else:
self._menuAnimation = MenuAnimationType.FADE_IN_PULL_UP
def isMenuDropDown(self):
return self._menuAnimation == MenuAnimationType.FADE_IN_DROP_DOWN
def _showMoreActionsMenu(self):
self.moreButton.clearState()
actions = self._hiddenActions.copy()
for w in reversed(self._hiddenWidgets):
if isinstance(w, CommandButton):
actions.insert(0, w.action())
menu = CommandViewMenu(self)
menu.addActions(actions)
# adjust the shape of view
view = self.parent() # type: CommandBarView
view.setMenuVisible(True)
# adjust the shape of menu
menu.closedSignal.connect(lambda: view.setMenuVisible(False))
menu.setDropDown(self.isMenuDropDown(), menu.view.width() > view.width()+5)
# adjust menu size
if menu.view.width() < view.width():
menu.view.setFixedWidth(view.width())
menu.adjustSize()
x = -menu.width() + menu.layout().contentsMargins().right() + \
self.moreButton.width() + 18
if self.isMenuDropDown():
y = self.moreButton.height()
else:
y = -13
menu.setShadowEffect(0, (0, 0), QColor(0, 0, 0, 0))
menu.layout().setContentsMargins(12, 20, 12, 8)
pos = self.moreButton.mapToGlobal(QPoint(x, y))
menu.exec(pos, aniType=self._menuAnimation)
class CommandBarView(FlyoutViewBase):
""" Command bar view """
def __init__(self, parent=None):
super().__init__(parent=parent)
self.bar = CommandViewBar(self)
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.setContentsMargins(6, 6, 6, 6)
self.hBoxLayout.addWidget(self.bar)
self.hBoxLayout.setSizeConstraint(QHBoxLayout.SetMinAndMaxSize)
self.setButtonTight(True)
self.setIconSize(QSize(14, 14))
self._isMenuVisible = False
def setMenuVisible(self, isVisible):
self._isMenuVisible = isVisible
self.update()
def addWidget(self, widget: QWidget):
self.bar.addWidget(widget)
def setSpaing(self, spacing: int):
self.bar.setSpaing(spacing)
def spacing(self):
return self.bar.spacing()
def addAction(self, action: QAction):
return self.bar.addAction(action)
def addActions(self, actions: Iterable[QAction]):
self.bar.addActions(actions)
def addHiddenAction(self, action: QAction):
self.bar.addHiddenAction(action)
def addHiddenActions(self, actions: List[QAction]):
self.bar.addHiddenActions(actions)
def insertAction(self, before: QAction, action: QAction):
return self.bar.insertAction(before, action)
def addSeparator(self):
self.bar.addSeparator()
def insertSeparator(self, index: int):
self.bar.insertSeparator(index)
def removeAction(self, action: QAction):
self.bar.removeAction(action)
def removeWidget(self, widget: QWidget):
self.bar.removeWidget(widget)
def removeHiddenAction(self, action: QAction):
self.bar.removeAction(action)
def setToolButtonStyle(self, style: Qt.ToolButtonStyle):
self.bar.setToolButtonStyle(style)
def toolButtonStyle(self):
return self.bar.toolButtonStyle()
def setButtonTight(self, isTight: bool):
self.bar.setButtonTight(isTight)
def isButtonTight(self):
return self.bar.isButtonTight()
def setIconSize(self, size: QSize):
self.bar.setIconSize(size)
def iconSize(self):
return self.bar.iconSize()
def setFont(self, font: QFont):
self.bar.setFont(font)
def setMenuDropDown(self, down: bool):
self.bar.setMenuDropDown(down)
def suitableWidth(self):
m = self.contentsMargins()
return m.left() + m.right() + self.bar.suitableWidth()
def resizeToSuitableWidth(self):
self.bar.resizeToSuitableWidth()
self.setFixedWidth(self.suitableWidth())
def actions(self):
return self.bar.actions()
def paintEvent(self, e):
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing)
path = QPainterPath()
path.setFillRule(Qt.WindingFill)
path.addRoundedRect(QRectF(self.rect().adjusted(1, 1, -1, -1)), 8, 8)
if self._isMenuVisible:
y = self.height() - 10 if self.bar.isMenuDropDown() else 1
path.addRect(1, y, self.width() - 2, 9)
painter.setBrush(
QColor(40, 40, 40) if isDarkTheme() else QColor(248, 248, 248))
painter.setPen(
QColor(56, 56, 56) if isDarkTheme() else QColor(233, 233, 233))
painter.drawPath(path.simplified())