You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

576 lines
18 KiB

# coding:utf-8
from typing import Iterable, List
from PySide6.QtCore import Qt, Signal, QSize, QRectF, QPoint, QPropertyAnimation, QEasingCurve, QObject
from PySide6.QtGui import QColor, QPainter, QCursor, QRegion
from PySide6.QtWidgets import (QApplication, QWidget, QFrame, QVBoxLayout, QHBoxLayout,
QGraphicsDropShadowEffect, QSizePolicy, QPushButton, QListWidgetItem)
from ..widgets.cycle_list_widget import CycleListWidget
from ..widgets.button import TransparentToolButton
from ...common.icon import FluentIcon
from ...common.style_sheet import FluentStyleSheet, themeColor, isDarkTheme
class SeparatorWidget(QWidget):
""" Separator widget """
def __init__(self, orient: Qt.Orientation, parent=None):
super().__init__(parent=parent)
if orient == Qt.Horizontal:
self.setFixedHeight(1)
else:
self.setFixedWidth(1)
self.setAttribute(Qt.WA_StyledBackground)
FluentStyleSheet.TIME_PICKER.apply(self)
class ItemMaskWidget(QWidget):
""" Item mask widget """
def __init__(self, listWidgets: List[CycleListWidget], parent=None):
super().__init__(parent=parent)
self.listWidgets = listWidgets
self.setFixedHeight(37)
FluentStyleSheet.TIME_PICKER.apply(self)
def paintEvent(self, e):
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing |
QPainter.TextAntialiasing)
# draw background
painter.setPen(Qt.NoPen)
painter.setBrush(themeColor())
painter.drawRoundedRect(self.rect().adjusted(4, 0, -3, 0), 5, 5)
# draw text
painter.setPen(Qt.black if isDarkTheme() else Qt.white)
painter.setFont(self.font())
w, h = 0, self.height()
for i, p in enumerate(self.listWidgets):
painter.save()
# draw first item's text
x = p.itemSize.width()//2 + 4 + self.x()
item1 = p.itemAt(QPoint(x, self.y() + 6))
if not item1:
painter.restore()
continue
iw = item1.sizeHint().width()
y = p.visualItemRect(item1).y()
painter.translate(w, y - self.y() + 7)
self._drawText(item1, painter, 0)
# draw second item's text
item2 = p.itemAt(self.pos() + QPoint(x, h - 6))
self._drawText(item2, painter, h)
painter.restore()
w += (iw + 8) # margin: 0 4px;
def _drawText(self, item: QListWidgetItem, painter: QPainter, y: int):
align = item.textAlignment()
w, h = item.sizeHint().width(), item.sizeHint().height()
if align & Qt.AlignLeft:
rect = QRectF(15, y, w, h) # padding-left: 11px
elif align & Qt.AlignRight:
rect = QRectF(4, y, w-15, h) # padding-right: 11px
elif align & Qt.AlignCenter:
rect = QRectF(4, y, w, h)
painter.drawText(rect, align, item.text())
class PickerColumnFormatter(QObject):
""" Picker column formatter """
def __init__(self):
super().__init__()
def encode(self, value):
""" convert original value to formatted value """
return str(value)
def decode(self, value: str):
""" convert formatted value to original value """
return str(value)
class DigitFormatter(PickerColumnFormatter):
""" Digit formatter """
def decode(self, value):
return int(value)
class PickerColumnButton(QPushButton):
""" Picker column button """
def __init__(self, name: str, items: Iterable, width: int, align=Qt.AlignLeft, formatter=None, parent=None):
super().__init__(text=name, parent=parent)
self._name = name
self._value = None # type: str
self.setItems(items)
self.setAlignment(align)
self.setFormatter(formatter)
self.setFixedSize(width, 30)
self.setObjectName('pickerButton')
self.setProperty('hasBorder', False)
self.setAttribute(Qt.WA_TransparentForMouseEvents)
def align(self):
return self._align
def setAlignment(self, align=Qt.AlignCenter):
""" set the text alignment """
if align == Qt.AlignLeft:
self.setProperty('align', 'left')
elif align == Qt.AlignRight:
self.setProperty('align', 'right')
else:
self.setProperty('align', 'center')
self._align = align
self.setStyle(QApplication.style())
def value(self) -> str:
if self._value is None:
return None
return self.formatter().encode(self._value)
def setValue(self, v):
self._value = v
if v is None:
self.setText(self.name())
self.setProperty('hasValue', False)
else:
self.setText(self.value())
self.setProperty('hasValue', True)
self.setStyle(QApplication.style())
def items(self):
return [self._formatter.encode(i) for i in self._items]
def setItems(self, items: Iterable):
self._items = list(items)
def formatter(self):
return self._formatter
def setFormatter(self, formatter):
self._formatter = formatter or PickerColumnFormatter()
def name(self):
return self._name
def setName(self, name: str):
if self.text() == self.name():
self.setText(name)
self._name = name
def checkColumnIndex(func):
""" check whether the index is out of range """
def wrapper(picker, index: int, *args, **kwargs):
if not 0 <= index < len(picker.columns):
return
return func(picker, index, *args, **kwargs)
return wrapper
class PickerBase(QPushButton):
""" Picker base class """
def __init__(self, parent=None):
super().__init__(parent=parent)
self.columns = [] # type: List[PickerColumnButton]
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.setSpacing(0)
self.hBoxLayout.setContentsMargins(0, 0, 0, 0)
self.hBoxLayout.setSizeConstraint(QHBoxLayout.SetFixedSize)
FluentStyleSheet.TIME_PICKER.apply(self)
self.clicked.connect(self._showPanel)
def addColumn(self, name: str, items: Iterable, width: int, align=Qt.AlignCenter,
formatter: PickerColumnFormatter = None):
""" add column
Parameters
----------
name: str
the name of column
items: Iterable
the items of column
width: int
the width of column
align: Qt.AlignmentFlag
the text alignment of button
formatter: PickerColumnFormatter
the formatter of column
"""
# create column button
button = PickerColumnButton(name, items, width, align, formatter, self)
self.columns.append(button)
self.hBoxLayout.addWidget(button, 0, Qt.AlignLeft)
# update the style of buttons
for btn in self.columns[:-1]:
btn.setProperty('hasBorder', True)
btn.setStyle(QApplication.style())
@checkColumnIndex
def setColumnAlignment(self, index: int, align=Qt.AlignCenter):
""" set the text alignment of specified column """
self.columns[index].setAlignment(align)
@checkColumnIndex
def setColumnWidth(self, index: int, width: int):
""" set the width of specified column """
self.columns[index].setFixedWidth(width)
@checkColumnIndex
def setColumnTight(self, index: int):
""" make the specified column to be tight """
fm = self.fontMetrics()
w = max(fm.width(i) for i in self.columns[index].items) + 30
self.setColumnWidth(index, w)
@checkColumnIndex
def setColumnVisible(self, index: int, isVisible: bool):
""" set the text alignment of specified column """
self.columns[index].setVisible(isVisible)
def value(self):
return [c.value() for c in self.columns if c.isVisible()]
def initialValue(self):
return [c.initialValue() for c in self.columns if c.isVisible()]
@checkColumnIndex
def setColumnValue(self, index: int, value):
self.columns[index].setValue(value)
@checkColumnIndex
def setColumnInitialValue(self, index: int, value):
self.columns[index].setInitialValue(value)
@checkColumnIndex
def setColumnFormatter(self, index: int, formatter: PickerColumnFormatter):
self.columns[index].setFormatter(formatter)
@checkColumnIndex
def setColumnItems(self, index: int, items: Iterable):
self.columns[index].setItems(items)
@checkColumnIndex
def encodeValue(self, index: int, value):
""" convert original value to formatted value """
return self.columns[index].formatter().encode(value)
@checkColumnIndex
def decodeValue(self, index: int, value):
""" convert formatted value to origin value """
return self.columns[index].formatter().decode(value)
@checkColumnIndex
def setColumn(self, index: int, name: str, items: Iterable, width: int, align=Qt.AlignCenter):
""" set column
Parameters
----------
index: int
the index of column
name: str
the name of column
items: Iterable
the items of column
width: int
the width of column
align: Qt.AlignmentFlag
the text alignment of button
"""
button = self.columns[index]
button.setText(name)
button.setFixedWidth(width)
button.setAlignment(align)
def clearColumns(self):
""" clear columns """
while self.columns:
btn = self.columns.pop()
self.hBoxLayout.removeWidget(btn)
btn.deleteLater()
def enterEvent(self, e):
self._setButtonProperty('enter', True)
def leaveEvent(self, e):
self._setButtonProperty('enter', False)
def mousePressEvent(self, e):
self._setButtonProperty('pressed', True)
super().mousePressEvent(e)
def mouseReleaseEvent(self, e):
self._setButtonProperty('pressed', False)
super().mouseReleaseEvent(e)
def _setButtonProperty(self, name, value):
""" send event to picker buttons """
for button in self.columns:
button.setProperty(name, value)
button.setStyle(QApplication.style())
def panelInitialValue(self):
""" initial value of panel """
return self.value()
def _showPanel(self):
""" show panel """
panel = PickerPanel(self)
for column in self.columns:
if column.isVisible():
panel.addColumn(column.items(), column.width(), column.align())
panel.setValue(self.panelInitialValue())
panel.confirmed.connect(self._onConfirmed)
panel.columnValueChanged.connect(
lambda i, v: self._onColumnValueChanged(panel, i, v))
w = panel.vBoxLayout.sizeHint().width() - self.width()
panel.exec(self.mapToGlobal(QPoint(-w//2, -37 * 4)))
def _onConfirmed(self, value: list):
for i, v in enumerate(value):
self.setColumnValue(i, v)
def _onColumnValueChanged(self, panel, index: int, value: str):
""" column value changed slot """
pass
class PickerToolButton(TransparentToolButton):
""" Picker tool button """
def _drawIcon(self, icon, painter, rect):
if self.isPressed:
painter.setOpacity(1)
super()._drawIcon(icon, painter, rect)
class PickerPanel(QWidget):
""" picker panel """
confirmed = Signal(list)
columnValueChanged = Signal(int, str)
def __init__(self, parent=None):
super().__init__(parent=parent)
self.itemHeight = 37
self.listWidgets = [] # type: List[CycleListWidget]
self.view = QFrame(self)
self.itemMaskWidget = ItemMaskWidget(self.listWidgets, self)
self.hSeparatorWidget = SeparatorWidget(Qt.Horizontal, self.view)
self.yesButton = PickerToolButton(FluentIcon.ACCEPT, self.view)
self.cancelButton = PickerToolButton(FluentIcon.CLOSE, self.view)
self.hBoxLayout = QHBoxLayout(self)
self.listLayout = QHBoxLayout()
self.buttonLayout = QHBoxLayout()
self.vBoxLayout = QVBoxLayout(self.view)
self.__initWidget()
def __initWidget(self):
self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint |
Qt.NoDropShadowWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setShadowEffect()
self.yesButton.setIconSize(QSize(16, 16))
self.cancelButton.setIconSize(QSize(13, 13))
self.yesButton.setFixedHeight(33)
self.cancelButton.setFixedHeight(33)
self.hBoxLayout.setContentsMargins(12, 8, 12, 20)
self.hBoxLayout.addWidget(self.view, 1, Qt.AlignCenter)
self.hBoxLayout.setSizeConstraint(QHBoxLayout.SetMinimumSize)
self.vBoxLayout.setSpacing(0)
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
self.vBoxLayout.addLayout(self.listLayout, 1)
self.vBoxLayout.addWidget(self.hSeparatorWidget)
self.vBoxLayout.addLayout(self.buttonLayout, 1)
self.vBoxLayout.setSizeConstraint(QVBoxLayout.SetMinimumSize)
self.buttonLayout.setSpacing(6)
self.buttonLayout.setContentsMargins(3, 3, 3, 3)
self.buttonLayout.addWidget(self.yesButton)
self.buttonLayout.addWidget(self.cancelButton)
self.yesButton.setSizePolicy(
QSizePolicy.Expanding, QSizePolicy.Expanding)
self.cancelButton.setSizePolicy(
QSizePolicy.Expanding, QSizePolicy.Expanding)
self.yesButton.clicked.connect(self._fadeOut)
self.yesButton.clicked.connect(
lambda: self.confirmed.emit(self.value()))
self.cancelButton.clicked.connect(self._fadeOut)
self.view.setObjectName('view')
FluentStyleSheet.TIME_PICKER.apply(self)
def setShadowEffect(self, blurRadius=30, offset=(0, 8), color=QColor(0, 0, 0, 30)):
""" add shadow to dialog """
self.shadowEffect = QGraphicsDropShadowEffect(self.view)
self.shadowEffect.setBlurRadius(blurRadius)
self.shadowEffect.setOffset(*offset)
self.shadowEffect.setColor(color)
self.view.setGraphicsEffect(None)
self.view.setGraphicsEffect(self.shadowEffect)
def addColumn(self, items: Iterable, width: int, align=Qt.AlignCenter):
""" add one column to view
Parameters
----------
items: Iterable[Any]
the items to be added
width: int
the width of item
align: Qt.AlignmentFlag
the text alignment of item
"""
if self.listWidgets:
self.listLayout.addWidget(SeparatorWidget(Qt.Vertical))
w = CycleListWidget(items, QSize(width, self.itemHeight), align, self)
w.vScrollBar.valueChanged.connect(self.itemMaskWidget.update)
N = len(self.listWidgets)
w.currentItemChanged.connect(
lambda i, n=N: self.columnValueChanged.emit(n, i.text()))
self.listWidgets.append(w)
self.listLayout.addWidget(w)
def resizeEvent(self, e):
self.itemMaskWidget.resize(self.view.width()-3, self.itemHeight)
m = self.hBoxLayout.contentsMargins()
self.itemMaskWidget.move(m.left()+2, m.top() + 148)
def value(self):
""" return the value of columns """
return [i.currentItem().text() for i in self.listWidgets]
def setValue(self, value: list):
""" set the value of columns """
if len(value) != len(self.listWidgets):
return
for v, w in zip(value, self.listWidgets):
w.setSelectedItem(v)
def columnValue(self, index: int) -> str:
""" return the value of specified column """
if not 0 <= index < len(self.listWidgets):
return
return self.listWidgets[index].currentItem().text()
def setColumnValue(self, index: int, value: str):
""" set the value of specified column """
if not 0 <= index < len(self.listWidgets):
return
self.listWidgets[index].setSelectedItem(value)
def column(self, index: int):
""" return the list widget of specified column """
return self.listWidgets[index]
def exec(self, pos, ani=True):
""" show panel
Parameters
----------
pos: QPoint
pop-up position
ani: bool
Whether to show pop-up animation
"""
if self.isVisible():
return
# show before running animation, or the height calculation will be wrong
self.show()
rect = QApplication.screenAt(QCursor.pos()).availableGeometry()
w, h = self.width() + 5, self.height()
pos.setX(
min(pos.x() - self.layout().contentsMargins().left(), rect.right() - w))
pos.setY(max(rect.top(), min(pos.y() - 4, rect.bottom() - h + 5)))
self.move(pos)
if not ani:
return
self.isExpanded = False
self.ani = QPropertyAnimation(self.view, b'windowOpacity', self)
self.ani.valueChanged.connect(self._onAniValueChanged)
self.ani.setStartValue(0)
self.ani.setEndValue(1)
self.ani.setDuration(150)
self.ani.setEasingCurve(QEasingCurve.OutQuad)
self.ani.start()
def _onAniValueChanged(self, opacity):
m = self.layout().contentsMargins()
w = self.view.width() + m.left() + m.right() + 120
h = self.view.height() + m.top() + m.bottom() + 12
if not self.isExpanded:
y = int(h / 2 * (1 - opacity))
self.setMask(QRegion(0, y, w, h-y*2))
else:
y = int(h / 3 * (1 - opacity))
self.setMask(QRegion(0, y, w, h-y*2))
def _fadeOut(self):
self.isExpanded = True
self.ani = QPropertyAnimation(self, b'windowOpacity', self)
self.ani.valueChanged.connect(self._onAniValueChanged)
self.ani.finished.connect(self.deleteLater)
self.ani.setStartValue(1)
self.ani.setEndValue(0)
self.ani.setDuration(150)
self.ani.setEasingCurve(QEasingCurve.OutQuad)
self.ani.start()