mirror of
https://github.com/283375/arcaea-offline-pyside-ui.git
synced 2025-04-21 10:10:17 +00:00
203 lines
7.2 KiB
Python
203 lines
7.2 KiB
Python
from enum import IntEnum
|
|
from typing import Callable, Literal
|
|
|
|
from PySide6.QtCore import QModelIndex, QPoint, QSize, Qt
|
|
from PySide6.QtGui import QBrush, QColor, QFont, QFontMetrics, QLinearGradient, QPainter
|
|
from PySide6.QtWidgets import QStyledItemDelegate, QStyleOptionViewItem
|
|
|
|
|
|
class TextSegmentDelegateVerticalAlign(IntEnum):
|
|
Top = 0
|
|
Middle = 1
|
|
Bottom = 2
|
|
|
|
|
|
class TextSegmentDelegate(QStyledItemDelegate):
|
|
VerticalPadding = 3
|
|
HorizontalPadding = 5
|
|
|
|
TextRole = 3375
|
|
ColorRole = TextRole + 1
|
|
BrushRole = TextRole + 2
|
|
GradientWrapperRole = TextRole + 3
|
|
FontRole = TextRole + 20
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
|
|
self.baseXOffsets: dict[str, int] = {}
|
|
self.baseYOffsets: dict[str, int] = {}
|
|
|
|
self.verticalAlign = TextSegmentDelegateVerticalAlign.Middle
|
|
|
|
def indexOffsetKey(self, index: QModelIndex):
|
|
return f"{index.row()},{index.column()}"
|
|
|
|
def setBaseXOffset(self, index: QModelIndex, offset: int):
|
|
key = self.indexOffsetKey(index)
|
|
if not offset:
|
|
self.baseXOffsets.pop(key, None)
|
|
else:
|
|
self.baseXOffsets[key] = offset
|
|
|
|
def setBaseYOffset(self, index: QModelIndex, offset: int):
|
|
key = self.indexOffsetKey(index)
|
|
if not offset:
|
|
self.baseYOffsets.pop(key, None)
|
|
else:
|
|
self.baseYOffsets[key] = offset
|
|
|
|
def setVerticalAlign(self, align: Literal["top", "middle", "bottom"]):
|
|
if not isinstance(align, str) and align not in ["top", "middle", "bottom"]:
|
|
raise ValueError(
|
|
"TextSegment only supports top/middle/bottom vertical aligning."
|
|
)
|
|
|
|
if align == "top":
|
|
self.verticalAlign = TextSegmentDelegateVerticalAlign.Top
|
|
elif align == "middle":
|
|
self.verticalAlign = TextSegmentDelegateVerticalAlign.Middle
|
|
elif align == "bottom":
|
|
self.verticalAlign = TextSegmentDelegateVerticalAlign.Bottom
|
|
|
|
def getTextSegments(
|
|
self, index: QModelIndex, option
|
|
) -> list[
|
|
list[
|
|
dict[
|
|
int,
|
|
str
|
|
| QColor
|
|
| QBrush
|
|
| Callable[[float, float, float, float], QLinearGradient]
|
|
| QFont,
|
|
]
|
|
]
|
|
]:
|
|
return []
|
|
|
|
def textsSizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
|
|
width = 0
|
|
height = 0
|
|
fm: QFontMetrics = option.fontMetrics
|
|
segments = self.getTextSegments(index, option)
|
|
for i in range(len(segments)):
|
|
line = segments[i]
|
|
lineWidth = 2 * self.HorizontalPadding
|
|
lineHeight = 0
|
|
for textFrag in line:
|
|
font = textFrag.get(self.FontRole)
|
|
_fm = QFontMetrics(font) if font else fm
|
|
text = textFrag[self.TextRole]
|
|
textWidth = _fm.horizontalAdvance(text)
|
|
textHeight = _fm.height()
|
|
lineWidth += textWidth
|
|
lineHeight = max(lineHeight, textHeight)
|
|
width = max(lineWidth, width)
|
|
height += lineHeight
|
|
if i != len(segments) - 1:
|
|
height += self.VerticalPadding
|
|
return QSize(width, height)
|
|
|
|
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
|
|
width = self.HorizontalPadding * 2
|
|
height = self.VerticalPadding * 2
|
|
textsSizeHint = self.textsSizeHint(option, index)
|
|
return QSize(textsSizeHint.width() + width, textsSizeHint.height() + height)
|
|
|
|
def baseX(self, option: QStyleOptionViewItem, index: QModelIndex):
|
|
return (
|
|
option.rect.x()
|
|
+ self.HorizontalPadding
|
|
+ self.baseXOffsets.get(self.indexOffsetKey(index), 0)
|
|
)
|
|
|
|
def baseY(self, option: QStyleOptionViewItem, index: QModelIndex):
|
|
baseY = (
|
|
option.rect.y()
|
|
+ self.VerticalPadding
|
|
+ self.baseYOffsets.get(self.indexOffsetKey(index), 0)
|
|
)
|
|
if self.verticalAlign != TextSegmentDelegateVerticalAlign.Top:
|
|
paintAreaSize: QSize = option.rect.size()
|
|
delegateSize = self.sizeHint(option, index)
|
|
if self.verticalAlign == TextSegmentDelegateVerticalAlign.Middle:
|
|
baseY += round((paintAreaSize.height() - delegateSize.height()) / 2)
|
|
elif self.verticalAlign == TextSegmentDelegateVerticalAlign.Bottom:
|
|
baseY += paintAreaSize.height() - delegateSize.height()
|
|
return baseY
|
|
|
|
def textMaxWidth(self, option: QStyleOptionViewItem, index: QModelIndex):
|
|
return (
|
|
option.rect.width()
|
|
- (2 * self.HorizontalPadding)
|
|
- self.baseXOffsets.get(self.indexOffsetKey(index), 0)
|
|
)
|
|
|
|
def paint(
|
|
self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex
|
|
):
|
|
self.initStyleOption(option, index)
|
|
|
|
baseX = self.baseX(option, index)
|
|
baseY = self.baseY(option, index)
|
|
maxWidth = self.textMaxWidth(option, index)
|
|
|
|
fm: QFontMetrics = option.fontMetrics
|
|
painter.save()
|
|
for line in self.getTextSegments(index, option):
|
|
lineBaseX = baseX
|
|
lineBaseY = baseY
|
|
lineHeight = 0
|
|
for textFrag in line:
|
|
painter.save()
|
|
# elide text, get font values
|
|
text = textFrag[self.TextRole]
|
|
fragMaxWidth = maxWidth - (lineBaseX - baseX)
|
|
if font := textFrag.get(self.FontRole):
|
|
painter.setFont(font)
|
|
_fm = QFontMetrics(font)
|
|
else:
|
|
_fm = fm
|
|
lineHeight = max(lineHeight, _fm.height())
|
|
elidedText = _fm.elidedText(
|
|
text, Qt.TextElideMode.ElideRight, fragMaxWidth
|
|
)
|
|
|
|
# confirm proper color
|
|
brush = textFrag.get(self.BrushRole)
|
|
gradientWrapper = textFrag.get(self.GradientWrapperRole)
|
|
color = textFrag.get(self.ColorRole)
|
|
pen = painter.pen()
|
|
if brush:
|
|
pen.setBrush(brush)
|
|
elif gradientWrapper:
|
|
gradient = gradientWrapper(
|
|
lineBaseX,
|
|
lineBaseY + lineHeight - _fm.height(),
|
|
fragMaxWidth,
|
|
_fm.height(),
|
|
)
|
|
pen.setBrush(gradient)
|
|
elif color:
|
|
pen.setColor(color)
|
|
painter.setPen(pen)
|
|
|
|
painter.drawText(
|
|
QPoint(lineBaseX, lineBaseY + lineHeight - _fm.descent()),
|
|
elidedText,
|
|
)
|
|
painter.restore()
|
|
|
|
# if text elided, skip to next line
|
|
# remember to add height before skipping
|
|
if _fm.boundingRect(text).width() >= fragMaxWidth:
|
|
break
|
|
lineBaseX += _fm.horizontalAdvance(elidedText)
|
|
|
|
baseY += lineHeight + self.VerticalPadding
|
|
painter.restore()
|
|
|
|
def super_styledItemDelegate_paint(self, painter, option, index):
|
|
return super().paint(painter, option, index)
|