initial fluent-widgets ui

This commit is contained in:
2025-08-14 18:45:16 +08:00
parent 746e83ab23
commit 4c66886257
1198 changed files with 805339 additions and 0 deletions

View File

@ -0,0 +1,9 @@
from .navigation_widget import (NavigationWidget, NavigationPushButton, NavigationSeparator, NavigationToolButton,
NavigationTreeWidget, NavigationTreeWidgetBase, NavigationAvatarWidget)
from .navigation_panel import NavigationPanel, NavigationItemPosition, NavigationDisplayMode
from .navigation_interface import NavigationInterface
from .navigation_bar import NavigationBarPushButton, NavigationBar
from .pivot import Pivot, PivotItem
from .segmented_widget import (SegmentedItem, SegmentedWidget, SegmentedToolItem, SegmentedToolWidget,
SegmentedToggleToolItem, SegmentedToggleToolWidget)
from .breadcrumb import BreadcrumbBar, BreadcrumbItem

View File

@ -0,0 +1,350 @@
# coding:utf-8
import math
from typing import Dict, List
from PySide6.QtCore import Qt, Signal, QRectF, Property, QPoint, QEvent
from PySide6.QtGui import QPainter, QFont, QHoverEvent, QAction
from PySide6.QtWidgets import QWidget, QApplication
from ...common.font import setFont
from ...common.icon import FluentIcon
from ...common.style_sheet import isDarkTheme
from ...components.widgets.menu import RoundMenu, MenuAnimationType
class BreadcrumbWidget(QWidget):
""" Bread crumb widget """
clicked = Signal()
def __init__(self, parent=None):
super().__init__(parent=parent)
self.isHover = False
self.isPressed = False
def mousePressEvent(self, e):
self.isPressed = True
self.update()
def mouseReleaseEvent(self, e):
self.isPressed = False
self.update()
self.clicked.emit()
def enterEvent(self, e):
self.isHover = True
self.update()
def leaveEvent(self, e):
self.isHover = False
self.update()
class ElideButton(BreadcrumbWidget):
""" Elide button """
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedSize(16, 16)
def paintEvent(self, e):
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing)
painter.setPen(Qt.NoPen)
if self.isPressed:
painter.setOpacity(0.5)
elif not self.isHover:
painter.setOpacity(0.61)
FluentIcon.MORE.render(painter, self.rect())
def clearState(self):
self.setAttribute(Qt.WA_UnderMouse, False)
self.isHover = False
e = QHoverEvent(QEvent.HoverLeave, QPoint(-1, -1), QPoint())
QApplication.sendEvent(self, e)
class BreadcrumbItem(BreadcrumbWidget):
""" Breadcrumb item """
def __init__(self, routeKey: str, text: str, index: int, parent=None):
super().__init__(parent=parent)
self.text = text
self.routeKey = routeKey
self.isHover = False
self.isPressed = False
self.isSelected = False
self.index = index
self.spacing = 5
def setText(self, text: str):
self.text = text
rect = self.fontMetrics().boundingRect(text)
w = rect.width() + math.ceil(self.font().pixelSize() / 10)
if not self.isRoot():
w += self.spacing * 2
self.setFixedWidth(w)
self.setFixedHeight(rect.height())
self.update()
def isRoot(self):
return self.index == 0
def setSelected(self, isSelected: bool):
self.isSelected = isSelected
self.update()
def setFont(self, font: QFont):
super().setFont(font)
self.setText(self.text)
def setSpacing(self, spacing: int):
self.spacing = spacing
self.setText(self.text)
def paintEvent(self, e):
painter = QPainter(self)
painter.setRenderHints(QPainter.TextAntialiasing | QPainter.Antialiasing)
painter.setPen(Qt.NoPen)
# draw seperator
sw = self.spacing * 2
if not self.isRoot():
iw = self.font().pixelSize() / 14 * 8
rect = QRectF((sw - iw) / 2, (self.height() - iw) / 2 + 1, iw, iw)
painter.setOpacity(0.61)
FluentIcon.CHEVRON_RIGHT_MED.render(painter, rect)
# draw text
if self.isPressed:
alpha = 0.54 if isDarkTheme() else 0.45
painter.setOpacity(1 if self.isSelected else alpha)
elif self.isSelected or self.isHover:
painter.setOpacity(1)
else:
painter.setOpacity(0.79 if isDarkTheme() else 0.61)
painter.setFont(self.font())
painter.setPen(Qt.white if isDarkTheme() else Qt.black)
if self.isRoot():
rect = self.rect()
else:
rect = QRectF(sw, 0, self.width() - sw, self.height())
painter.drawText(rect, Qt.AlignVCenter | Qt.AlignLeft, self.text)
class BreadcrumbBar(QWidget):
""" Breadcrumb bar """
currentItemChanged = Signal(str)
currentIndexChanged = Signal(int)
def __init__(self, parent=None):
super().__init__(parent=parent)
self.itemMap = {} # type: Dict[BreadcrumbItem]
self.items = [] # type: List[BreadcrumbItem]
self.hiddenItems = [] # type: List[BreadcrumbItem]
self._spacing = 10
self._currentIndex = -1
self.elideButton = ElideButton(self)
setFont(self, 14)
self.setAttribute(Qt.WA_TranslucentBackground)
self.elideButton.hide()
self.elideButton.clicked.connect(self._showHiddenItemsMenu)
def addItem(self, routeKey: str, text: str):
""" add item
Parameters
----------
routeKey: str
unique key of item
text: str
the text of item
"""
if routeKey in self.itemMap:
return
item = BreadcrumbItem(routeKey, text, len(self.items), self)
item.setFont(self.font())
item.setSpacing(self.spacing)
item.clicked.connect(lambda: self.setCurrentItem(routeKey))
self.itemMap[routeKey] = item
self.items.append(item)
self.setFixedHeight(max(i.height() for i in self.items))
self.setCurrentItem(routeKey)
self.updateGeometry()
def setCurrentIndex(self, index: int):
if not 0 <= index < len(self.items) or index == self.currentIndex():
return
if 0<= self.currentIndex() < len(self.items):
self.currentItem().setSelected(False)
self._currentIndex = index
self.currentItem().setSelected(True)
# remove trailing items
for item in self.items[-1:index:-1]:
item = self.items.pop()
self.itemMap.pop(item.routeKey)
item.deleteLater()
self.updateGeometry()
self.currentIndexChanged.emit(index)
self.currentItemChanged.emit(self.currentItem().routeKey)
def setCurrentItem(self, routeKey: str):
if routeKey not in self.itemMap:
return
self.setCurrentIndex(self.items.index(self.itemMap[routeKey]))
def setItemText(self, routeKey: str, text: str):
item = self.item(routeKey)
if item:
item.setText(text)
def item(self, routeKey: str) -> BreadcrumbItem:
return self.itemMap.get(routeKey, None)
def itemAt(self, index: int):
if 0 <= index < len(self.items):
return self.items[index]
return None
def currentIndex(self):
return self._currentIndex
def currentItem(self) -> BreadcrumbItem:
if self.currentIndex() >= 0:
return self.items[self.currentIndex()]
return None
def resizeEvent(self, e):
self.updateGeometry()
def clear(self):
""" clear all items """
while self.items:
item = self.items.pop()
self.itemMap.pop(item.routeKey)
item.deleteLater()
self.elideButton.hide()
self._currentIndex = -1
def popItem(self):
""" pop trailing item """
if not self.items:
return
if self.count() >= 2:
self.setCurrentIndex(self.currentIndex() - 1)
else:
self.clear()
def count(self):
""" Returns the number of items """
return len(self.items)
def updateGeometry(self):
if not self.items:
return
x = 0
self.elideButton.hide()
self.hiddenItems = self.items[:-1].copy()
if not self.isElideVisible():
visibleItems = self.items
self.hiddenItems.clear()
else:
visibleItems = [self.elideButton, self.items[-1]]
w = sum(i.width() for i in visibleItems)
for item in self.items[-2::-1]:
w += item.width()
if w > self.width():
break
visibleItems.insert(1, item)
self.hiddenItems.remove(item)
for item in self.hiddenItems:
item.hide()
for item in visibleItems:
item.move(x, (self.height() - item.height()) // 2)
item.show()
x += item.width()
def isElideVisible(self):
w = sum(i.width() for i in self.items)
return w > self.width()
def setFont(self, font: QFont):
super().setFont(font)
s = int(font.pixelSize() / 14 * 16)
self.elideButton.setFixedSize(s, s)
for item in self.items:
item.setFont(font)
def _showHiddenItemsMenu(self):
self.elideButton.clearState()
menu = RoundMenu(parent=self)
menu.setItemHeight(32)
for item in self.hiddenItems:
menu.addAction(
QAction(item.text, menu, triggered=lambda checked=True, i=item: self.setCurrentItem(i.routeKey)))
# determine the animation type by choosing the maximum height of view
x = -menu.layout().contentsMargins().left()
pd = self.mapToGlobal(QPoint(x, self.height()))
hd = menu.view.heightForAnimation(pd, MenuAnimationType.DROP_DOWN)
pu = self.mapToGlobal(QPoint(x, 0))
hu = menu.view.heightForAnimation(pu, MenuAnimationType.PULL_UP)
if hd >= hu:
menu.view.adjustSize(pd, MenuAnimationType.DROP_DOWN)
menu.exec(pd, aniType=MenuAnimationType.DROP_DOWN)
else:
menu.view.adjustSize(pu, MenuAnimationType.PULL_UP)
menu.exec(pu, aniType=MenuAnimationType.PULL_UP)
def getSpacing(self):
return self._spacing
def setSpacing(self, spacing: int):
if spacing == self._spacing:
return
self._spacing = spacing
for item in self.items:
item.setSpacing(spacing)
spacing = Property(int, getSpacing, setSpacing)

View File

@ -0,0 +1,416 @@
# coding:utf-8
from typing import Dict, Union
from PySide6.QtCore import Qt, QRect, QPropertyAnimation, QEasingCurve, Property, QRectF
from PySide6.QtGui import QFont, QPainter, QColor, QIcon
from PySide6.QtWidgets import QWidget, QVBoxLayout
from ...common.config import isDarkTheme
from ...common.font import setFont
from ...common.style_sheet import themeColor
from ...common.color import autoFallbackThemeColor
from ...common.icon import drawIcon, FluentIconBase, toQIcon
from ...common.icon import FluentIcon as FIF
from ...common.router import qrouter
from ...common.style_sheet import FluentStyleSheet
from ..widgets.scroll_area import ScrollArea
from .navigation_widget import NavigationPushButton, NavigationWidget
from .navigation_panel import RouteKeyError, NavigationItemPosition
class IconSlideAnimation(QPropertyAnimation):
""" Icon sliding animation """
def __init__(self, parent=None):
super().__init__(parent)
self._offset = 0
self.maxOffset = 6
self.setTargetObject(self)
self.setPropertyName(b"offset")
def getOffset(self):
return self._offset
def setOffset(self, value: float):
self._offset = value
self.parent().update()
def slideDown(self):
""" slide down """
self.setEndValue(self.maxOffset)
self.setDuration(100)
self.start()
def slideUp(self):
""" slide up """
self.setEndValue(0)
self.setDuration(100)
self.start()
offset = Property(float, getOffset, setOffset)
class NavigationBarPushButton(NavigationPushButton):
""" Navigation bar push button """
def __init__(self, icon: Union[str, QIcon, FIF], text: str, isSelectable: bool, selectedIcon=None, parent=None):
super().__init__(icon, text, isSelectable, parent)
self.iconAni = IconSlideAnimation(self)
self._selectedIcon = selectedIcon
self._isSelectedTextVisible = True
self.lightSelectedColor = QColor()
self.darkSelectedColor = QColor()
self.setFixedSize(64, 58)
setFont(self, 11)
def setSelectedColor(self, light, dark):
self.lightSelectedColor = QColor(light)
self.darkSelectedColor = QColor(dark)
self.update()
def selectedIcon(self):
if self._selectedIcon:
return toQIcon(self._selectedIcon)
return QIcon()
def setSelectedIcon(self, icon: Union[str, QIcon, FIF]):
self._selectedIcon = icon
self.update()
def setSelectedTextVisible(self, isVisible):
self._isSelectedTextVisible = isVisible
self.update()
def paintEvent(self, e):
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing |
QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform)
painter.setPen(Qt.NoPen)
self._drawBackground(painter)
self._drawIcon(painter)
self._drawText(painter)
def _drawBackground(self, painter: QPainter):
if self.isSelected:
painter.setBrush(QColor(255, 255, 255, 42) if isDarkTheme() else Qt.white)
painter.drawRoundedRect(self.rect(), 5, 5)
# draw indicator
painter.setBrush(autoFallbackThemeColor(self.lightSelectedColor, self.darkSelectedColor))
if not self.isPressed:
painter.drawRoundedRect(0, 16, 4, 24, 2, 2)
else:
painter.drawRoundedRect(0, 19, 4, 18, 2, 2)
elif self.isPressed or self.isEnter:
c = 255 if isDarkTheme() else 0
alpha = 9 if self.isEnter else 6
painter.setBrush(QColor(c, c, c, alpha))
painter.drawRoundedRect(self.rect(), 5, 5)
def _drawIcon(self, painter: QPainter):
if (self.isPressed or not self.isEnter) and not self.isSelected:
painter.setOpacity(0.6)
if not self.isEnabled():
painter.setOpacity(0.4)
if self._isSelectedTextVisible:
rect = QRectF(22, 13, 20, 20)
else:
rect = QRectF(22, 13 + self.iconAni.offset, 20, 20)
selectedIcon = self._selectedIcon or self._icon
if isinstance(selectedIcon, FluentIconBase) and self.isSelected:
color = autoFallbackThemeColor(self.lightSelectedColor, self.darkSelectedColor)
selectedIcon.render(painter, rect, fill=color.name())
elif self.isSelected:
drawIcon(selectedIcon, painter, rect)
else:
drawIcon(self._icon, painter, rect)
def _drawText(self, painter: QPainter):
if self.isSelected and not self._isSelectedTextVisible:
return
if self.isSelected:
painter.setPen(autoFallbackThemeColor(self.lightSelectedColor, self.darkSelectedColor))
else:
painter.setPen(Qt.white if isDarkTheme() else Qt.black)
painter.setFont(self.font())
rect = QRect(0, 32, self.width(), 26)
painter.drawText(rect, Qt.AlignCenter, self.text())
def setSelected(self, isSelected: bool):
if isSelected == self.isSelected:
return
self.isSelected = isSelected
if isSelected:
self.iconAni.slideDown()
else:
self.iconAni.slideUp()
class NavigationBar(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.lightSelectedColor = QColor()
self.darkSelectedColor = QColor()
self.scrollArea = ScrollArea(self)
self.scrollWidget = QWidget()
self.vBoxLayout = QVBoxLayout(self)
self.topLayout = QVBoxLayout()
self.bottomLayout = QVBoxLayout()
self.scrollLayout = QVBoxLayout(self.scrollWidget)
self.items = {} # type: Dict[str, NavigationWidget]
self.history = qrouter
self.__initWidget()
def __initWidget(self):
self.resize(48, self.height())
self.setAttribute(Qt.WA_StyledBackground)
self.window().installEventFilter(self)
self.scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.scrollArea.horizontalScrollBar().setEnabled(False)
self.scrollArea.setWidget(self.scrollWidget)
self.scrollArea.setWidgetResizable(True)
self.scrollWidget.setObjectName('scrollWidget')
FluentStyleSheet.NAVIGATION_INTERFACE.apply(self)
FluentStyleSheet.NAVIGATION_INTERFACE.apply(self.scrollWidget)
self.__initLayout()
def __initLayout(self):
self.vBoxLayout.setContentsMargins(0, 5, 0, 5)
self.topLayout.setContentsMargins(4, 0, 4, 0)
self.bottomLayout.setContentsMargins(4, 0, 4, 0)
self.scrollLayout.setContentsMargins(4, 0, 4, 0)
self.vBoxLayout.setSpacing(4)
self.topLayout.setSpacing(4)
self.bottomLayout.setSpacing(4)
self.scrollLayout.setSpacing(4)
self.vBoxLayout.addLayout(self.topLayout, 0)
self.vBoxLayout.addWidget(self.scrollArea)
self.vBoxLayout.addLayout(self.bottomLayout, 0)
self.vBoxLayout.setAlignment(Qt.AlignTop)
self.topLayout.setAlignment(Qt.AlignTop)
self.scrollLayout.setAlignment(Qt.AlignTop)
self.bottomLayout.setAlignment(Qt.AlignBottom)
def widget(self, routeKey: str):
if routeKey not in self.items:
raise RouteKeyError(f"`{routeKey}` is illegal.")
return self.items[routeKey]
def addItem(self, routeKey: str, icon: Union[str, QIcon, FluentIconBase], text: str, onClick=None,
selectable=True, selectedIcon=None, position=NavigationItemPosition.TOP):
""" add navigation item
Parameters
----------
routeKey: str
the unique name of item
icon: str | QIcon | FluentIconBase
the icon of navigation item
text: str
the text of navigation item
onClick: callable
the slot connected to item clicked signal
selectable: bool
whether the item is selectable
selectedIcon: str | QIcon | FluentIconBase
the icon of navigation item in selected state
position: NavigationItemPosition
where the button is added
"""
return self.insertItem(-1, routeKey, icon, text, onClick, selectable, selectedIcon, position)
def addWidget(self, routeKey: str, widget: NavigationWidget, onClick=None, position=NavigationItemPosition.TOP):
""" add custom widget
Parameters
----------
routeKey: str
the unique name of item
widget: NavigationWidget
the custom widget to be added
onClick: callable
the slot connected to item clicked signal
position: NavigationItemPosition
where the button is added
"""
self.insertWidget(-1, routeKey, widget, onClick, position)
def insertItem(self, index: int, routeKey: str, icon: Union[str, QIcon, FluentIconBase], text: str, onClick=None,
selectable=True, selectedIcon=None, position=NavigationItemPosition.TOP):
""" insert navigation tree item
Parameters
----------
index: int
the insert position of parent widget
routeKey: str
the unique name of item
icon: str | QIcon | FluentIconBase
the icon of navigation item
text: str
the text of navigation item
onClick: callable
the slot connected to item clicked signal
selectable: bool
whether the item is selectable
selectedIcon: str | QIcon | FluentIconBase
the icon of navigation item in selected state
position: NavigationItemPosition
where the button is added
"""
if routeKey in self.items:
return
w = NavigationBarPushButton(icon, text, selectable, selectedIcon, self)
w.setSelectedColor(self.lightSelectedColor, self.darkSelectedColor)
self.insertWidget(index, routeKey, w, onClick, position)
return w
def insertWidget(self, index: int, routeKey: str, widget: NavigationWidget, onClick=None,
position=NavigationItemPosition.TOP):
""" insert custom widget
Parameters
----------
index: int
insert position
routeKey: str
the unique name of item
widget: NavigationWidget
the custom widget to be added
onClick: callable
the slot connected to item clicked signal
position: NavigationItemPosition
where the button is added
"""
if routeKey in self.items:
return
self._registerWidget(routeKey, widget, onClick)
self._insertWidgetToLayout(index, widget, position)
def _registerWidget(self, routeKey: str, widget: NavigationWidget, onClick):
""" register widget """
widget.clicked.connect(self._onWidgetClicked)
if onClick is not None:
widget.clicked.connect(onClick)
widget.setProperty('routeKey', routeKey)
self.items[routeKey] = widget
def _insertWidgetToLayout(self, index: int, widget: NavigationWidget, position: NavigationItemPosition):
""" insert widget to layout """
if position == NavigationItemPosition.TOP:
widget.setParent(self)
self.topLayout.insertWidget(
index, widget, 0, Qt.AlignTop | Qt.AlignHCenter)
elif position == NavigationItemPosition.SCROLL:
widget.setParent(self.scrollWidget)
self.scrollLayout.insertWidget(
index, widget, 0, Qt.AlignTop | Qt.AlignHCenter)
else:
widget.setParent(self)
self.bottomLayout.insertWidget(
index, widget, 0, Qt.AlignBottom | Qt.AlignHCenter)
widget.show()
def removeWidget(self, routeKey: str):
""" remove widget
Parameters
----------
routeKey: str
the unique name of item
"""
if routeKey not in self.items:
return
widget = self.items.pop(routeKey)
widget.deleteLater()
self.history.remove(routeKey)
def setCurrentItem(self, routeKey: str):
""" set current selected item
Parameters
----------
routeKey: str
the unique name of item
"""
if routeKey not in self.items:
return
for k, widget in self.items.items():
widget.setSelected(k == routeKey)
def setFont(self, font: QFont):
""" set the font of navigation item """
super().setFont(font)
for widget in self.buttons():
widget.setFont(font)
def setSelectedTextVisible(self, isVisible: bool):
""" set whether the text is visible when button is selected """
for widget in self.buttons():
widget.setSelectedTextVisible(isVisible)
def setSelectedColor(self, light, dark):
""" set the selected color of all items """
self.lightSelectedColor = QColor(light)
self.darkSelectedColor = QColor(dark)
for button in self.buttons():
button.setSelectedColor(self.lightSelectedColor, self.darkSelectedColor)
def buttons(self):
return [i for i in self.items.values() if isinstance(i, NavigationPushButton)]
def _onWidgetClicked(self):
widget = self.sender() # type: NavigationWidget
if widget.isSelectable:
self.setCurrentItem(widget.property('routeKey'))

View File

@ -0,0 +1,268 @@
# coding:utf-8
from typing import Union
from PySide6.QtCore import Qt, QEvent, Signal
from PySide6.QtGui import QResizeEvent, QIcon
from PySide6.QtWidgets import QWidget
from .navigation_panel import NavigationPanel, NavigationItemPosition, NavigationWidget, NavigationDisplayMode
from .navigation_widget import NavigationTreeWidget
from ...common.style_sheet import FluentStyleSheet
from ...common.icon import FluentIconBase
class NavigationInterface(QWidget):
""" Navigation interface """
displayModeChanged = Signal(NavigationDisplayMode)
def __init__(self, parent=None, showMenuButton=True, showReturnButton=False, collapsible=True):
"""
Parameters
----------
parent: widget
parent widget
showMenuButton: bool
whether to show menu button
showReturnButton: bool
whether to show return button
collapsible: bool
Is the navigation interface collapsible
"""
super().__init__(parent=parent)
self.panel = NavigationPanel(self)
self.panel.setMenuButtonVisible(showMenuButton and collapsible)
self.panel.setReturnButtonVisible(showReturnButton)
self.panel.setCollapsible(collapsible)
self.panel.installEventFilter(self)
self.panel.displayModeChanged.connect(self.displayModeChanged)
self.resize(48, self.height())
self.setMinimumWidth(48)
self.setAttribute(Qt.WA_TranslucentBackground)
def addItem(self, routeKey: str, icon: Union[str, QIcon, FluentIconBase], text: str, onClick=None,
selectable=True, position=NavigationItemPosition.TOP, tooltip: str = None,
parentRouteKey: str = None) -> NavigationTreeWidget:
""" add navigation item
Parameters
----------
routKey: str
the unique name of item
icon: str | QIcon | FluentIconBase
the icon of navigation item
text: str
the text of navigation item
onClick: callable
the slot connected to item clicked signal
selectable: bool
whether the item is selectable
position: NavigationItemPosition
where the button is added
tooltip: str
the tooltip of item
parentRouteKey: str
the route key of parent item, the parent item should be `NavigationTreeWidgetBase`
"""
return self.insertItem(-1, routeKey, icon, text, onClick, selectable, position, tooltip, parentRouteKey)
def addWidget(self, routeKey: str, widget: NavigationWidget, onClick=None, position=NavigationItemPosition.TOP,
tooltip: str = None, parentRouteKey: str = None):
""" add custom widget
Parameters
----------
routKey: str
the unique name of item
widget: NavigationWidget
the custom widget to be added
onClick: callable
the slot connected to item clicked signal
position: NavigationItemPosition
where the widget is added
tooltip: str
the tooltip of widget
parentRouteKey: str
the route key of parent item, the parent item should be `NavigationTreeWidgetBase`
"""
self.insertWidget(-1, routeKey, widget, onClick, position, tooltip, parentRouteKey)
def insertItem(self, index: int, routeKey: str, icon: Union[str, QIcon, FluentIconBase], text: str,
onClick=None, selectable=True, position=NavigationItemPosition.TOP, tooltip: str = None,
parentRouteKey: str = None) -> NavigationTreeWidget:
""" insert navigation item
Parameters
----------
index: int
insert position
routKey: str
the unique name of item
icon: str | QIcon | FluentIconBase
the icon of navigation item
text: str
the text of navigation item
onClick: callable
the slot connected to item clicked signal
selectable: bool
whether the item is selectable
position: NavigationItemPosition
where the item is added
tooltip: str
the tooltip of item
parentRouteKey: str
the route key of parent item, the parent item should be `NavigationTreeWidgetBase`
"""
w = self.panel.insertItem(index, routeKey, icon, text, onClick, selectable, position, tooltip, parentRouteKey)
self.setMinimumHeight(self.panel.layoutMinHeight())
return w
def insertWidget(self, index: int, routeKey: str, widget: NavigationWidget, onClick=None,
position=NavigationItemPosition.TOP, tooltip: str = None, parentRouteKey: str = None):
""" insert custom widget
Parameters
----------
index: int
insert position
routKey: str
the unique name of item
widget: NavigationWidget
the custom widget to be added
onClick: callable
the slot connected to item clicked signal
position: NavigationItemPosition
where the widget is added
tooltip: str
the tooltip of widget
parentRouteKey: str
the route key of parent item, the parent item should be `NavigationTreeWidgetBase`
"""
self.panel.insertWidget(index, routeKey, widget, onClick, position, tooltip, parentRouteKey)
self.setMinimumHeight(self.panel.layoutMinHeight())
def addSeparator(self, position=NavigationItemPosition.TOP):
""" add separator
Parameters
----------
position: NavigationPostion
where to add the separator
"""
self.insertSeparator(-1, position)
def insertSeparator(self, index: int, position=NavigationItemPosition.TOP):
""" add separator
Parameters
----------
index: int
insert position
position: NavigationPostion
where to add the separator
"""
self.panel.insertSeparator(index, position)
self.setMinimumHeight(self.panel.layoutMinHeight())
def removeWidget(self, routeKey: str):
""" remove widget
Parameters
----------
routKey: str
the unique name of item
"""
self.panel.removeWidget(routeKey)
def setCurrentItem(self, name: str):
""" set current selected item
Parameters
----------
name: str
the unique name of item
"""
self.panel.setCurrentItem(name)
def expand(self, useAni=True):
""" expand navigation panel """
self.panel.expand(useAni)
def toggle(self):
""" toggle navigation panel """
self.panel.toggle()
def setExpandWidth(self, width: int):
""" set the maximum width """
self.panel.setExpandWidth(width)
def setMinimumExpandWidth(self, width: int):
""" Set the minimum window width that allows panel to be expanded """
self.panel.setMinimumExpandWidth(width)
def setMenuButtonVisible(self, isVisible: bool):
""" set whether the menu button is visible """
self.panel.setMenuButtonVisible(isVisible)
def setReturnButtonVisible(self, isVisible: bool):
""" set whether the return button is visible """
self.panel.setReturnButtonVisible(isVisible)
def setCollapsible(self, collapsible: bool):
self.panel.setCollapsible(collapsible)
def isAcrylicEnabled(self):
return self.panel.isAcrylicEnabled()
def setAcrylicEnabled(self, isEnabled: bool):
""" set whether the acrylic background effect is enabled """
self.panel.setAcrylicEnabled(isEnabled)
def widget(self, routeKey: str):
return self.panel.widget(routeKey)
def eventFilter(self, obj, e: QEvent):
if obj is not self.panel or e.type() != QEvent.Resize:
return super().eventFilter(obj, e)
if self.panel.displayMode != NavigationDisplayMode.MENU:
event = QResizeEvent(e)
if event.oldSize().width() != event.size().width():
self.setFixedWidth(event.size().width())
return super().eventFilter(obj, e)
def resizeEvent(self, e: QResizeEvent):
if e.oldSize().height() != self.height():
self.panel.setFixedHeight(self.height())

View File

@ -0,0 +1,658 @@
# coding:utf-8
from enum import Enum
from typing import Dict, Union
from PySide6.QtCore import Qt, QPropertyAnimation, QRect, QSize, QEvent, QEasingCurve, Signal, QPoint
from PySide6.QtGui import QResizeEvent, QIcon, QColor, QPainterPath
from PySide6.QtWidgets import QWidget, QVBoxLayout, QFrame, QApplication, QHBoxLayout
from .navigation_widget import (NavigationTreeWidgetBase, NavigationToolButton, NavigationWidget, NavigationSeparator,
NavigationTreeWidget, NavigationFlyoutMenu)
from ..widgets.acrylic_label import AcrylicBrush
from ..widgets.scroll_area import ScrollArea
from ..widgets.tool_tip import ToolTipFilter
from ..widgets.scroll_bar import ScrollBarHandleDisplayMode
from ..widgets.flyout import Flyout, FlyoutAnimationType, FlyoutViewBase, SlideRightFlyoutAnimationManager
from ..material.acrylic_flyout import AcrylicFlyout, AcrylicFlyoutViewBase
from ...common.router import qrouter
from ...common.style_sheet import FluentStyleSheet, isDarkTheme
from ...common.icon import FluentIconBase
from ...common.icon import FluentIcon as FIF
class NavigationDisplayMode(Enum):
""" Navigation display mode """
MINIMAL = 0
COMPACT = 1
EXPAND = 2
MENU = 3
class NavigationItemPosition(Enum):
""" Navigation item position """
TOP = 0
SCROLL = 1
BOTTOM = 2
class NavigationToolTipFilter(ToolTipFilter):
""" Navigation tool tip filter """
def _canShowToolTip(self) -> bool:
isVisible = super()._canShowToolTip()
parent = self.parent() # type: NavigationWidget
return isVisible and parent.isCompacted
class RouteKeyError(Exception):
""" Route key error """
class NavigationItem:
""" Navigation item """
def __init__(self, routeKey: str, parentRouteKey: str, widget: NavigationWidget):
self.routeKey = routeKey
self.parentRouteKey = parentRouteKey
self.widget = widget
class NavigationPanel(QFrame):
""" Navigation panel """
displayModeChanged = Signal(NavigationDisplayMode)
def __init__(self, parent=None, isMinimalEnabled=False):
super().__init__(parent=parent)
self._parent = parent # type: QWidget
self._isMenuButtonVisible = True
self._isReturnButtonVisible = False
self._isCollapsible = True
self._isAcrylicEnabled = False
self.acrylicBrush = AcrylicBrush(self, 30)
self.scrollArea = ScrollArea(self)
self.scrollWidget = QWidget()
self.menuButton = NavigationToolButton(FIF.MENU, self)
self.returnButton = NavigationToolButton(FIF.RETURN, self)
self.vBoxLayout = NavigationItemLayout(self)
self.topLayout = NavigationItemLayout()
self.bottomLayout = NavigationItemLayout()
self.scrollLayout = NavigationItemLayout(self.scrollWidget)
self.items = {} # type: Dict[str, NavigationItem]
self.history = qrouter
self.expandAni = QPropertyAnimation(self, b'geometry', self)
self.expandWidth = 322
self.minimumExpandWidth = 1008
self.isMinimalEnabled = isMinimalEnabled
if isMinimalEnabled:
self.displayMode = NavigationDisplayMode.MINIMAL
else:
self.displayMode = NavigationDisplayMode.COMPACT
self.__initWidget()
def __initWidget(self):
self.resize(48, self.height())
self.setAttribute(Qt.WA_StyledBackground)
self.window().installEventFilter(self)
self.returnButton.hide()
self.returnButton.setDisabled(True)
self.scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.scrollArea.horizontalScrollBar().setEnabled(False)
self.scrollArea.setWidget(self.scrollWidget)
self.scrollArea.setWidgetResizable(True)
self.scrollArea.scrollDelagate.vScrollBar.setHandleDisplayMode(ScrollBarHandleDisplayMode.ON_HOVER)
self.expandAni.setEasingCurve(QEasingCurve.OutQuad)
self.expandAni.setDuration(150)
self.menuButton.clicked.connect(self.toggle)
self.expandAni.finished.connect(self._onExpandAniFinished)
self.history.emptyChanged.connect(self.returnButton.setDisabled)
self.returnButton.clicked.connect(self.history.pop)
# add tool tip
self.returnButton.installEventFilter(ToolTipFilter(self.returnButton, 1000))
self.returnButton.setToolTip(self.tr('Back'))
self.menuButton.installEventFilter(ToolTipFilter(self.menuButton, 1000))
self.menuButton.setToolTip(self.tr('Open Navigation'))
self.scrollWidget.setObjectName('scrollWidget')
self.setProperty('menu', False)
FluentStyleSheet.NAVIGATION_INTERFACE.apply(self)
FluentStyleSheet.NAVIGATION_INTERFACE.apply(self.scrollWidget)
self.__initLayout()
def __initLayout(self):
self.vBoxLayout.setContentsMargins(0, 5, 0, 5)
self.topLayout.setContentsMargins(4, 0, 4, 0)
self.bottomLayout.setContentsMargins(4, 0, 4, 0)
self.scrollLayout.setContentsMargins(4, 0, 4, 0)
self.vBoxLayout.setSpacing(4)
self.topLayout.setSpacing(4)
self.bottomLayout.setSpacing(4)
self.scrollLayout.setSpacing(4)
self.vBoxLayout.addLayout(self.topLayout, 0)
self.vBoxLayout.addWidget(self.scrollArea, 1)
self.vBoxLayout.addLayout(self.bottomLayout, 0)
self.vBoxLayout.setAlignment(Qt.AlignTop)
self.topLayout.setAlignment(Qt.AlignTop)
self.scrollLayout.setAlignment(Qt.AlignTop)
self.bottomLayout.setAlignment(Qt.AlignBottom)
self.topLayout.addWidget(self.returnButton, 0, Qt.AlignTop)
self.topLayout.addWidget(self.menuButton, 0, Qt.AlignTop)
def _updateAcrylicColor(self):
if isDarkTheme():
tintColor = QColor(32, 32, 32, 200)
luminosityColor = QColor(0, 0, 0, 0)
else:
tintColor = QColor(255, 255, 255, 180)
luminosityColor = QColor(255, 255, 255, 0)
self.acrylicBrush.tintColor = tintColor
self.acrylicBrush.luminosityColor = luminosityColor
def widget(self, routeKey: str):
if routeKey not in self.items:
raise RouteKeyError(f"`{routeKey}` is illegal.")
return self.items[routeKey].widget
def addItem(self, routeKey: str, icon: Union[str, QIcon, FluentIconBase], text: str, onClick=None, selectable=True,
position=NavigationItemPosition.TOP, tooltip: str = None, parentRouteKey: str = None):
""" add navigation item
Parameters
----------
routeKey: str
the unique name of item
icon: str | QIcon | FluentIconBase
the icon of navigation item
text: str
the text of navigation item
onClick: callable
the slot connected to item clicked signal
position: NavigationItemPosition
where the button is added
selectable: bool
whether the item is selectable
tooltip: str
the tooltip of item
parentRouteKey: str
the route key of parent item, the parent widget should be `NavigationTreeWidget`
"""
return self.insertItem(-1, routeKey, icon, text, onClick, selectable, position, tooltip, parentRouteKey)
def addWidget(self, routeKey: str, widget: NavigationWidget, onClick=None, position=NavigationItemPosition.TOP,
tooltip: str = None, parentRouteKey: str = None):
""" add custom widget
Parameters
----------
routeKey: str
the unique name of item
widget: NavigationWidget
the custom widget to be added
onClick: callable
the slot connected to item clicked signal
position: NavigationItemPosition
where the button is added
tooltip: str
the tooltip of widget
parentRouteKey: str
the route key of parent item, the parent item should be `NavigationTreeWidget`
"""
self.insertWidget(-1, routeKey, widget, onClick, position, tooltip, parentRouteKey)
def insertItem(self, index: int, routeKey: str, icon: Union[str, QIcon, FluentIconBase], text: str, onClick=None,
selectable=True, position=NavigationItemPosition.TOP, tooltip: str = None, parentRouteKey=None):
""" insert navigation tree item
Parameters
----------
index: int
the insert position of parent widget
routeKey: str
the unique name of item
icon: str | QIcon | FluentIconBase
the icon of navigation item
text: str
the text of navigation item
onClick: callable
the slot connected to item clicked signal
position: NavigationItemPosition
where the button is added
selectable: bool
whether the item is selectable
tooltip: str
the tooltip of item
parentRouteKey: str
the route key of parent item, the parent item should be `NavigationTreeWidget`
"""
if routeKey in self.items:
return
w = NavigationTreeWidget(icon, text, selectable, self)
self.insertWidget(index, routeKey, w, onClick, position, tooltip, parentRouteKey)
return w
def insertWidget(self, index: int, routeKey: str, widget: NavigationWidget, onClick=None,
position=NavigationItemPosition.TOP, tooltip: str = None, parentRouteKey: str = None):
""" insert custom widget
Parameters
----------
index: int
insert position
routeKey: str
the unique name of item
widget: NavigationWidget
the custom widget to be added
onClick: callable
the slot connected to item clicked signal
position: NavigationItemPosition
where the button is added
tooltip: str
the tooltip of widget
parentRouteKey: str
the route key of parent item, the parent item should be `NavigationTreeWidget`
"""
if routeKey in self.items:
return
self._registerWidget(routeKey, parentRouteKey, widget, onClick, tooltip)
if parentRouteKey:
self.widget(parentRouteKey).insertChild(index, widget)
else:
self._insertWidgetToLayout(index, widget, position)
def addSeparator(self, position=NavigationItemPosition.TOP):
""" add separator
Parameters
----------
position: NavigationPostion
where to add the separator
"""
self.insertSeparator(-1, position)
def insertSeparator(self, index: int, position=NavigationItemPosition.TOP):
""" add separator
Parameters
----------
index: int
insert position
position: NavigationPostion
where to add the separator
"""
separator = NavigationSeparator(self)
self._insertWidgetToLayout(index, separator, position)
def _registerWidget(self, routeKey: str, parentRouteKey: str, widget: NavigationWidget, onClick, tooltip: str):
""" register widget """
widget.clicked.connect(self._onWidgetClicked)
if onClick is not None:
widget.clicked.connect(onClick)
widget.setProperty('routeKey', routeKey)
widget.setProperty('parentRouteKey', parentRouteKey)
self.items[routeKey] = NavigationItem(routeKey, parentRouteKey, widget)
if self.displayMode in [NavigationDisplayMode.EXPAND, NavigationDisplayMode.MENU]:
widget.setCompacted(False)
if tooltip:
widget.setToolTip(tooltip)
widget.installEventFilter(NavigationToolTipFilter(widget, 1000))
def _insertWidgetToLayout(self, index: int, widget: NavigationWidget, position: NavigationItemPosition):
""" insert widget to layout """
if position == NavigationItemPosition.TOP:
widget.setParent(self)
self.topLayout.insertWidget(index, widget, 0, Qt.AlignTop)
elif position == NavigationItemPosition.SCROLL:
widget.setParent(self.scrollWidget)
self.scrollLayout.insertWidget(index, widget, 0, Qt.AlignTop)
else:
widget.setParent(self)
self.bottomLayout.insertWidget(index, widget, 0, Qt.AlignBottom)
widget.show()
def removeWidget(self, routeKey: str):
""" remove widget
Parameters
----------
routeKey: str
the unique name of item
"""
if routeKey not in self.items:
return
item = self.items.pop(routeKey)
if item.parentRouteKey is not None:
self.widget(item.parentRouteKey).removeChild(item.widget)
if isinstance(item.widget, NavigationTreeWidgetBase):
for child in item.widget.findChildren(NavigationWidget, options=Qt.FindChildrenRecursively):
key = child.property('routeKey')
if key is None:
continue
self.items.pop(key)
child.deleteLater()
self.history.remove(key)
item.widget.deleteLater()
self.history.remove(routeKey)
def setMenuButtonVisible(self, isVisible: bool):
""" set whether the menu button is visible """
self._isMenuButtonVisible = isVisible
self.menuButton.setVisible(isVisible)
def setReturnButtonVisible(self, isVisible: bool):
""" set whether the return button is visible """
self._isReturnButtonVisible = isVisible
self.returnButton.setVisible(isVisible)
def setCollapsible(self, on: bool):
self._isCollapsible = on
if not on and self.displayMode != NavigationDisplayMode.EXPAND:
self.expand(False)
def setExpandWidth(self, width: int):
""" set the maximum width """
if width <= 42:
return
self.expandWidth = width
NavigationWidget.EXPAND_WIDTH = width - 10
def setMinimumExpandWidth(self, width: int):
""" Set the minimum window width that allows panel to be expanded """
self.minimumExpandWidth = width
def setAcrylicEnabled(self, isEnabled: bool):
if isEnabled == self.isAcrylicEnabled():
return
self._isAcrylicEnabled = isEnabled
self.setProperty("transparent", self._canDrawAcrylic())
self.setStyle(QApplication.style())
self.update()
def isAcrylicEnabled(self):
""" whether the acrylic effect is enabled """
return self._isAcrylicEnabled
def expand(self, useAni=True):
""" expand navigation panel """
self._setWidgetCompacted(False)
self.expandAni.setProperty('expand', True)
self.menuButton.setToolTip(self.tr('Close Navigation'))
# determine the display mode according to the width of window
# https://learn.microsoft.com/en-us/windows/apps/design/controls/navigationview#default
expandWidth = self.minimumExpandWidth + self.expandWidth - 322
if (self.window().width() >= expandWidth and not self.isMinimalEnabled) or not self._isCollapsible:
self.displayMode = NavigationDisplayMode.EXPAND
else:
self.setProperty('menu', True)
self.setStyle(QApplication.style())
self.displayMode = NavigationDisplayMode.MENU
# grab acrylic image
if self._canDrawAcrylic():
self.acrylicBrush.grabImage(
QRect(self.mapToGlobal(QPoint()), QSize(self.expandWidth, self.height())))
if not self._parent.isWindow():
pos = self.parent().pos()
self.setParent(self.window())
self.move(pos)
self.show()
if useAni:
self.displayModeChanged.emit(self.displayMode)
self.expandAni.setStartValue(
QRect(self.pos(), QSize(48, self.height())))
self.expandAni.setEndValue(
QRect(self.pos(), QSize(self.expandWidth, self.height())))
self.expandAni.start()
else:
self.resize(self.expandWidth, self.height())
self._onExpandAniFinished()
def collapse(self):
""" collapse navigation panel """
if self.expandAni.state() == QPropertyAnimation.Running:
return
for item in self.items.values():
w = item.widget
if isinstance(w, NavigationTreeWidgetBase) and w.isRoot():
w.setExpanded(False)
self.expandAni.setStartValue(
QRect(self.pos(), QSize(self.width(), self.height())))
self.expandAni.setEndValue(
QRect(self.pos(), QSize(48, self.height())))
self.expandAni.setProperty('expand', False)
self.expandAni.start()
self.menuButton.setToolTip(self.tr('Open Navigation'))
def toggle(self):
""" toggle navigation panel """
if self.displayMode in [NavigationDisplayMode.COMPACT, NavigationDisplayMode.MINIMAL]:
self.expand()
else:
self.collapse()
def setCurrentItem(self, routeKey: str):
""" set current selected item
Parameters
----------
routeKey: str
the unique name of item
"""
if routeKey not in self.items:
return
for k, item in self.items.items():
item.widget.setSelected(k == routeKey)
def _onWidgetClicked(self):
widget = self.sender() # type: NavigationWidget
if not widget.isSelectable:
return self._showFlyoutNavigationMenu(widget)
self.setCurrentItem(widget.property('routeKey'))
isLeaf = not isinstance(widget, NavigationTreeWidgetBase) or widget.isLeaf()
if self.displayMode == NavigationDisplayMode.MENU and isLeaf:
self.collapse()
elif self.isCollapsed():
self._showFlyoutNavigationMenu(widget)
def _showFlyoutNavigationMenu(self, widget: NavigationTreeWidget):
""" show flyout navigation menu """
if not (self.isCollapsed() and isinstance(widget, NavigationTreeWidget)):
return
if not widget.isRoot() or widget.isLeaf():
return
layout = QHBoxLayout()
if self._canDrawAcrylic():
view = AcrylicFlyoutViewBase()
view.setLayout(layout)
flyout = AcrylicFlyout(view, self.window())
else:
view = FlyoutViewBase()
view.setLayout(layout)
flyout = Flyout(view, self.window())
# add navigation menu to flyout
menu = NavigationFlyoutMenu(widget, view)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(menu)
# execuse flyout animation
flyout.resize(flyout.sizeHint())
pos = SlideRightFlyoutAnimationManager(flyout).position(widget)
flyout.exec(pos, FlyoutAnimationType.SLIDE_RIGHT)
menu.expanded.connect(lambda: self._adjustFlyoutMenuSize(flyout, widget, menu))
def _adjustFlyoutMenuSize(self, flyout: Flyout, widget: NavigationTreeWidget, menu: NavigationFlyoutMenu):
flyout.view.setFixedSize(menu.size())
flyout.setFixedSize(flyout.layout().sizeHint())
manager = flyout.aniManager
pos = manager.position(widget)
rect = self.window().geometry()
w, h = flyout.sizeHint().width() + 5, flyout.sizeHint().height()
x = max(rect.left(), min(pos.x(), rect.right() - w))
y = max(rect.top() + 42, min(pos.y() - 4, rect.bottom() - h + 5))
flyout.move(x, y)
def isCollapsed(self):
return self.displayMode == NavigationDisplayMode.COMPACT
def eventFilter(self, obj, e: QEvent):
if obj is not self.window() or not self._isCollapsible:
return super().eventFilter(obj, e)
if e.type() == QEvent.MouseButtonRelease:
if not self.geometry().contains(e.pos()) and self.displayMode == NavigationDisplayMode.MENU:
self.collapse()
elif e.type() == QEvent.Resize:
w = QResizeEvent(e).size().width()
if w < self.minimumExpandWidth and self.displayMode == NavigationDisplayMode.EXPAND:
self.collapse()
elif w >= self.minimumExpandWidth and self.displayMode == NavigationDisplayMode.COMPACT and \
not self._isMenuButtonVisible:
self.expand()
return super().eventFilter(obj, e)
def _onExpandAniFinished(self):
if not self.expandAni.property('expand'):
if self.isMinimalEnabled:
self.displayMode = NavigationDisplayMode.MINIMAL
else:
self.displayMode = NavigationDisplayMode.COMPACT
self.displayModeChanged.emit(self.displayMode)
if self.displayMode == NavigationDisplayMode.MINIMAL:
self.hide()
self.setProperty('menu', False)
self.setStyle(QApplication.style())
elif self.displayMode == NavigationDisplayMode.COMPACT:
self.setProperty('menu', False)
self.setStyle(QApplication.style())
for item in self.items.values():
item.widget.setCompacted(True)
if not self._parent.isWindow():
self.setParent(self._parent)
self.move(0, 0)
self.show()
def _setWidgetCompacted(self, isCompacted: bool):
""" set whether the navigation widget is compacted """
for item in self.findChildren(NavigationWidget):
item.setCompacted(isCompacted)
def layoutMinHeight(self):
th = self.topLayout.minimumSize().height()
bh = self.bottomLayout.minimumSize().height()
sh = sum(w.height() for w in self.findChildren(NavigationSeparator))
spacing = self.topLayout.count() * self.topLayout.spacing()
spacing += self.bottomLayout.count() * self.bottomLayout.spacing()
return 36 + th + bh + sh + spacing
def _canDrawAcrylic(self):
return self.acrylicBrush.isAvailable() and self.isAcrylicEnabled()
def paintEvent(self, e):
if not self._canDrawAcrylic() or self.displayMode != NavigationDisplayMode.MENU:
return super().paintEvent(e)
path = QPainterPath()
path.setFillRule(Qt.WindingFill)
path.addRoundedRect(0, 1, self.width() - 1, self.height() - 1, 7, 7)
path.addRect(0, 1, 8, self.height() - 1)
self.acrylicBrush.setClipPath(path)
self._updateAcrylicColor()
self.acrylicBrush.paint()
super().paintEvent(e)
class NavigationItemLayout(QVBoxLayout):
""" Navigation layout """
def setGeometry(self, rect: QRect):
super().setGeometry(rect)
for i in range(self.count()):
item = self.itemAt(i)
if isinstance(item.widget(), NavigationSeparator):
geo = item.geometry()
item.widget().setGeometry(0, geo.y(), geo.width(), geo.height())

View File

@ -0,0 +1,695 @@
# coding:utf-8
from typing import Union, List
from PySide6.QtCore import (Qt, Signal, QRect, QRectF, QPropertyAnimation, Property, QMargins,
QEasingCurve, QPoint, QEvent)
from PySide6.QtGui import QColor, QPainter, QPen, QIcon, QCursor, QFont, QBrush, QPixmap, QImage
from PySide6.QtWidgets import QWidget, QVBoxLayout
from collections import deque
from ...common.config import isDarkTheme
from ...common.style_sheet import themeColor
from ...common.icon import drawIcon, toQIcon
from ...common.icon import FluentIcon as FIF
from ...common.color import autoFallbackThemeColor
from ...common.font import setFont
from ..widgets.scroll_area import ScrollArea
from ..widgets.label import AvatarWidget
from ..widgets.info_badge import InfoBadgeManager, InfoBadgePosition
class NavigationWidget(QWidget):
""" Navigation widget """
clicked = Signal(bool) # whether triggered by the user
selectedChanged = Signal(bool)
EXPAND_WIDTH = 312
def __init__(self, isSelectable: bool, parent=None):
super().__init__(parent)
self.isCompacted = True
self.isSelected = False
self.isPressed = False
self.isEnter = False
self.isSelectable = isSelectable
self.treeParent = None
self.nodeDepth = 0
# text color
self.lightTextColor = QColor(0, 0, 0)
self.darkTextColor = QColor(255, 255, 255)
self.setFixedSize(40, 36)
def enterEvent(self, e):
self.isEnter = True
self.update()
def leaveEvent(self, e):
self.isEnter = False
self.isPressed = False
self.update()
def mousePressEvent(self, e):
super().mousePressEvent(e)
self.isPressed = True
self.update()
def mouseReleaseEvent(self, e):
super().mouseReleaseEvent(e)
self.isPressed = False
self.update()
self.clicked.emit(True)
def click(self):
self.clicked.emit(True)
def setCompacted(self, isCompacted: bool):
""" set whether the widget is compacted """
if isCompacted == self.isCompacted:
return
self.isCompacted = isCompacted
if isCompacted:
self.setFixedSize(40, 36)
else:
self.setFixedSize(self.EXPAND_WIDTH, 36)
self.update()
def setSelected(self, isSelected: bool):
""" set whether the button is selected
Parameters
----------
isSelected: bool
whether the button is selected
"""
if not self.isSelectable:
return
self.isSelected = isSelected
self.update()
self.selectedChanged.emit(isSelected)
def textColor(self):
return self.darkTextColor if isDarkTheme() else self.lightTextColor
def setLightTextColor(self, color):
""" set the text color in light theme mode """
self.lightTextColor = QColor(color)
self.update()
def setDarkTextColor(self, color):
""" set the text color in dark theme mode """
self.darkTextColor = QColor(color)
self.update()
def setTextColor(self, light, dark):
""" set the text color in light/dark theme mode """
self.setLightTextColor(light)
self.setDarkTextColor(dark)
class NavigationPushButton(NavigationWidget):
""" Navigation push button """
def __init__(self, icon: Union[str, QIcon, FIF], text: str, isSelectable: bool, parent=None):
"""
Parameters
----------
icon: str | QIcon | FluentIconBase
the icon to be drawn
text: str
the text of button
"""
super().__init__(isSelectable=isSelectable, parent=parent)
self._icon = icon
self._text = text
self.lightIndicatorColor = QColor()
self.darkIndicatorColor = QColor()
setFont(self)
def text(self):
return self._text
def setText(self, text: str):
self._text = text
self.update()
def icon(self):
return toQIcon(self._icon)
def setIcon(self, icon: Union[str, QIcon, FIF]):
self._icon = icon
self.update()
def _margins(self):
return QMargins(0, 0, 0, 0)
def _canDrawIndicator(self):
return self.isSelected
def setIndicatorColor(self, light, dark):
self.lightIndicatorColor = QColor(light)
self.darkIndicatorColor = QColor(dark)
self.update()
def paintEvent(self, e):
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing |
QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform)
painter.setPen(Qt.NoPen)
if self.isPressed:
painter.setOpacity(0.7)
if not self.isEnabled():
painter.setOpacity(0.4)
# draw background
c = 255 if isDarkTheme() else 0
m = self._margins()
pl, pr = m.left(), m.right()
globalRect = QRect(self.mapToGlobal(QPoint()), self.size())
if self._canDrawIndicator():
painter.setBrush(QColor(c, c, c, 6 if self.isEnter else 10))
painter.drawRoundedRect(self.rect(), 5, 5)
# draw indicator
painter.setBrush(autoFallbackThemeColor(self.lightIndicatorColor, self.darkIndicatorColor))
painter.drawRoundedRect(pl, 10, 3, 16, 1.5, 1.5)
elif self.isEnter and self.isEnabled() and globalRect.contains(QCursor.pos()):
painter.setBrush(QColor(c, c, c, 10))
painter.drawRoundedRect(self.rect(), 5, 5)
drawIcon(self._icon, painter, QRectF(11.5+pl, 10, 16, 16))
# draw text
if self.isCompacted:
return
painter.setFont(self.font())
painter.setPen(self.textColor())
left = 44 + pl if not self.icon().isNull() else pl + 16
painter.drawText(QRectF(left, 0, self.width()-13-left-pr, self.height()), Qt.AlignVCenter, self.text())
class NavigationToolButton(NavigationPushButton):
""" Navigation tool button """
def __init__(self, icon: Union[str, QIcon, FIF], parent=None):
super().__init__(icon, '', False, parent)
def setCompacted(self, isCompacted: bool):
self.setFixedSize(40, 36)
class NavigationSeparator(NavigationWidget):
""" Navigation Separator """
def __init__(self, parent=None):
super().__init__(False, parent=parent)
self.setCompacted(True)
def setCompacted(self, isCompacted: bool):
if isCompacted:
self.setFixedSize(48, 3)
else:
self.setFixedSize(self.EXPAND_WIDTH + 10, 3)
self.update()
def paintEvent(self, e):
painter = QPainter(self)
c = 255 if isDarkTheme() else 0
pen = QPen(QColor(c, c, c, 15))
pen.setCosmetic(True)
painter.setPen(pen)
painter.drawLine(0, 1, self.width(), 1)
class NavigationTreeItem(NavigationPushButton):
""" Navigation tree item widget """
itemClicked = Signal(bool, bool) # triggerByUser, clickArrow
def __init__(self, icon: Union[str, QIcon, FIF], text: str, isSelectable: bool, parent=None):
super().__init__(icon, text, isSelectable, parent)
self._arrowAngle = 0
self.rotateAni = QPropertyAnimation(self, b'arrowAngle', self)
def setExpanded(self, isExpanded: bool):
self.rotateAni.stop()
self.rotateAni.setEndValue(180 if isExpanded else 0)
self.rotateAni.setDuration(150)
self.rotateAni.start()
def mouseReleaseEvent(self, e):
super().mouseReleaseEvent(e)
clickArrow = QRectF(self.width()-30, 8, 20, 20).contains(e.pos())
self.itemClicked.emit(True, clickArrow and not self.parent().isLeaf())
self.update()
def _canDrawIndicator(self):
p = self.parent() # type: NavigationTreeWidget
if p.isLeaf() or p.isSelected:
return p.isSelected
for child in p.treeChildren:
if child.itemWidget._canDrawIndicator() and not child.isVisible():
return True
return False
def _margins(self):
p = self.parent() # type: NavigationTreeWidget
return QMargins(p.nodeDepth*28, 0, 20*bool(p.treeChildren), 0)
def paintEvent(self, e):
super().paintEvent(e)
if self.isCompacted or not self.parent().treeChildren:
return
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing)
painter.setPen(Qt.NoPen)
if self.isPressed:
painter.setOpacity(0.7)
if not self.isEnabled():
painter.setOpacity(0.4)
painter.translate(self.width() - 20, 18)
painter.rotate(self.arrowAngle)
FIF.ARROW_DOWN.render(painter, QRectF(-5, -5, 9.6, 9.6))
def getArrowAngle(self):
return self._arrowAngle
def setArrowAngle(self, angle):
self._arrowAngle = angle
self.update()
arrowAngle = Property(float, getArrowAngle, setArrowAngle)
class NavigationTreeWidgetBase(NavigationWidget):
""" Navigation tree widget base class """
def addChild(self, child):
""" add child
Parameters
----------
child: NavigationTreeWidgetBase
child item
"""
raise NotImplementedError
def insertChild(self, index: int, child: NavigationWidget):
""" insert child
Parameters
----------
child: NavigationTreeWidgetBase
child item
"""
raise NotImplementedError
def removeChild(self, child: NavigationWidget):
""" remove child
Parameters
----------
child: NavigationTreeWidgetBase
child item
"""
raise NotImplementedError
def isRoot(self):
""" is root node """
return True
def isLeaf(self):
""" is leaf node """
return True
def setExpanded(self, isExpanded: bool):
""" set the expanded status
Parameters
----------
isExpanded: bool
whether to expand node
"""
raise NotImplementedError
def childItems(self) -> list:
""" return child items """
raise NotImplementedError
class NavigationTreeWidget(NavigationTreeWidgetBase):
""" Navigation tree widget """
expanded = Signal()
def __init__(self, icon: Union[str, QIcon, FIF], text: str, isSelectable: bool, parent=None):
super().__init__(isSelectable, parent)
self.treeChildren = [] # type: List[NavigationTreeWidget]
self.isExpanded = False
self._icon = icon
self.itemWidget = NavigationTreeItem(icon, text, isSelectable, self)
self.vBoxLayout = QVBoxLayout(self)
self.expandAni = QPropertyAnimation(self, b'geometry', self)
self.__initWidget()
def __initWidget(self):
self.vBoxLayout.setSpacing(4)
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
self.vBoxLayout.addWidget(self.itemWidget, 0, Qt.AlignTop)
self.itemWidget.itemClicked.connect(self._onClicked)
self.setAttribute(Qt.WA_TranslucentBackground)
self.expandAni.valueChanged.connect(lambda g: self.setFixedSize(g.size()))
self.expandAni.valueChanged.connect(self.expanded)
self.expandAni.finished.connect(self.parentWidget().layout().invalidate)
def addChild(self, child):
self.insertChild(-1, child)
def text(self):
return self.itemWidget.text()
def icon(self):
return self.itemWidget.icon()
def setText(self, text):
self.itemWidget.setText(text)
def setIcon(self, icon: Union[str, QIcon, FIF]):
self.itemWidget.setIcon(icon)
def textColor(self):
return self.itemWidget.textColor()
def setLightTextColor(self, color):
""" set the text color in light theme mode """
self.itemWidget.setLightTextColor(color)
def setDarkTextColor(self, color):
""" set the text color in dark theme mode """
self.itemWidget.setDarkTextColor(color)
def setTextColor(self, light, dark):
""" set the text color in light/dark theme mode """
self.lightTextColor = QColor(light)
self.darkTextColor = QColor(dark)
self.itemWidget.setTextColor(light, dark)
def setIndicatorColor(self, light, dark):
""" set the indicator color in light/dark theme mode """
self.itemWidget.setIndicatorColor(light, dark)
def setFont(self, font: QFont):
super().setFont(font)
self.itemWidget.setFont(font)
def clone(self):
root = NavigationTreeWidget(self._icon, self.text(), self.isSelectable, self.parent())
root.setSelected(self.isSelected)
root.setFixedSize(self.size())
root.setTextColor(self.lightTextColor, self.darkTextColor)
root.setIndicatorColor(self.itemWidget.lightIndicatorColor, self.itemWidget.darkIndicatorColor)
root.nodeDepth = self.nodeDepth
root.clicked.connect(self.clicked)
self.selectedChanged.connect(root.setSelected)
for child in self.treeChildren:
root.addChild(child.clone())
return root
def suitableWidth(self):
m = self.itemWidget._margins()
left = 57 + m.left() if not self.icon().isNull() else m.left() + 29
tw = self.itemWidget.fontMetrics().boundingRect(self.text()).width()
return left + tw + m.right()
def insertChild(self, index, child):
if child in self.treeChildren:
return
child.treeParent = self
child.nodeDepth = self.nodeDepth + 1
child.setVisible(self.isExpanded)
child.expandAni.valueChanged.connect(lambda: self.setFixedSize(self.sizeHint()))
child.expandAni.valueChanged.connect(self.expanded)
# connect height changed signal to parent recursively
p = self.treeParent
while p:
child.expandAni.valueChanged.connect(lambda v, p=p: p.setFixedSize(p.sizeHint()))
p = p.treeParent
if index < 0:
index = len(self.treeChildren)
index += 1 # item widget should always be the first
self.treeChildren.insert(index, child)
self.vBoxLayout.insertWidget(index, child, 0, Qt.AlignTop)
# adjust height
if self.isExpanded:
self.setFixedHeight(self.height() + child.height() + self.vBoxLayout.spacing())
p = self.treeParent
while p:
p.setFixedSize(p.sizeHint())
p = p.treeParent
self.update()
def removeChild(self, child):
self.treeChildren.remove(child)
self.vBoxLayout.removeWidget(child)
def childItems(self) -> list:
return self.treeChildren
def setExpanded(self, isExpanded: bool, ani=False):
""" set the expanded status """
if isExpanded == self.isExpanded:
return
self.isExpanded = isExpanded
self.itemWidget.setExpanded(isExpanded)
for child in self.treeChildren:
child.setVisible(isExpanded)
child.setFixedSize(child.sizeHint())
if ani:
self.expandAni.stop()
self.expandAni.setStartValue(self.geometry())
self.expandAni.setEndValue(QRect(self.pos(), self.sizeHint()))
self.expandAni.setDuration(120)
self.expandAni.setEasingCurve(QEasingCurve.OutQuad)
self.expandAni.start()
else:
self.setFixedSize(self.sizeHint())
def isRoot(self):
return self.treeParent is None
def isLeaf(self):
return len(self.treeChildren) == 0
def setSelected(self, isSelected: bool):
super().setSelected(isSelected)
self.itemWidget.setSelected(isSelected)
def mouseReleaseEvent(self, e):
pass
def setCompacted(self, isCompacted: bool):
super().setCompacted(isCompacted)
self.itemWidget.setCompacted(isCompacted)
def _onClicked(self, triggerByUser, clickArrow):
if not self.isCompacted:
if self.isSelectable and not self.isSelected and not clickArrow:
self.setExpanded(True, ani=True)
else:
self.setExpanded(not self.isExpanded, ani=True)
if not clickArrow or self.isCompacted:
self.clicked.emit(triggerByUser)
class NavigationAvatarWidget(NavigationWidget):
""" Avatar widget """
def __init__(self, name: str, avatar: Union[str, QPixmap, QImage] = None, parent=None):
super().__init__(isSelectable=False, parent=parent)
self.name = name
self.avatar = AvatarWidget(self)
self.avatar.setRadius(12)
self.avatar.setText(name)
self.avatar.move(8, 6)
setFont(self)
if avatar:
self.setAvatar(avatar)
def setName(self, name: str):
self.name = name
self.avatar.setText(name)
self.update()
def setAvatar(self, avatar: Union[str, QPixmap, QImage]):
self.avatar.setImage(avatar)
self.avatar.setRadius(12)
self.update()
def paintEvent(self, e):
painter = QPainter(self)
painter.setRenderHints(
QPainter.SmoothPixmapTransform | QPainter.Antialiasing)
painter.setPen(Qt.NoPen)
if self.isPressed:
painter.setOpacity(0.7)
# draw background
if self.isEnter:
c = 255 if isDarkTheme() else 0
painter.setBrush(QColor(c, c, c, 10))
painter.drawRoundedRect(self.rect(), 5, 5)
if not self.isCompacted:
painter.setPen(self.textColor())
painter.setFont(self.font())
painter.drawText(QRect(44, 0, 255, 36), Qt.AlignVCenter, self.name)
@InfoBadgeManager.register(InfoBadgePosition.NAVIGATION_ITEM)
class NavigationItemInfoBadgeManager(InfoBadgeManager):
""" Navigation item info badge manager """
def eventFilter(self, obj, e: QEvent):
if obj is self.target:
if e.type() == QEvent.Show:
self.badge.show()
return super().eventFilter(obj, e)
def position(self):
target = self.target
self.badge.setVisible(target.isVisible())
if target.isCompacted:
return target.geometry().topRight() - QPoint(self.badge.width() + 2, -2)
if isinstance(target, NavigationTreeWidget):
dx = 10 if target.isLeaf() else 35
x = target.geometry().right() - self.badge.width() - dx
y = target.y() + 18 - self.badge.height() // 2
else:
x = target.geometry().right() - self.badge.width() - 10
y = target.geometry().center().y() - self.badge.height() // 2
return QPoint(x, y)
class NavigationFlyoutMenu(ScrollArea):
""" Navigation flyout menu """
expanded = Signal()
def __init__(self, tree: NavigationTreeWidget, parent=None):
super().__init__(parent)
self.view = QWidget(self)
self.treeWidget = tree
self.treeChildren = []
self.vBoxLayout = QVBoxLayout(self.view)
self.setWidget(self.view)
self.setWidgetResizable(True)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setStyleSheet("ScrollArea{border:none;background:transparent}")
self.view.setStyleSheet("QWidget{border:none;background:transparent}")
self.vBoxLayout.setSpacing(5)
self.vBoxLayout.setContentsMargins(5, 8, 5, 8)
# add nodes to menu
for child in tree.treeChildren:
node = child.clone()
node.expanded.connect(self._adjustViewSize)
self.treeChildren.append(node)
self.vBoxLayout.addWidget(node)
self._initNode(self)
self._adjustViewSize(False)
def _initNode(self, root: NavigationTreeWidget):
for c in root.treeChildren:
c.nodeDepth -= 1
c.setCompacted(False)
if c.isLeaf():
c.clicked.connect(self.window().fadeOut)
self._initNode(c)
def _adjustViewSize(self, emit=True):
w = self._suitableWidth()
# adjust the width of node
for node in self.visibleTreeNodes():
node.setFixedWidth(w - 10)
node.itemWidget.setFixedWidth(w - 10)
self.view.setFixedSize(w, self.view.sizeHint().height())
h = min(self.window().parent().height() - 48, self.view.height())
self.setFixedSize(w, h)
if emit:
self.expanded.emit()
def _suitableWidth(self):
w = 0
for node in self.visibleTreeNodes():
if not node.isHidden():
w = max(w, node.suitableWidth() + 10)
window = self.window().parent() # type: QWidget
return min(window.width() // 2 - 25, w) + 10
def visibleTreeNodes(self):
nodes = []
queue = deque()
queue.extend(self.treeChildren)
while queue:
node = queue.popleft() # type: NavigationTreeWidget
nodes.append(node)
queue.extend([i for i in node.treeChildren if not i.isHidden()])
return nodes

View File

@ -0,0 +1,272 @@
# coding:utf-8
from typing import Dict
from PySide6.QtCore import Qt, Signal, QRectF
from PySide6.QtGui import QPainter, QFont, QColor
from PySide6.QtWidgets import QApplication, QPushButton, QWidget, QHBoxLayout, QSizePolicy
from ...common.font import setFont
from ...common.router import qrouter
from ...common.style_sheet import themeColor, FluentStyleSheet
from ...common.color import autoFallbackThemeColor
from ...common.animation import FluentAnimation, FluentAnimationType, FluentAnimationProperty
from ..widgets.button import PushButton
from .navigation_panel import RouteKeyError
class PivotItem(PushButton):
""" Pivot item """
itemClicked = Signal(bool)
def _postInit(self):
self.isSelected = False
self.setProperty('isSelected', False)
self.clicked.connect(lambda: self.itemClicked.emit(True))
self.setAttribute(Qt.WA_LayoutUsesWidgetRect)
FluentStyleSheet.PIVOT.apply(self)
setFont(self, 18)
def setSelected(self, isSelected: bool):
if self.isSelected == isSelected:
return
self.isSelected = isSelected
self.setProperty('isSelected', isSelected)
self.setStyle(QApplication.style())
self.update()
class Pivot(QWidget):
""" Pivot """
currentItemChanged = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.items = {} # type: Dict[str, PivotItem]
self._currentRouteKey = None
self.lightIndicatorColor = QColor()
self.darkIndicatorColor = QColor()
self.hBoxLayout = QHBoxLayout(self)
self.slideAni = FluentAnimation.create(
FluentAnimationType.POINT_TO_POINT, FluentAnimationProperty.SCALE, value=0, parent=self)
FluentStyleSheet.PIVOT.apply(self)
self.hBoxLayout.setSpacing(0)
self.hBoxLayout.setAlignment(Qt.AlignLeft)
self.hBoxLayout.setContentsMargins(0, 0, 0, 0)
self.hBoxLayout.setSizeConstraint(QHBoxLayout.SetMinimumSize)
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
def addItem(self, routeKey: str, text: str, onClick=None, icon=None):
""" add item
Parameters
----------
routeKey: str
the unique name of item
text: str
the text of navigation item
onClick: callable
the slot connected to item clicked signal
icon: str
the icon of navigation item
"""
return self.insertItem(-1, routeKey, text, onClick, icon)
def addWidget(self, routeKey: str, widget: PivotItem, onClick=None):
""" add widget
Parameters
----------
routeKey: str
the unique name of item
widget: PivotItem
navigation widget
onClick: callable
the slot connected to item clicked signal
"""
self.insertWidget(-1, routeKey, widget, onClick)
def insertItem(self, index: int, routeKey: str, text: str, onClick=None, icon=None):
""" insert item
Parameters
----------
index: int
insert position
routeKey: str
the unique name of item
text: str
the text of navigation item
onClick: callable
the slot connected to item clicked signal
icon: str
the icon of navigation item
"""
if routeKey in self.items:
return
item = PivotItem(text, self)
if icon:
item.setIcon(icon)
self.insertWidget(index, routeKey, item, onClick)
return item
def insertWidget(self, index: int, routeKey: str, widget: PivotItem, onClick=None):
""" insert item
Parameters
----------
index: int
insert position
routeKey: str
the unique name of item
widget: PivotItem
navigation widget
onClick: callable
the slot connected to item clicked signal
"""
if routeKey in self.items:
return
widget.setProperty('routeKey', routeKey)
widget.itemClicked.connect(self._onItemClicked)
if onClick:
widget.itemClicked.connect(onClick)
self.items[routeKey] = widget
self.hBoxLayout.insertWidget(index, widget, 1)
def removeWidget(self, routeKey: str):
""" remove widget
Parameters
----------
routeKey: str
the unique name of item
"""
if routeKey not in self.items:
return
item = self.items.pop(routeKey)
self.hBoxLayout.removeWidget(item)
qrouter.remove(routeKey)
item.deleteLater()
if not self.items:
self._currentRouteKey = None
def clear(self):
""" clear all navigation items """
for k, w in self.items.items():
self.hBoxLayout.removeWidget(w)
qrouter.remove(k)
w.deleteLater()
self.items.clear()
self._currentRouteKey = None
def currentItem(self):
""" Returns the current selected item """
if self._currentRouteKey is None:
return None
return self.widget(self._currentRouteKey)
def currentRouteKey(self):
return self._currentRouteKey
def setCurrentItem(self, routeKey: str):
""" set current selected item
Parameters
----------
routeKey: str
the unique name of item
"""
if routeKey not in self.items or routeKey == self.currentRouteKey():
return
self._currentRouteKey = routeKey
self.slideAni.startAnimation(self.widget(routeKey).x())
for k, item in self.items.items():
item.setSelected(k == routeKey)
self.currentItemChanged.emit(routeKey)
def showEvent(self, e):
super().showEvent(e)
self._adjustIndicatorPos()
def setItemFontSize(self, size: int):
""" set the pixel font size of items """
for item in self.items.values():
font = item.font()
font.setPixelSize(size)
item.setFont(font)
item.adjustSize()
def setItemText(self, routeKey: str, text: str):
""" set the text of item """
item = self.widget(routeKey)
item.setText(text)
def setIndicatorColor(self, light, dark):
self.lightIndicatorColor = QColor(light)
self.darkIndicatorColor = QColor(dark)
self.update()
def _onItemClicked(self):
item = self.sender() # type: PivotItem
self.setCurrentItem(item.property('routeKey'))
def widget(self, routeKey: str):
if routeKey not in self.items:
raise RouteKeyError(f"`{routeKey}` is illegal.")
return self.items[routeKey]
def resizeEvent(self, e) -> None:
super().resizeEvent(e)
self._adjustIndicatorPos()
def _adjustIndicatorPos(self):
item = self.currentItem()
if item:
self.slideAni.stop()
self.slideAni.setValue(item.x())
def paintEvent(self, e):
super().paintEvent(e)
if not self.currentItem():
return
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing)
painter.setPen(Qt.NoPen)
painter.setBrush(autoFallbackThemeColor(self.lightIndicatorColor, self.darkIndicatorColor))
x = int(self.currentItem().width() / 2 - 8 + self.slideAni.value())
painter.drawRoundedRect(x, self.height() - 3, 16, 3, 1.5, 1.5)

View File

@ -0,0 +1,174 @@
# coding:utf-8
from typing import Union
from PySide6.QtCore import Qt, Signal, QRectF
from PySide6.QtGui import QPainter, QIcon, QColor
from PySide6.QtWidgets import QApplication, QWidget
from ...common.font import setFont
from ...common.icon import FluentIconBase, drawIcon, Theme
from ...common.color import autoFallbackThemeColor
from ...common.style_sheet import themeColor, FluentStyleSheet, isDarkTheme
from ..widgets.button import PushButton, ToolButton, TransparentToolButton
from .pivot import Pivot, PivotItem
class SegmentedItem(PivotItem):
""" Segmented item """
def _postInit(self):
super()._postInit()
setFont(self, 14)
class SegmentedToolItem(ToolButton):
""" Pivot item """
itemClicked = Signal(bool)
def _postInit(self):
self.isSelected = False
self.setProperty('isSelected', False)
self.clicked.connect(lambda: self.itemClicked.emit(True))
self.setFixedSize(38, 33)
FluentStyleSheet.PIVOT.apply(self)
def setSelected(self, isSelected: bool):
if self.isSelected == isSelected:
return
self.isSelected = isSelected
self.setProperty('isSelected', isSelected)
self.setStyle(QApplication.style())
self.update()
class SegmentedToggleToolItem(TransparentToolButton):
itemClicked = Signal(bool)
def _postInit(self):
super()._postInit()
self.isSelected = False
self.setFixedSize(50, 32)
self.clicked.connect(lambda: self.itemClicked.emit(True))
def setSelected(self, isSelected: bool):
if self.isSelected == isSelected:
return
self.isSelected = isSelected
self.setChecked(isSelected)
def _drawIcon(self, icon, painter: QPainter, rect: QRectF, state=QIcon.State.Off):
if self.isSelected and isinstance(icon, FluentIconBase):
theme = Theme.DARK if not isDarkTheme() else Theme.LIGHT
icon = icon.icon(theme)
return drawIcon(icon, painter, rect, state)
class SegmentedWidget(Pivot):
""" Segmented widget """
def __init__(self, parent=None):
super().__init__(parent)
self.setAttribute(Qt.WA_StyledBackground)
def insertItem(self, index: int, routeKey: str, text: str, onClick=None, icon=None):
if routeKey in self.items:
return
item = SegmentedItem(text, self)
if icon:
item.setIcon(icon)
self.insertWidget(index, routeKey, item, onClick)
return item
def paintEvent(self, e):
QWidget.paintEvent(self, e)
if not self.currentItem():
return
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing)
# draw background
if isDarkTheme():
painter.setPen(QColor(255, 255, 255, 14))
painter.setBrush(QColor(255, 255, 255, 15))
else:
painter.setPen(QColor(0, 0, 0, 19))
painter.setBrush(QColor(255, 255, 255, 179))
item = self.currentItem()
rect = item.rect().adjusted(1, 1, -1, -1).translated(int(self.slideAni.value()), 0)
painter.drawRoundedRect(rect, 5, 5)
# draw indicator
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(autoFallbackThemeColor(self.lightIndicatorColor, self.darkIndicatorColor))
x = int(self.currentItem().width() / 2 - 8 + self.slideAni.value())
painter.drawRoundedRect(QRectF(x, self.height() - 3.5, 16, 3), 1.5, 1.5)
class SegmentedToolWidget(SegmentedWidget):
""" Segmented tool widget """
def __init__(self, parent=None):
super().__init__(parent)
self.setAttribute(Qt.WA_StyledBackground)
def addItem(self, routeKey: str, icon: Union[str, QIcon, FluentIconBase], onClick=None):
""" add item
Parameters
----------
routeKey: str
the unique name of item
icon: str | QIcon | FluentIconBase
the icon of navigation item
onClick: callable
the slot connected to item clicked signal
"""
return self.insertItem(-1, routeKey, icon, onClick)
def insertItem(self, index: int, routeKey: str, icon: Union[str, QIcon, FluentIconBase], onClick=None):
if routeKey in self.items:
return
item = self._createItem(icon)
self.insertWidget(index, routeKey, item, onClick)
return item
def _createItem(self, icon):
return SegmentedToolItem(icon)
class SegmentedToggleToolWidget(SegmentedToolWidget):
""" Segmented toggle tool widget """
def _createItem(self, icon):
return SegmentedToggleToolItem(icon)
def paintEvent(self, e):
QWidget.paintEvent(self, e)
if not self.currentItem():
return
painter = QPainter(self)
painter.setRenderHints(QPainter.RenderHint.Antialiasing)
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(autoFallbackThemeColor(self.lightIndicatorColor, self.darkIndicatorColor))
item = self.currentItem()
painter.drawRoundedRect(
QRectF(self.slideAni.value(), 0, item.width(), item.height()), 4, 4)