1

I'm currently making a to-do application which has a calendar. Whenever the user has an event on a specific date, A red circle appears in the top left corner. Whenever the user double clicks on the date, I want it to display a new window with information on event for the day. However, I am having trouble storing information into each date. How can I make it so each date has a sort of list that can store events?

Here is the UI:

enter image description here

Here is the code for the subclassed QCalendarWidget:

class TodoCalendar(QtWidgets.QCalendarWidget):
    def __init__(self, list_of_events, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.list_of_events = list_of_events
        //list_of_events is a list of all events the user has created

        self.table = self.findChild(QtWidgets.QTableView)
        self.table.viewport().installEventFilter(self)

    def paintCell(self, painter, rect, date):
        super().paintCell(painter, rect, date)
        for event in self.list_of_events.values():
            if event.due_time == date:
                painter.setBrush(Qt.red)
                painter.drawEllipse(rect.topLeft() + QPoint(12, 7), 3, 3)

    def eventFilter(self, source, event):
        if (event.type() == QtCore.QEvent.MouseButtonDblClick and source is self.table.viewport()):
            index = self.table.indexAt(event.pos())
            print(f"row: {index.row()}, column: {index.column()}, text: {index.data()}")
        return super().eventFilter(source, event)

Also here is the list for list_of_events:

{'test changed': <CustomWidgets.TodoEvent object at 0x00000230A5A72908>, 'due 10/8': <CustomWidgets.TodoEvent object at 0x00000230A5AA5080>, 'also due 10/9': <CustomWidgets.TodoEvent object at 0x00000230A5AC4B00>, 'also due 10/9 too': <CustomWidgets.TodoEvent object at 0x00000230A5AD0550>, 'due 10/9 too too': <CustomWidgets.TodoEvent object at 0x00000230A5AD0A90>, '10/9 2': <CustomWidgets.TodoEvent object at 0x00000230A5AD6438>, '10/10': <CustomWidgets.TodoEvent object at 0x00000230A5AD64A8>, '10/10 also': <CustomWidgets.TodoEvent object at 0x00000230A5AD64E0>, '10/10 2': <CustomWidgets.TodoEvent object at 0x00000230A5AD6550>, '10/10 3': <CustomWidgets.TodoEvent object at 0x00000230A5AD65C0>, '10/10 4': <CustomWidgets.TodoEvent object at 0x00000230A5AD6630>, 'due 10/9 changed': <CustomWidgets.TodoEvent object at 0x00000230A5AD6668>}

each toDoEvent has a title, due_time, remind_time, and description

Silent Beast
  • 119
  • 2
  • 12

1 Answers1

2

Instead of storing in some event by date another approach is to get the date given the row and column, and then filter the events.

The problem is that there is no public method to calculate the date given the row and column, so my solution uses the Qt private API code.

Considering the above, the solution is:

import random
from dataclasses import dataclass

from PyQt5 import QtCore, QtGui, QtWidgets


@dataclass
class Todo:
    date: QtCore.QDate
    name: str


class TodoCalendar(QtWidgets.QCalendarWidget):
    def __init__(self, list_of_events, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.list_of_events = list_of_events

        self.table = self.findChild(QtWidgets.QTableView)
        self.table.viewport().installEventFilter(self)

    def paintCell(self, painter, rect, date):
        super().paintCell(painter, rect, date)
        for event in self.list_of_events:
            if event.date == date:
                painter.setBrush(QtCore.Qt.red)
                painter.drawEllipse(rect.topLeft() + QtCore.QPoint(12, 7), 3, 3)

    def eventFilter(self, source, event):
        if (
            event.type() == QtCore.QEvent.MouseButtonDblClick
            and source is self.table.viewport()
        ):
            index = self.table.indexAt(event.pos())
            date = self.dateForCell(index.row(), index.column())
            today_events = [ev for ev in self.list_of_events if ev.date == date]
            if today_events:
                print(today_events)
        return super().eventFilter(source, event)

    def referenceDate(self):
        refDay = 1
        while refDay <= 31:
            refDate = QtCore.QDate(self.yearShown(), self.monthShown(), refDay)
            if refDate.isValid():
                return refDate
            refDay += 1
        return QtCore.QDate()

    @property
    def firstColumn(self):
        return (
            1
            if self.verticalHeaderFormat() == QtWidgets.QCalendarWidget.ISOWeekNumbers
            else 0
        )

    @property
    def firstRow(self):
        return (
            0
            if self.horizontalHeaderFormat()
            == QtWidgets.QCalendarWidget.NoHorizontalHeader
            else 1
        )

    def columnForDayOfWeek(self, day):
        if day < 1 or day > 7:
            return -1
        column = day - self.firstDayOfWeek()
        if column < 0:
            column += 7
        return column + self.firstColumn

    def columnForFirstOfMonth(self, date):
        return (self.columnForDayOfWeek(date.dayOfWeek()) - (date.day() % 7) + 8) % 7

    def dateForCell(self, row, column):
        if (
            row < self.firstRow
            or row > (self.firstRow + 6 - 1)
            or column < self.firstColumn
            or column > (self.firstColumn + 7 - 1)
        ):
            return QtCore.QDate()
        refDate = self.referenceDate()
        if not refDate.isValid():
            return QtCore.QDate()
        columnForFirstOfShownMonth = self.columnForFirstOfMonth(refDate)
        if columnForFirstOfShownMonth - self.firstColumn < 1:
            row -= 1
        requestedDay = (
            7 * (row - self.firstRow)
            + column
            - columnForFirstOfShownMonth
            - refDate.day()
            + 1
        )
        return refDate.addDays(requestedDay)


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)

    events = [
        Todo(QtCore.QDate.currentDate().addDays(random.randint(1, 10)), f"name-{i}")
        for i in range(15)
    ]

    w = TodoCalendar(events)
    w.show()
    sys.exit(app.exec_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Why not use the [`activated`](https://doc.qt.io/qt-5/qcalendarwidget.html#activated) signal, that already returns the date of the double clicked cell? – musicamante Oct 10 '19 at 18:38
  • @musicamante mmm, not only is it emitted by the doubleclicked (also by pressing the Enter and Return keys) maybe that is not a requirement of the OP. – eyllanesc Oct 10 '19 at 18:43
  • Yes, but maybe that should be noted in the answer, because if the OP doesn't need to distinguish from the Enter/Return, that's much easier, while if a different operation with those keys is required, the filter should return True, otherwise it will react to both the filter *and* the signal; also, I think it should be checked if the date is within the calendar date range. Another question: why do you use a while for referenceDate? As far as I can tell, it will always return the first day of the current year/month shown (which is always a valid date, if I'm not missing something), right? – musicamante Oct 10 '19 at 19:16
  • @musicamante I have not analyzed the logic much, I have only exposed the logic of the private API of Qt: https://github.com/qt/qtbase/blob/13e0a36626bd75e631dd9536e795a494432b1945/src/widgets/widgets/qcalendarwidget.cpp#L1048 – eyllanesc Oct 10 '19 at 19:19
  • Ok, I've got some interesting insight, thanks for pointing it out! The problem comes from the Julian calendar which "doesn't" have a January 1st and considers the infamous day skipping from 5th to 14th of October 1582. The code you report relies on QCalendar, which will be available with Qt5.14, but, in the meantime and since the Qt5, QDate only supports the Gregorian calendar, so the switch before 1582 has been removed and January first 4713b.C. is considered valid: so Qt5 always reports all first day occurrencies as valid (while Qt4 didn't), unless a QCalendar is (will be) added as argument. – musicamante Oct 10 '19 at 20:07