initial fluent-widgets ui
This commit is contained in:
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)
|
||||
Reference in New Issue
Block a user