350 lines
9.7 KiB
Python
350 lines
9.7 KiB
Python
# 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) |