Files
fluent_widgets_pyside6/qfluentwidgets/components/navigation/navigation_bar.py
2025-08-14 18:45:16 +08:00

417 lines
13 KiB
Python

# 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'))