488 lines
16 KiB
Python
488 lines
16 KiB
Python
|
|
# coding: utf-8
|
||
|
|
from math import ceil
|
||
|
|
from collections import defaultdict, Counter
|
||
|
|
from typing import Type
|
||
|
|
|
||
|
|
from PySide6.QtCore import Qt, Signal, QSize, QDate, QCalendar, QLocale
|
||
|
|
from PySide6.QtGui import QPainter, QColor
|
||
|
|
from PySide6.QtWidgets import QHBoxLayout, QListWidgetItem, QLabel, QWidget, QStackedWidget, QStyle
|
||
|
|
|
||
|
|
from ..widgets.flyout import FlyoutViewBase
|
||
|
|
from ...common.style_sheet import isDarkTheme, themeColor, ThemeColor
|
||
|
|
|
||
|
|
|
||
|
|
from .calendar_view import (ScrollItemDelegate, ScrollViewBase,
|
||
|
|
CalendarViewBase)
|
||
|
|
|
||
|
|
|
||
|
|
class FastScrollItemDelegate(ScrollItemDelegate):
|
||
|
|
""" Fast scroll item delegate """
|
||
|
|
|
||
|
|
def __init__(self, min, max):
|
||
|
|
super().__init__(min, max)
|
||
|
|
self.selectedDate = None
|
||
|
|
self.currentDate = QDate.currentDate()
|
||
|
|
|
||
|
|
def setSelectedDate(self, date: QDate):
|
||
|
|
self.selectedDate = date
|
||
|
|
|
||
|
|
def setCurrentDate(self, date: QDate):
|
||
|
|
self.currentDate = date
|
||
|
|
|
||
|
|
def _drawBackground(self, painter, option, index):
|
||
|
|
date = index.data(Qt.UserRole)
|
||
|
|
if not date:
|
||
|
|
return
|
||
|
|
|
||
|
|
painter.save()
|
||
|
|
|
||
|
|
# outer ring
|
||
|
|
if date != self.selectedDate:
|
||
|
|
painter.setPen(Qt.PenStyle.NoPen)
|
||
|
|
else:
|
||
|
|
painter.setPen(themeColor())
|
||
|
|
|
||
|
|
if date == self.currentDate:
|
||
|
|
if index == self.pressedIndex:
|
||
|
|
painter.setBrush(ThemeColor.LIGHT_2.color())
|
||
|
|
elif option.state & QStyle.StateFlag.State_MouseOver:
|
||
|
|
painter.setBrush(ThemeColor.LIGHT_1.color())
|
||
|
|
else:
|
||
|
|
painter.setBrush(themeColor())
|
||
|
|
else:
|
||
|
|
c = 255 if isDarkTheme() else 0
|
||
|
|
if index == self.pressedIndex:
|
||
|
|
painter.setBrush(QColor(c, c, c, 7))
|
||
|
|
elif option.state & QStyle.StateFlag.State_MouseOver:
|
||
|
|
painter.setBrush(QColor(c, c, c, 9))
|
||
|
|
else:
|
||
|
|
painter.setBrush(Qt.GlobalColor.transparent)
|
||
|
|
|
||
|
|
m = self._itemMargin()
|
||
|
|
painter.drawEllipse(option.rect.adjusted(m, m, -m, -m))
|
||
|
|
painter.restore()
|
||
|
|
|
||
|
|
def _drawText(self, painter, option, index):
|
||
|
|
date = index.data(Qt.UserRole)
|
||
|
|
if not date:
|
||
|
|
return
|
||
|
|
|
||
|
|
painter.save()
|
||
|
|
painter.setFont(self.font)
|
||
|
|
|
||
|
|
if date == self.currentDate:
|
||
|
|
c = 0 if isDarkTheme() else 255
|
||
|
|
painter.setPen(QColor(c, c, c))
|
||
|
|
else:
|
||
|
|
painter.setPen(Qt.white if isDarkTheme() else Qt.black)
|
||
|
|
if not (self.min <= date <= self.max or option.state & QStyle.State_MouseOver) or \
|
||
|
|
index == self.pressedIndex:
|
||
|
|
painter.setOpacity(0.6)
|
||
|
|
|
||
|
|
text = index.data(Qt.DisplayRole)
|
||
|
|
painter.drawText(option.rect, Qt.AlignCenter, text)
|
||
|
|
painter.restore()
|
||
|
|
|
||
|
|
|
||
|
|
class FastYearScrollItemDelegate(FastScrollItemDelegate):
|
||
|
|
""" Year scroll item delegate """
|
||
|
|
|
||
|
|
def _itemMargin(self):
|
||
|
|
return 8
|
||
|
|
|
||
|
|
|
||
|
|
class FastDayScrollItemDelegate(FastScrollItemDelegate):
|
||
|
|
""" Fast day scroll item delegate """
|
||
|
|
|
||
|
|
def _itemMargin(self):
|
||
|
|
return 3
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
class FastScrollViewBase(ScrollViewBase):
|
||
|
|
""" Scroll view base class """
|
||
|
|
|
||
|
|
pageChanged = Signal(int)
|
||
|
|
|
||
|
|
def __init__(self, Delegate: Type[FastScrollItemDelegate], parent=None):
|
||
|
|
super().__init__(Delegate, parent)
|
||
|
|
self.delegate.setRange(*self.currentPageRange())
|
||
|
|
self.delegate.setCurrentDate(self.currentDate)
|
||
|
|
|
||
|
|
def scrollToPage(self, page: int):
|
||
|
|
if not 0 <= page < self.pageCount():
|
||
|
|
return
|
||
|
|
|
||
|
|
self.currentPage = page
|
||
|
|
self._updateItems()
|
||
|
|
self.delegate.setRange(*self.currentPageRange())
|
||
|
|
self.pageChanged.emit(page)
|
||
|
|
|
||
|
|
def wheelEvent(self, e):
|
||
|
|
pass
|
||
|
|
|
||
|
|
def _updateItems(self):
|
||
|
|
""" update the items of current page """
|
||
|
|
pass
|
||
|
|
|
||
|
|
def pageCount(self):
|
||
|
|
return ceil((self.maxYear - self.minYear + 1) / (self.pageRows * self.cols))
|
||
|
|
|
||
|
|
def pageSize(self):
|
||
|
|
return self.pageRows * self.cols
|
||
|
|
|
||
|
|
def _setSelectedDate(self, date: QDate):
|
||
|
|
self.delegate.setSelectedDate(date)
|
||
|
|
self.viewport().update()
|
||
|
|
|
||
|
|
|
||
|
|
class FastYearScrollView(FastScrollViewBase):
|
||
|
|
""" Year scroll view """
|
||
|
|
|
||
|
|
def __init__(self, parent=None):
|
||
|
|
super().__init__(FastYearScrollItemDelegate, parent)
|
||
|
|
self.delegate.setCurrentDate(QDate(self.currentDate.year(), 1, 1))
|
||
|
|
|
||
|
|
def _initItems(self):
|
||
|
|
self.years = list(range(self.minYear, self.maxYear+1))
|
||
|
|
count = self.cols * self.cols
|
||
|
|
years = self.years[:count]
|
||
|
|
self.addItems([str(i) for i in years])
|
||
|
|
|
||
|
|
for i, year in enumerate(years):
|
||
|
|
item = self.item(i)
|
||
|
|
item.setData(Qt.ItemDataRole.UserRole, QDate(year, 1, 1))
|
||
|
|
item.setSizeHint(self.sizeHint())
|
||
|
|
|
||
|
|
def scrollToDate(self, date: QDate):
|
||
|
|
page = (date.year() - self._startYear()) // 10
|
||
|
|
self.scrollToPage(page)
|
||
|
|
|
||
|
|
def currentPageRange(self):
|
||
|
|
year = self.currentPage * 10 + self._startYear()
|
||
|
|
return QDate(year, 1, 1), QDate(year + 9, 1, 1)
|
||
|
|
|
||
|
|
def _startYear(self):
|
||
|
|
if self.minYear % 10 <= 2:
|
||
|
|
return self.minYear - self.minYear % 10
|
||
|
|
|
||
|
|
return self.minYear - self.minYear % 10 + 10
|
||
|
|
|
||
|
|
def _updateItems(self):
|
||
|
|
start, _ = self.currentPageRange()
|
||
|
|
index = (start.year() - self.minYear) % 4
|
||
|
|
left = start.year() - index
|
||
|
|
right = left + 16
|
||
|
|
|
||
|
|
for i, year in enumerate(range(left, right)):
|
||
|
|
item = self.item(i)
|
||
|
|
item.setText(str(year))
|
||
|
|
item.setData(Qt.ItemDataRole.UserRole, QDate(year, 1, 1))
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
class FastMonthScrollView(FastScrollViewBase):
|
||
|
|
""" Month scroll view """
|
||
|
|
|
||
|
|
def __init__(self, parent=None):
|
||
|
|
super().__init__(FastYearScrollItemDelegate, parent)
|
||
|
|
self.delegate.setCurrentDate(QDate(self.currentDate.year(), self.currentDate.month(), 1))
|
||
|
|
|
||
|
|
def _initItems(self):
|
||
|
|
self.months = [
|
||
|
|
self.tr('Jan'), self.tr('Feb'), self.tr('Mar'), self.tr('Apr'),
|
||
|
|
self.tr('May'), self.tr('Jun'), self.tr('Jul'), self.tr('Aug'),
|
||
|
|
self.tr('Sep'), self.tr('Oct'), self.tr('Nov'), self.tr('Dec'),
|
||
|
|
self.tr('Jan'), self.tr('Feb'), self.tr('Mar'), self.tr('Apr'),
|
||
|
|
]
|
||
|
|
self.addItems(self.months)
|
||
|
|
|
||
|
|
# add month items
|
||
|
|
for i in range(len(self.months)):
|
||
|
|
year = i // 12 + self.minYear
|
||
|
|
m = i % 12 + 1
|
||
|
|
item = self.item(i)
|
||
|
|
item.setData(Qt.ItemDataRole.UserRole, QDate(year, m, 1))
|
||
|
|
item.setSizeHint(self.gridSize())
|
||
|
|
|
||
|
|
if year == self.currentDate.year() and m == self.currentDate.month():
|
||
|
|
self.delegate.setCurrentIndex(self.indexFromItem(item))
|
||
|
|
|
||
|
|
def scrollToDate(self, date: QDate):
|
||
|
|
page = date.year() - self.minYear
|
||
|
|
self.scrollToPage(page)
|
||
|
|
|
||
|
|
def currentPageRange(self):
|
||
|
|
year = self.minYear + self.currentPage
|
||
|
|
return QDate(year, 1, 1), QDate(year, 12, 31)
|
||
|
|
|
||
|
|
def pageCount(self):
|
||
|
|
return (self.maxYear - self.minYear + 1) * 12
|
||
|
|
|
||
|
|
def _updateItems(self):
|
||
|
|
year = self.minYear + self.currentPage
|
||
|
|
|
||
|
|
for i in range(16):
|
||
|
|
m = i % 12 + 1
|
||
|
|
y = year + (i > 11)
|
||
|
|
self.item(i).setData(Qt.ItemDataRole.UserRole, QDate(y, m, 1))
|
||
|
|
|
||
|
|
|
||
|
|
class FastDayScrollView(FastScrollViewBase):
|
||
|
|
""" Day scroll view """
|
||
|
|
|
||
|
|
def __init__(self, parent=None):
|
||
|
|
super().__init__(FastDayScrollItemDelegate, parent)
|
||
|
|
self.vBoxLayout = QHBoxLayout(self)
|
||
|
|
|
||
|
|
# add week day labels
|
||
|
|
self.weekDays = [
|
||
|
|
self.tr('Mo'), self.tr('Tu'), self.tr('We'),
|
||
|
|
self.tr('Th'), self.tr('Fr'), self.tr('Sa'), self.tr('Su')
|
||
|
|
]
|
||
|
|
self.weekDayGroup = QWidget(self)
|
||
|
|
self.weekDayLayout = QHBoxLayout(self.weekDayGroup)
|
||
|
|
self.weekDayGroup.setObjectName('weekDayGroup')
|
||
|
|
for day in self.weekDays:
|
||
|
|
label = QLabel(day)
|
||
|
|
label.setObjectName('weekDayLabel')
|
||
|
|
self.weekDayLayout.addWidget(label, 1, Qt.AlignHCenter)
|
||
|
|
|
||
|
|
self.setViewportMargins(0, 38, 0, 0)
|
||
|
|
self.vBoxLayout.setAlignment(Qt.AlignTop)
|
||
|
|
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
|
||
|
|
self.weekDayLayout.setSpacing(0)
|
||
|
|
self.weekDayLayout.setContentsMargins(3, 12, 3, 12)
|
||
|
|
self.vBoxLayout.addWidget(self.weekDayGroup)
|
||
|
|
|
||
|
|
def gridSize(self) -> QSize:
|
||
|
|
return QSize(44, 44)
|
||
|
|
|
||
|
|
def _initItems(self):
|
||
|
|
self.cols = 7
|
||
|
|
self.pageRows = 6
|
||
|
|
|
||
|
|
startDate = QDate(self.minYear, 1, 1)
|
||
|
|
currentDate = startDate
|
||
|
|
|
||
|
|
# add placeholder
|
||
|
|
bias = currentDate.dayOfWeek() - 1
|
||
|
|
for i in range(bias):
|
||
|
|
item = QListWidgetItem(self)
|
||
|
|
item.setFlags(Qt.ItemFlag.NoItemFlags)
|
||
|
|
self.addItem(item)
|
||
|
|
|
||
|
|
# add day items
|
||
|
|
items, dates = [], []
|
||
|
|
endDate = startDate.addDays(self.pageSize() - bias)
|
||
|
|
while currentDate < endDate:
|
||
|
|
items.append(str(currentDate.day()))
|
||
|
|
dates.append(QDate(currentDate))
|
||
|
|
currentDate = currentDate.addDays(1)
|
||
|
|
|
||
|
|
self.addItems(items)
|
||
|
|
for i in range(bias, self.count()):
|
||
|
|
item = self.item(i)
|
||
|
|
item.setData(Qt.ItemDataRole.UserRole, dates[i-bias])
|
||
|
|
item.setSizeHint(self.gridSize())
|
||
|
|
|
||
|
|
self.delegate.setCurrentIndex(
|
||
|
|
self.model().index(self._dateToRow(self.currentDate)))
|
||
|
|
|
||
|
|
def setDate(self, date: QDate):
|
||
|
|
self.scrollToDate(date)
|
||
|
|
self.delegate.setSelectedDate(date)
|
||
|
|
|
||
|
|
def scrollToDate(self, date: QDate):
|
||
|
|
page = (date.year() - self.minYear) * 12 + date.month() - 1
|
||
|
|
self.scrollToPage(page)
|
||
|
|
|
||
|
|
def currentPageRange(self):
|
||
|
|
date = self._pageToDate()
|
||
|
|
return date, date.addMonths(1).addDays(-1)
|
||
|
|
|
||
|
|
def _pageToDate(self):
|
||
|
|
year = self.currentPage // 12 + self.minYear
|
||
|
|
month = self.currentPage % 12 + 1
|
||
|
|
return QDate(year, month, 1)
|
||
|
|
|
||
|
|
def _dateToRow(self, date: QDate):
|
||
|
|
startDate = QDate(self.minYear, 1, 1)
|
||
|
|
days = startDate.daysTo(date)
|
||
|
|
return days + startDate.dayOfWeek() - 1
|
||
|
|
|
||
|
|
def _updateItems(self):
|
||
|
|
startDate = QDate(self.minYear, 1, 1)
|
||
|
|
bias = startDate.dayOfWeek() - 1
|
||
|
|
|
||
|
|
left = startDate.addMonths(self.currentPage)
|
||
|
|
left = left.addDays(-left.dayOfWeek() + 1)
|
||
|
|
right = left.addDays(self.pageRows * self.cols)
|
||
|
|
|
||
|
|
if self.currentPage == 0:
|
||
|
|
for i in range(bias):
|
||
|
|
self.item(i).setText("")
|
||
|
|
self.item(i).setFlags(Qt.ItemFlag.NoItemFlags)
|
||
|
|
|
||
|
|
currentDate = left
|
||
|
|
for i in range(left.daysTo(right)):
|
||
|
|
item = self.item(i + bias if self.currentPage == 0 else i)
|
||
|
|
if item:
|
||
|
|
item.setText(str(currentDate.day()))
|
||
|
|
item.setData(Qt.ItemDataRole.UserRole, currentDate)
|
||
|
|
currentDate = currentDate.addDays(1)
|
||
|
|
|
||
|
|
def mouseReleaseEvent(self, e):
|
||
|
|
super().mouseReleaseEvent(e)
|
||
|
|
item = self.currentItem()
|
||
|
|
if item:
|
||
|
|
self._setSelectedDate(item.data(Qt.ItemDataRole.UserRole))
|
||
|
|
|
||
|
|
def pageCount(self):
|
||
|
|
return (self.maxYear - self.minYear + 1) * 12
|
||
|
|
|
||
|
|
|
||
|
|
class FastYearCalendarView(CalendarViewBase):
|
||
|
|
""" Year calendar view """
|
||
|
|
|
||
|
|
def __init__(self, parent=None):
|
||
|
|
super().__init__(parent)
|
||
|
|
self.setScrollView(FastYearScrollView(self))
|
||
|
|
self.titleButton.setEnabled(False)
|
||
|
|
|
||
|
|
def _updateTitle(self):
|
||
|
|
left, right = self.scrollView.currentPageRange()
|
||
|
|
self.setTitle(f'{left.year()} - {right.year()}')
|
||
|
|
|
||
|
|
|
||
|
|
class FastMonthCalendarView(CalendarViewBase):
|
||
|
|
""" Month calendar view """
|
||
|
|
|
||
|
|
def __init__(self, parent=None):
|
||
|
|
super().__init__(parent)
|
||
|
|
self.setScrollView(FastMonthScrollView(self))
|
||
|
|
|
||
|
|
def _updateTitle(self):
|
||
|
|
date, _ = self.scrollView.currentPageRange()
|
||
|
|
self.setTitle(str(date.year()))
|
||
|
|
|
||
|
|
def currentPageDate(self) -> QDate:
|
||
|
|
date, _ = self.scrollView.currentPageRange()
|
||
|
|
item = self.scrollView.currentItem()
|
||
|
|
month = item.data(Qt.UserRole).month() if item else 1
|
||
|
|
return QDate(date.year(), month, 1)
|
||
|
|
|
||
|
|
|
||
|
|
class FastDayCalendarView(CalendarViewBase):
|
||
|
|
""" Day calendar view """
|
||
|
|
|
||
|
|
def __init__(self, parent=None):
|
||
|
|
super().__init__(parent)
|
||
|
|
self.setScrollView(FastDayScrollView(self))
|
||
|
|
|
||
|
|
def _updateTitle(self):
|
||
|
|
date = self.currentPageDate()
|
||
|
|
name = QCalendar().monthName(self.locale(), date.month(), date.year())
|
||
|
|
self.setTitle(f'{name} {date.year()}')
|
||
|
|
|
||
|
|
def currentPageDate(self) -> QDate:
|
||
|
|
date, _ = self.scrollView.currentPageRange()
|
||
|
|
return date
|
||
|
|
|
||
|
|
def scrollToDate(self, date: QDate):
|
||
|
|
self.scrollView.scrollToDate(date)
|
||
|
|
self._updateTitle()
|
||
|
|
|
||
|
|
|
||
|
|
class FastCalendarView(FlyoutViewBase):
|
||
|
|
|
||
|
|
dateChanged = Signal(QDate)
|
||
|
|
resetted = Signal()
|
||
|
|
|
||
|
|
def __init__(self, parent=None):
|
||
|
|
super().__init__(parent)
|
||
|
|
self.date = QDate()
|
||
|
|
self._isResetEnabled = False
|
||
|
|
|
||
|
|
self.hBoxLayout = QHBoxLayout(self)
|
||
|
|
self.stackedWidget = QStackedWidget(self)
|
||
|
|
self.yearView = FastYearCalendarView(self)
|
||
|
|
self.monthView = FastMonthCalendarView(self)
|
||
|
|
self.dayView = FastDayCalendarView(self)
|
||
|
|
|
||
|
|
self.__initWidget()
|
||
|
|
|
||
|
|
def __initWidget(self):
|
||
|
|
self.stackedWidget.addWidget(self.dayView)
|
||
|
|
self.stackedWidget.addWidget(self.monthView)
|
||
|
|
self.stackedWidget.addWidget(self.yearView)
|
||
|
|
|
||
|
|
self.hBoxLayout.setContentsMargins(0, 0, 0, 0)
|
||
|
|
self.hBoxLayout.addWidget(self.stackedWidget)
|
||
|
|
|
||
|
|
self.dayView.setDate(QDate.currentDate())
|
||
|
|
|
||
|
|
self.dayView.titleClicked.connect(self._onDayViewTitleClicked)
|
||
|
|
self.monthView.titleClicked.connect(self._onMonthTitleClicked)
|
||
|
|
|
||
|
|
self.monthView.itemClicked.connect(self._onMonthItemClicked)
|
||
|
|
self.yearView.itemClicked.connect(self._onYearItemClicked)
|
||
|
|
self.dayView.itemClicked.connect(self._onDayItemClicked)
|
||
|
|
|
||
|
|
self.monthView.resetted.connect(self._onResetted)
|
||
|
|
self.yearView.resetted.connect(self._onResetted)
|
||
|
|
self.dayView.resetted.connect(self._onResetted)
|
||
|
|
|
||
|
|
def isRestEnabled(self):
|
||
|
|
return self._isResetEnabled
|
||
|
|
|
||
|
|
def setResetEnabled(self, isEnabled: bool):
|
||
|
|
""" set the visibility of reset button """
|
||
|
|
self._isResetEnabled = isEnabled
|
||
|
|
self.yearView.setResetEnabled(isEnabled)
|
||
|
|
self.monthView.setResetEnabled(isEnabled)
|
||
|
|
self.dayView.setResetEnabled(isEnabled)
|
||
|
|
|
||
|
|
def _onResetted(self):
|
||
|
|
self.resetted.emit()
|
||
|
|
self.close()
|
||
|
|
|
||
|
|
def _onDayViewTitleClicked(self):
|
||
|
|
self.stackedWidget.setCurrentWidget(self.monthView)
|
||
|
|
self.monthView.setDate(self.dayView.currentPageDate())
|
||
|
|
|
||
|
|
def _onMonthTitleClicked(self):
|
||
|
|
self.stackedWidget.setCurrentWidget(self.yearView)
|
||
|
|
self.yearView.setDate(self.monthView.currentPageDate())
|
||
|
|
|
||
|
|
def _onMonthItemClicked(self, date: QDate):
|
||
|
|
self.stackedWidget.setCurrentWidget(self.dayView)
|
||
|
|
self.dayView.scrollToDate(date)
|
||
|
|
|
||
|
|
def _onYearItemClicked(self, date: QDate):
|
||
|
|
self.stackedWidget.setCurrentWidget(self.monthView)
|
||
|
|
self.monthView.setDate(date)
|
||
|
|
|
||
|
|
def _onDayItemClicked(self, date: QDate):
|
||
|
|
self.close()
|
||
|
|
if date != self.date:
|
||
|
|
self.date = date
|
||
|
|
self.dateChanged.emit(date)
|
||
|
|
|
||
|
|
def setDate(self, date: QDate):
|
||
|
|
""" set the selected date """
|
||
|
|
self.dayView.setDate(date)
|
||
|
|
self.date = date
|
||
|
|
|
||
|
|
def paintEvent(self, e):
|
||
|
|
painter = QPainter(self)
|
||
|
|
painter.setRenderHints(QPainter.RenderHint.Antialiasing)
|
||
|
|
|
||
|
|
painter.setBrush(
|
||
|
|
QColor(40, 40, 40) if isDarkTheme() else QColor(248, 248, 248))
|
||
|
|
painter.setPen(
|
||
|
|
QColor(23, 23, 23) if isDarkTheme() else QColor(234, 234, 234))
|
||
|
|
|
||
|
|
rect = self.rect().adjusted(1, 1, -1, -1)
|
||
|
|
painter.drawRoundedRect(rect, 8, 8)
|