# 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)