initial fluent-widgets ui
This commit is contained in:
9
qfluentwidgets/components/navigation/__init__.py
Normal file
9
qfluentwidgets/components/navigation/__init__.py
Normal 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
|
||||
350
qfluentwidgets/components/navigation/breadcrumb.py
Normal file
350
qfluentwidgets/components/navigation/breadcrumb.py
Normal 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)
|
||||
416
qfluentwidgets/components/navigation/navigation_bar.py
Normal file
416
qfluentwidgets/components/navigation/navigation_bar.py
Normal 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'))
|
||||
268
qfluentwidgets/components/navigation/navigation_interface.py
Normal file
268
qfluentwidgets/components/navigation/navigation_interface.py
Normal 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())
|
||||
658
qfluentwidgets/components/navigation/navigation_panel.py
Normal file
658
qfluentwidgets/components/navigation/navigation_panel.py
Normal 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())
|
||||
|
||||
695
qfluentwidgets/components/navigation/navigation_widget.py
Normal file
695
qfluentwidgets/components/navigation/navigation_widget.py
Normal 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
|
||||
272
qfluentwidgets/components/navigation/pivot.py
Normal file
272
qfluentwidgets/components/navigation/pivot.py
Normal 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)
|
||||
174
qfluentwidgets/components/navigation/segmented_widget.py
Normal file
174
qfluentwidgets/components/navigation/segmented_widget.py
Normal 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)
|
||||
Reference in New Issue
Block a user