Todo

A PyObjC Example without documentation

Sources

CalendarMatrix.py

import Cocoa
import objc

gNumDaysInMonth = (0, 31, 28, 31, 30, 21, 30, 31, 31, 30, 31, 30, 31)


def isLeap(year):
    return ((year % 4) == 0 and ((year % 100) != 0)) or (year % 400) == 0


class CalendarMatrix(Cocoa.NSMatrix):
    lastMonthButton = objc.IBOutlet()
    monthName = objc.IBOutlet()
    nextMonthButton = objc.IBOutlet()

    __slots__ = ("_selectedDay", "_startOffset")

    def initWithFrame_(self, frameRect):
        self._selectedDay = None
        self._startOffset = 0

        cell = Cocoa.NSButtonCell.alloc().initTextCell_("")
        now = Cocoa.NSCalendarDate.date()

        cell.setShowsStateBy_(Cocoa.NSOnOffButton)
        self.initWithFrame_mode_prototype_numberOfRows_numberOfColumns_(
            frameRect, Cocoa.NSRadioModeMatrix, cell, 5, 7
        )

        count = 0
        for i in range(6):
            for j in range(7):
                val = self.cellAtRow_column_(i, j)
                if val:
                    val.setTag_(count)
                count += 1

        self._selectedDay = Cocoa.NSCalendarDate.dateWithYear_month_day_hour_minute_second_timeZone_(  # noqa: B950
            now.yearOfCommonEra(),
            now.monthOfYear(),
            now.dayOfMonth(),
            0,
            0,
            0,
            Cocoa.NSTimeZone.localTimeZone(),
        )
        return self

    @objc.IBAction
    def choseDay_(self, sender):
        prevSelDate = self.selectedDay()
        selDay = self.selectedCell().tag() - self._startOffset + 1

        selDate = (
            Cocoa.NSCalendarDate.dateWithYear_month_day_hour_minute_second_timeZone_(
                prevSelDate.yearOfCommonEra(),
                prevSelDate.monthOfYear(),
                selDay,
                0,
                0,
                0,
                Cocoa.NSTimeZone.localTimeZone(),
            )
        )
        self.setSelectedDay_(selDate)
        self.highlightTodayIfVisible()

        if self.delegate().respondsToSelector_("calendarMatrix:didChangeToDate:"):
            self.delegate().calendarMatrix_didChangeToDate_(self, selDate)

    @objc.IBAction
    def monthChanged_(self, sender):
        thisDate = self.selectedDay()
        currentYear = thisDate.yearOfCommonEra()
        currentMonth = thisDate.monthOfYear()

        if sender is self.nextMonthButton:
            if currentMonth == 12:
                currentMonth = 1
                currentYear += 1
            else:
                currentMonth += 1
        else:
            if currentMonth == 1:
                currentMonth = 12
                currentYear -= 1
            else:
                currentMonth -= 1

        self.setSelectedDay_(
            Cocoa.NSCalendarDate.dateWithYear_month_day_hour_minute_second_timeZone_(
                currentYear, currentMonth, 1, 0, 0, 0, Cocoa.NSTimeZone.localTimeZone()
            )
        )
        self.refreshCalendar()
        self.choseDay_(self)

    def setSelectedDay_(self, newDay):
        self._selectedDay = newDay

    def selectedDay(self):
        return self._selectedDay

    def refreshCalendar(self):
        selDate = self.selectedDay()
        currentMonth = selDate.monthOfYear()
        currentYear = selDate.yearOfCommonEra()

        firstOfMonth = (
            Cocoa.NSCalendarDate.dateWithYear_month_day_hour_minute_second_timeZone_(
                currentYear, currentMonth, 1, 0, 0, 0, Cocoa.NSTimeZone.localTimeZone()
            )
        )
        self.monthName.setStringValue_(
            firstOfMonth.descriptionWithCalendarFormat_("%B %Y")
        )
        daysInMonth = gNumDaysInMonth[currentMonth]

        if (currentMonth == 2) and isLeap(currentYear):
            daysInMonth += 1

        self._startOffset = firstOfMonth.dayOfWeek()

        dayLabel = 1

        for i in range(42):
            cell = self.cellWithTag_(i)
            if cell is None:
                continue

            if i < self._startOffset or i >= (daysInMonth + self._startOffset):
                # blank out unused cells in the matrix
                cell.setBordered_(False)
                cell.setEnabled_(False)
                cell.setTitle_("")
                cell.setCellAttribute_to_(Cocoa.NSCellHighlighted, False)
            else:
                # Fill in valid days in the matrix
                cell.setBordered_(True)
                cell.setEnabled_(True)
                cell.setFont_(Cocoa.NSFont.systemFontOfSize_(12))
                cell.setTitle_(str(dayLabel))
                dayLabel += 1
                cell.setCellAttribute_to_(Cocoa.NSCellHighlighted, False)

        self.selectCellWithTag_(selDate.dayOfMonth() + self._startOffset - 1)
        self.highlightTodayIfVisible()

    def highlightTodayIfVisible(self):
        now = Cocoa.NSCalendarDate.date()
        selDate = self.selectedDay()

        if (
            selDate.yearOfCommonEra() == now.yearOfCommonEra()
            and selDate.monthOfYear() == now.monthOfYear()
            and selDate.dayOfMonth() == now.dayOfMonth()
        ):
            aCell = self.cellWithTag_(now.dayOfMonth() + self._startOffset - 1)
            aCell.setHighlightsBy_(Cocoa.NSMomentaryChangeButton)
            aCell.setCellAttribute_to_(Cocoa.NSCellHighlighted, True)

    def awakeFromNib(self):
        self.setTarget_(self)
        self.setAction_("choseDay:")
        self.setAutosizesCells_(True)
        self.refreshCalendar()
        self.choseDay_(self)

InfoWindowController.py

import Cocoa
import objc
from ToDoDocument import ToDoDocument
from ToDoItem import (
    COMPLETE,
    SECS_IN_HOUR,
    SECS_IN_DAY,
    ToDoItem,
    ConvertSecondsToTime,
    ConvertTimeToSeconds,
    ToDoItemChangedNotification,
)

NOTIFY_TAG = 0
RESCHEDULE_TAG = 1
NOTES_TAG = 2

NotifyLengthNone = 0
NotifyLengthQuarter = 1
NotifyLengthHour = 2
NotifyLengthDay = 3
NotifyLengthOther = 4

_sharedInfoWindowController = None


class InfoWindowController(Cocoa.NSWindowController):
    dummyView = objc.IBOutlet()
    infoDate = objc.IBOutlet()
    infoItem = objc.IBOutlet()
    infoNotes = objc.IBOutlet()
    infoNotifyAMPM = objc.IBOutlet()
    infoNotifyHour = objc.IBOutlet()
    infoNotifyMinute = objc.IBOutlet()
    infoNotifyOtherHours = objc.IBOutlet()
    infoNotifySwitchMatrix = objc.IBOutlet()
    infoPopUp = objc.IBOutlet()
    infoSchedComplete = objc.IBOutlet()
    infoSchedDate = objc.IBOutlet()
    infoSchedMatrix = objc.IBOutlet()
    infoWindowViews = objc.IBOutlet()
    notesView = objc.IBOutlet()
    notifyView = objc.IBOutlet()
    reschedView = objc.IBOutlet()

    __slots__ = ("_inspectingDocument",)

    @objc.IBAction
    def switchClicked_(self, sender):
        dueSecs = 0
        idx = 0
        theItem = self._inspectingDocument.selectedItem()
        if theItem is None:
            return

        if sender is self.infoNotifyAMPM:
            if self.infoNotifyHour.intValue():
                pmFlag = self.infoNotifyAMPM.selectedRow() == 1
                dueSecs = ConvertTimeToSeconds(
                    self.infoNotifyHour.intValue(),
                    self.infoNotifyMinute.intValue(),
                    pmFlag,
                )
                theItem.setSecsUntilDue_(dueSecs)
        elif sender is self.infoNotifySwitchMatrix:
            idx = self.infoNotifySwitchMatrix.selectedRow()

            if not theItem:
                pass
            elif idx == NotifyLengthNone:
                theItem.setSecsUntilNotify_(0)
            elif idx == NotifyLengthQuarter:
                theItem.setSecsUntilNotify_(SECS_IN_HOUR / 4)
            elif idx == NotifyLengthHour:
                theItem.setSecsUntilNotify_(SECS_IN_HOUR)
            elif idx == NotifyLengthDay:
                theItem.setSecsUntilNotify_(SECS_IN_DAY)
            elif idx == NotifyLengthOther:
                theItem.setSecsUntilNotify_(
                    self.infoNotifyOtherHours.intValue() * SECS_IN_HOUR
                )
            else:
                Cocoa.NSLog("Error in selectedRow")
        elif sender is self.infoSchedComplete:
            if theItem:
                theItem.setStatus_(COMPLETE)
        elif sender is self.infoSchedMatrix:
            # left as an exercise in the objective-C code
            pass

        self.updateInfoWindow()
        self._inspectingDocument.selectedItemModified()

    def textDidChange_(self, notification):
        if notification.object() is self.infoNotes:
            self._inspectingDocument.selectedItem().setNotes_(self.infoNotes.string())
            self._inspectingDocument.selectItemModified()

    def textDidEndEditing_(self, notification):
        if notification.object() is self.infoNotes:
            self._inspectingDocument.selectedItem().setNotes_(self.infoNotes.string())
            self._inspectingDocument.selectedItemModified()

    def controlTextDidEndEditing_(self, notification):
        dueSecs = 0
        theItem = self._inspectingDocument.selectedItem()
        if theItem is None:
            return

        if (notification.object() is self.infoNotifyHour) or (
            notification.object() is self.infoNotifyMinute
        ):
            dueSecs = ConvertTimeToSeconds(
                self.infoNotifyHour.intValue(),
                self.infoNotifyMinute.intValue(),
                self.infoNotifyAMPM.cellAtRow_column_(1, 0).state(),
            )
            theItem.setSecsUntilNotify_(dueSecs)
        elif notification.object() is self.infoNotifyOtherHours:
            if self.infoNotifySwitchMatrix.selectedRow() == NotifyLengthOther:
                theItem.setSecsUntilNotify_(
                    self.infoNotifyOtherHours.intValue() * SECS_IN_HOUR
                )
            else:
                return
        elif notification.object() is self.infoSchedDate:
            # Left as an exercise
            pass

        self._inspectingDocument.selectedItemModified()

    @classmethod
    def sharedInfoWindowController(self):
        global _sharedInfoWindowController

        if not _sharedInfoWindowController:
            _sharedInfoWindowController = InfoWindowController.alloc().init()

        return _sharedInfoWindowController

    def init(self):
        self = self.initWithWindowNibName_("ToDoInfoWindow")
        if self:
            self.setWindowFrameAutosaveName_("Info")

        return self

    def dump_outlets(self):
        print("dummyView", self.dummyView)
        print("infoDate", self.infoDate)
        print("infoItem", self.infoItem)
        print("infoNotes", self.infoNotes)
        print("infoNotifyAMPM", self.infoNotifyAMPM)
        print("infoNotifyHour", self.infoNotifyHour)
        print("infoNotifyMinute", self.infoNotifyMinute)
        print("infoNotifyOtherHours", self.infoNotifyOtherHours)
        print("infoNotifySwitchMatrix", self.infoNotifySwitchMatrix)
        print("infoPopUp", self.infoPopUp)
        print("infoSchedComplet", self.infoSchedComplete)
        print("infoSchedDate", self.infoSchedDate)
        print("infoSchedMatrix", self.infoSchedMatrix)
        print("infoWindowViews", self.infoWindowViews)
        print("notesView", self.notesView)
        print("notifyView", self.notifyView)
        print("reschedView", self.reschedView)

    def windowDidLoad(self):
        Cocoa.NSWindowController.windowDidLoad(self)

        self.notifyView.retain()
        self.notifyView.removeFromSuperview()

        self.reschedView.retain()
        self.reschedView.removeFromSuperview()

        self.notesView.retain()
        self.notesView.removeFromSuperview()

        self.infoWindowViews = None

        self.infoNotes.setDelegate_(self)
        self.swapInfoWindowView_(self)
        self.setMainWindow_(Cocoa.NSApp().mainWindow())
        self.updateInfoWindow()

        Cocoa.NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
            self, "mainWindowChanged:", Cocoa.NSWindowDidBecomeMainNotification, None
        )

        Cocoa.NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
            self, "mainWindowResigned:", Cocoa.NSWindowDidResignMainNotification, None
        )

        Cocoa.NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
            self, "selectedItemChanged:", ToDoItemChangedNotification, None
        )

    def __del__(self):  # dealloc
        Cocoa.NSNotificationCenter.defaultCenter().removeObserver_(self)

        # Cannot to this
        Cocoa.NSWindowController.dealloc(self)

    def updateInfoWindow(self):
        minute = 0
        hour = 0

        selected = self.infoPopUp.selectedItem().tag()
        selectedItem = self._inspectingDocument.selectedItem()

        if isinstance(selectedItem, ToDoItem):
            self.infoItem.setStringValue_(selectedItem.itemName())
            self.infoDate.setStringValue_(
                selectedItem.day().descriptionWithCalendarFormat_timeZone_locale_(
                    "%a, %b %d %Y", Cocoa.NSTimeZone.localTimeZone(), None
                )
            )

            if selected == NOTIFY_TAG:
                dueSecs = selectedItem.secsUntilDue()
                hour, minutes, pmFlag = ConvertSecondsToTime(dueSecs)
                self.infoNotifyAMPM.cellAtRow_column_(0, 0).setState_(not pmFlag)
                self.infoNotifyAMPM.cellAtRow_column_(1, 0).setState_(pmFlag)
                self.infoNotifyHour.setIntValue_(hour)
                self.infoNotifyMinute.setIntValue_(minute)

                notifySecs = selectedItem.secsUntilNotify()
                clearButtonMatrix(self.infoNotifySwitchMatrix)

                if notifySecs == 0:
                    self.infoNotifySwitchMatrix.cellAtRow_column_(
                        NotifyLengthNone, 0
                    ).setState_(Cocoa.NSOnState)
                elif notifySecs == SECS_IN_HOUR / 4:
                    self.infoNotifySwitchMatrix.cellAtRow_column_(
                        NotifyLengthQuarter, 0
                    ).setState_(Cocoa.NSOnState)
                elif notifySecs == SECS_IN_HOUR:
                    self.infoNotifySwitchMatrix.cellAtRow_column_(
                        NotifyLengthHour, 0
                    ).setState_(Cocoa.NSOnState)
                elif notifySecs == SECS_IN_DAY:
                    self.infoNotifySwitchMatrix.cellAtRow_column_(
                        NotifyLengthDay, 0
                    ).setState_(Cocoa.NSOnState)
                else:
                    self.infoNotifySwitchMatrix.cellAtRow_column_(
                        NotifyLengthOther, 0
                    ).setState_(Cocoa.NSOnState)
                    self.infoNotifyOtherHours.setIntValue_(notifySecs / SECS_IN_HOUR)
            elif selected == RESCHEDULE_TAG:
                # left as an exercise
                pass
            elif selected == NOTES_TAG:
                self.infoNotes.setString_(selectedItem.notes())
        else:
            self.infoItem.setStringValue_("")
            self.infoDate.setStringValue_("")
            self.infoNotifyHour.setStringValue_("")
            self.infoNotifyMinute.setStringValue_("")
            self.infoNotifyAMPM.cellAtRow_column_(0, 0).setState_(Cocoa.NSOnState)
            self.infoNotifyAMPM.cellAtRow_column_(1, 0).setState_(Cocoa.NSOffState)
            clearButtonMatrix(self.infoNotifySwitchMatrix)
            self.infoNotifySwitchMatrix.cellAtRow_column_(
                NotifyLengthNone, 0
            ).setState_(Cocoa.NSOnState)
            self.infoNotifyOtherHours.setStringValue_("")
            self.infoNotes.setString_("")

    def setMainWindow_(self, mainWindow):
        if not mainWindow:
            return

        controller = mainWindow.windowController()

        if isinstance(controller.document(), ToDoDocument):
            self._inspectingDocument = controller.document()
        else:
            self._inspectingDocument = None

        self.updateInfoWindow()

    def mainWindowChanged_(self, notification):
        self.setMainWindow_(notification.object())

    def mainWindowResigned_(self, notification):
        self.setMainWindow_(None)

    @objc.IBAction
    def swapInfoWindowView_(self, sender):
        selected = self.infoPopUp.selectedItem().tag()

        if selected == NOTIFY_TAG:
            newView = self.notifyView
        elif selected == RESCHEDULE_TAG:
            newView = self.reschedView
        elif selected == NOTES_TAG:
            newView = self.notesView

        if self.dummyView.contentView() != newView:
            self.dummyView.setContentView_(newView)

    def selectedItemChanged_(self, notification):
        self.updateInfoWindow()


def clearButtonMatrix(matrix):
    rows, cols = matrix.getNumberOfRows_columns_()

    for i in range(rows):
        cell = matrix.cellAtRow_column_(i, 0)
        if cell:
            cell.setState_(False)

SelectionNotifyMatrix.py

import Cocoa
from objc import super  # noqa: A004

RowSelectedNotification = "RowSelectedNotification"


class SelectionNotifyMatrix(Cocoa.NSMatrix):
    def mouseDown_(self, theEvent):
        super().mouseDown_(theEvent)

        row = self.selectedRow()
        if row != -1:
            Cocoa.NSNotificationCenter.defaultCenter().postNotificationName_object_userInfo_(
                RowSelectedNotification, self, None
            )

    def selectCellAtRow_column_(self, row, col):
        super().selectCellAtRow_column_(row, col)

        Cocoa.NSNotificationCenter.defaultCenter().postNotificationName_object_userInfo_(
            RowSelectedNotification, self, None
        )

ToDoCell.py

import Cocoa
import objc

NOT_DONE = 0
DONE = 1
DEFERRED = 2


class ToDoCell(Cocoa.NSButtonCell):
    __slots__ = ("_triState", "_doneImage", "_deferredImage", "_timeDue")

    def init(self):
        self._triState = NOT_DONE
        self._timeDue = None
        self._doneImage = None
        self._deferredImage = None

        Cocoa.NSButtonCell.initTextCell_(self, "")

        self.setType_(Cocoa.NSToggleButton)
        self.setImagePosition_(Cocoa.NSImageLeft)
        self.setBezelStyle_(Cocoa.NSShadowlessSquareBezelStyle)
        self.setFont_(Cocoa.NSFont.userFontOfSize_(10))
        self.setAlignment_(Cocoa.NSRightTextAlignment)

        self._doneImage = Cocoa.NSImage.imageNamed_("DoneMark")
        self._deferredImage = Cocoa.NSImage.imageNamed_("DeferredMark")
        return self

    @objc.typedAccessor(objc._C_INT)
    def setTriState_(self, newState):
        if newState > DEFERRED:
            self._triState = NOT_DONE
        else:
            self._triState = newState

        self.updateImage()

    @objc.typedAccessor(objc._C_INT)
    def triState(self):
        return self._triState

    def setState_(self, val):
        pass

    def state(self):
        if self._triState == DEFERRED:
            return DONE
        else:
            return self._triState

    def updateImage(self):
        if self._triState == NOT_DONE:
            self.setImage_(None)
        elif self._triState == DONE:
            self.setImage_(self._doneImage)
        elif self._triState == DEFERRED:
            self.setImage_(self._deferredImage)

        self.controlView().updateCell_(self)

    def startTrackingAt_inView_(self, startPoint, controlView):
        return 1

    def stopTracking_at_inView_mouseIsUp_(
        self, lastPoint, stopPoint, controlView, flag
    ):
        if flag:
            self.setTriState_(self.triState() + 1)

    def setTimeDue_(self, newTime):
        if newTime:
            self._timeDue = newTime
            self.setTitle_(
                self._timeDue.descriptionWithCalendarFormat_timeZone_locale_(
                    "%I:%M %p", Cocoa.NSTimeZone.localTimeZone(), None
                )
            )
        else:
            self._timeDue = None
            self.setTitle_("-->")

    def timeDue(self):
        return self._timeDue

ToDoDocument.py

import Cocoa
import objc
from objc import super  # noqa: A004
from SelectionNotifyMatrix import RowSelectedNotification
from ToDoCell import ToDoCell
from ToDoItem import ToDoItem, INCOMPLETE

ToDoItemChangedNotification = "ToDoItemChangedNotification"


class ToDoDocument(Cocoa.NSDocument):
    calendar = objc.IBOutlet()
    dayLabel = objc.IBOutlet()
    itemList = objc.IBOutlet()
    statusList = objc.IBOutlet()

    __slots__ = (
        "_dataFromFile",
        "_activeDays",
        "_currentItems",
        "_selectedItem",
        "_selectedItemEdited",
    )

    def rowSelected_(self, notification):
        row = notification.object().selectedRow()

        if row == -1:
            return

        self._selectedItem = self._currentItems.objectAtIndex_(row)

        if not isinstance(self._selectedItem, ToDoItem):
            self._selectedItem = None

        Cocoa.NSNotificationCenter.defaultCenter().postNotificationName_object_userInfo_(
            ToDoItemChangedNotification, self._selectedItem, None
        )

    def init(self):
        self = super().init()
        if self is None:
            return self
        self._activeDays = None
        self._currentItems = None
        self._selectedItem = None
        self._selectedItemEdited = 0
        self._dataFromFile = None

        return self

    def __del__(self):  # dealloc in Objective-C code
        Cocoa.NSNotificationCenter.defaultCenter().removeObserver_(self)

    def selectedItem(self):
        return self._selectedItem

    def windowNibName(self):
        return "ToDoDocument"

    def windowControllerDidLoadNib_(self, aController):
        # Cocoa.NSDocument.windowControllerDidLoadNib_(self, aController)

        self.setHasUndoManager_(0)
        self.itemList.setDelegate_(self)

        index = self.statusList.cells().count()
        while index:
            index -= 1

            aCell = ToDoCell.alloc().init()
            aCell.setTarget_(self)
            aCell.setAction_("itemStatusClicked:")
            self.statusList.putCell_atRow_column_(aCell, index, 0)

        if self._dataFromFile:
            self.loadDocWithData_(self._dataFromFile)
            self._dataFromFile = None
        else:
            self.loadDocWithData_(None)

        Cocoa.NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
            self, "rowSelected:", RowSelectedNotification, self.itemList
        )
        Cocoa.NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
            self, "rowSelected:", RowSelectedNotification, self.statusList
        )

    def loadDocWithData_(self, data):
        if data:
            dct = Cocoa.NSUnarchiver.unarchiveObjectWithData_(data)
            self.initDataModelWithDictinary_(dct)
            dayEnum = self._activeDays.keyEnumerator()
            now = Cocoa.NSDate.date()

            itemDate = dayEnum.nextObject()
            while itemDate:
                itemArray = self._activeDays.objectForKey_(itemDate)
                itemEnum = itemArray.objectEnumerator()

                anItem = itemEnum.nextObject()
                while anItem:
                    if (
                        isinstance(anItem, ToDoItem)
                        and anItem.secsUntilNotify()
                        and anItem.status() == INCOMPLETE
                    ):
                        due = anItem.day().addTimeInterfval_(anItem.secondsUntilDue())
                        elapsed = due.timeIntervalSinceDate_(now)
                        if elapsed > 0:
                            self.setTimerForItem_(anItem)
                        else:
                            Cocoa.NSBeep()
                            Cocoa.NSRunAlertPanel(
                                "To Do",
                                "%s on %s is past due!"
                                % (
                                    anItem.itemName(),
                                    due.descriptionWithCalendarFormat_timeZone_locale_(
                                        "%b %d, %Y at %I:%M %p",
                                        Cocoa.NSTimeZone.localTimeZone(),
                                        None,
                                    ),
                                ),
                                None,
                                None,
                                None,
                            )
                            anItem.setSecsUntilNotify_(0)
                    anItem = itemEnum.nextObject()

                itemDate = dayEnum.nextObject()
        else:
            self.initDataModelWithDictionary_(None)

        self.selectItemAtRow_(0)
        self.updateLists()

        self.dayLabel.setStringValue_(
            self.calendar.selectedDay().descriptionWithCalendarFormat_timeZone_locale_(
                "To Do on %a %B %d %Y", Cocoa.NSTimeZone.defaultTimeZone(), None
            )
        )

    def initDataModelWithDictionary_(self, aDict):
        if aDict:
            self._activeDays = aDict
        else:
            self._activeDays = Cocoa.NSMutableDictionary.alloc().init()

        date = self.calendar.selectedDay()
        self.setCurrentItems_(self._activeDays.objectForKey_(date))

    def setCurrentItems_(self, newItems):
        if newItems:
            self._currentItems = newItems.mutableCopy()
        else:
            numRows, numCols = self.itemList.getNumberOfRows_columns_(None, None)
            self._currentItems = Cocoa.NSMutableArray.alloc().initWithCapacity_(numRows)

            for _ in range(numRows):
                self._currentItems.addObject_("")

    def updateLists(self):
        numRows = self.itemList.cells().count()

        for i in range(numRows):
            if self._currentItems:
                thisItem = self._currentItems.objectAtIndex_(i)
            else:
                thisItem = None

            if isinstance(thisItem, ToDoItem):
                if thisItem.secsUntilDue():
                    due = thisItem.day().addTimeInterval_(thisItem.secsUntilDue())
                else:
                    due = None

                self.itemList.cellAtRow_column_(i, 0).setStringValue_(
                    thisItem.itemName()
                )
                self.statusList.cellAtRow_column_(i, 0).setTimeDue_(due)
                self.statusList.cellAtRow_column_(i, 0).setTriState_(thisItem.status())
            else:
                self.itemList.cellAtRow_column_(i, 0).setStringValue_("")
                self.statusList.cellAtRow_column_(i, 0).setTitle_("")
                self.statusList.cellAtRow_column_(i, 0).setImage_(None)

    def saveDocItems(self):
        if self._currentItems:
            cnt = self._currentItems.count()

            for i in range(cnt):
                anItem = self._currentItems.objectAtIndex_(i)
                if isinstance(anItem, ToDoItem):
                    self._activeDays.setObject_forKey_(self._currentItems, anItem.day())
                    break

    def controlTextDidEndEditing_(self, notif):
        if not self._selectedItemEdited:
            return

        row = self.itemList.selectedRow()
        newName = self.itemList.selectedCell().stringValue()

        if isinstance(self._currentItems.objectAtIndex_(row), ToDoItem):
            prevNameAtIndex = self._currentItems.objectAtIndex_(row).itemName()
            if newName == "":
                self._currentItems.replaceObjectAtRow_withObject_(row, "")
            elif prevNameAtIndex != newName:
                self._currentItems.objectAtRow_(row).setItemName_(newName)
        elif newName != "":
            newItem = ToDoItem.alloc().initWithName_andDate_(
                newName, self.calendar.selectedDay()
            )
            self._currentItems.replaceObjectAtIndex_withObject_(row, newItem)

        self._selectedItem = self._currentItems.objectAtIndex_(row)

        if not isinstance(self._selectedItem, ToDoItem):
            self._selectedItem = None

        self.updateLists()
        self._selectedItemEdited = 0
        self.updateChangeCount_(Cocoa.NSChangeDone)

        Cocoa.NSNotificationCenter.defaultCenter().postNotificationName_object_userInfo_(
            ToDoItemChangedNotification, self._selectedItem, None
        )

    def selectedItemModified(self):
        if self._selectedItem:
            self.setTimerForItem_(self._selectedItem)

        self.updateLists()
        self.updateChangeCount_(Cocoa.NSChangeDone)

    def calendarMatrix_didChangeToDate_(self, matrix, date):
        self.saveDocItems()

        if self._activeDays:
            self.setCurrentItems_(self._activeDays.objectForKey_(date))
        else:
            pass

        self.dayLabel.setStringValue_(
            date.descriptionWithCalendarFormat_timeZone_locale_(
                "To Do on %a %B %d %Y", Cocoa.NSTimeZone.defaultTimeZone(), None
            )
        )
        self.updateLists()
        self.selectedItemAtRow_(0)

    def selectedItemAtRow_(self, row):
        self.itemList.selectCellAtRow_column_(row, 0)

    def controlTextDidBeginEditing_(self, notif):
        self._selectedItemEdited = 1

    def dataRepresentationOfType_(self, aType):
        self.saveDocItems()

        return Cocoa.NSArchiver.archivedDataWithRootObject_(self._activeDays)

    def loadRepresentation_ofType_(self, data, aType):
        if self.calendar:
            self.loadDocWithData_(data)
        else:
            self._dataFromFile = data

        return 1

    @objc.IBAction
    def itemStatusClicked_(self, sender):
        row = sender.selectedRow()
        cell = sender.cellAtRow_column_(row, 0)
        item = self._currentItems.objectAtIndex_(row)

        if isinstance(item, ToDoItem):
            item.setStatus_(cell.triState())
            self.setTimerForItem_(item)

            self.updateLists()
            self.updateChangeCount_(Cocoa.NSChangeDone)

            Cocoa.NSNotificationCenter.defaultCenter().postNotificationName_object_userInfo_(
                ToDoItemChangedNotification, item, None
            )

    def setTimerForItem_(self, anItem):
        if anItem.secsUntilNotify() and anItem.status() == INCOMPLETE:
            notifyDate = anItem.day().addTimeInterval_(
                anItem.secsUntilDue() - anItem.secsUntilNotify()
            )

            aTimer = Cocoa.NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(  # noqa: B950
                notifyDate.timeIntervalSinceNow(),
                self,
                "itemTimerFired:",
                anItem,
                False,
            )
            anItem.setTimer_(aTimer)
        else:
            anItem.setTimer_(None)

    def itemTimerFired_(self, timer):
        anItem = timer.userInfo()
        dueDate = anItem.day().addTimeInterval_(anItem.secsUntilDue())

        Cocoa.NSBeep()

        Cocoa.NSRunAlertPanel(
            "To Do",
            "%s on %s"
            % (
                anItem.itemName(),
                dueDate.descriptionWithCalendarFormat_timeZone_locale_(
                    "%b %d, %Y at %I:%M: %p", Cocoa.NSTimeZone.defaultTimeZone(), None
                ),
            ),
            None,
            None,
            None,
        )
        anItem.setSecsUntilNotify_(0)
        self.setTimerForItem_(anItem)
        self.updateLists()

        Cocoa.NSNotificationCenter.defaultCenter().postNotificationName_object_userInfo_(
            ToDoItemChangedNotification, anItem, None
        )

    def selectItemAtRow_(self, row):
        self.itemList.selectCellAtRow_column_(row, 0)

ToDoItem.py

import Cocoa
import objc
from objc import super  # noqa: A004

# enum ToDoItemStatus
INCOMPLETE = 0
COMPLETE = 1
DEFER_TO_NEXT_DAY = 2

SECS_IN_MINUTE = 60
SECS_IN_HOUR = SECS_IN_MINUTE * 60
SECS_IN_DAY = SECS_IN_HOUR * 24
SECS_IN_WEEK = SECS_IN_DAY * 7


class ToDoItem(Cocoa.NSObject):
    __slots__ = (
        "_day",
        "_itemName",
        "_notes",
        "_timer",
        "_secsUntilDue",
        "_secsUntilNotify",
        "_status",
    )

    def init(self):
        self = super().init()
        if self is None:
            return None

        self._day = None
        self._itemName = None
        self._notes = None
        self._secsUntilDue = 0
        self._secsUntilNotify = 0
        self._status = None
        self._timer = None

    def description(self):
        descr = """%s
\tName: %s
\tDay: %s
\tNotes: %s
\tCompleted: %s
\tSecs Until Due: %d
\tSecs Until Notify: %d
""" % (
            super.description(),
            self.itemName(),
            self._day,
            self._notes,
            ["No", "YES"][self.status() == COMPLETE],
            self._secsUntilDue,
            self._secsUntilNotify,
        )
        return descr

    def initWithName_andDate_(self, aName, aDate):
        self = super().init()
        if self is None:
            return None

        self._day = None
        self._itemName = None
        self._notes = None
        self._secsUntilDue = 0
        self._secsUntilNotify = 0
        self._status = None
        self._timer = None

        if not aName:
            return None

        self.setItemName_(aName)

        if aDate:
            self.setDay_(aDate)
        else:
            now = Cocoa.NSCalendarDate.date()

            self.setDay_(
                Cocoa.NSCalendarDate.dateWithYear_month_day_hour_minute_second_timeZone_(
                    now.yearOfCommonEra(),
                    now.monthOfYear(),
                    now.dayOfMonth(),
                    0,
                    0,
                    0,
                    Cocoa.NSTimeZone.localTimeZone(),
                )
            )
        self.setStatus_(INCOMPLETE)
        self.setNotes_("")
        return self

    def encodeWithCoder_(self, coder):
        coder.encodeObject_(self._day)
        coder.encodeObject_(self._itemName)
        coder.encodeObject_(self._notes)

        tempTime = self._secsUntilDue
        coder.encodeValueOfObjCType_at_(objc._C_LNG, tempTime)

        tempTime = self._secsUntilNotify
        coder.encodeValueOfObjCType_at_(objc._C_LNG, tempTime)

        tempStatus = self._status
        coder.encodeValueOfObjCType_at_(objc._C_INT, tempStatus)

    def initWithCoder_(self, coder):
        self.setDay_(coder.decodeObject())
        self.setItemName_(coder.decodeObject())
        self.setNotes_(coder.decodeObject())

        tempTime = coder.decodeObjectOfObjCType_at_(objc._C_LNG)
        self.setSecsUntilDue_(tempTime)

        tempTime = coder.decodeObjectOfObjCType_at_(objc._C_LNG)
        self.setSecsUntilNotify_(tempTime)

        tempStatus = coder.decodeObjectOfObjCType_at_(objc._C_INT)
        self.setSecsUntilNotify_(tempStatus)

        return self

    def __del__(self):  # dealloc
        if self._notes:
            self._timer.invalidate()

    def setDay_(self, newDay):
        self._day = newDay

    def day(self):
        return self._day

    def setItemName_(self, newName):
        self._itemName = newName

    def itemName(self):
        return self._itemName

    def setNotes_(self, newNotes):
        self._notes = newNotes

    def notes(self):
        return self._notes

    def setTimer_(self, newTimer):
        if self._timer:
            self._timer.invalidate()

        if newTimer:
            self._timer = newTimer
        else:
            self._timer = None

    def timer(self):
        return self._timer

    def setStatus_(self, newStatus):
        self._status = newStatus

    def status(self):
        return self._status

    def setSecsUntilDue_(self, secs):
        self._secsUntilDue = secs

    def secsUntilDue(self):
        return self._secsUntilDue

    def setSecsUntilNotify_(self, secs):
        self._secsUntilNotify = secs

    def secsUntilNotify(self):
        return self._secsUntilNotify


def ConvertTimeToSeconds(hour, minute, pm):
    if hour == 12:
        hour = 0

    if pm:
        hour += 12

    return (hour * SECS_IN_HOUR) + (minute * SECS_IN_MINUTE)


def ConvertSecondsToTime(secs):
    pm = 0

    hour = secs / SECS_IN_HOUR
    if hour > 11:
        hour -= 12
        pm = 1

    if hour == 0:
        hour = 12

    minute = (secs % SECS_IN_HOUR) / SECS_IN_MINUTE

    return (hour, minute, pm)

TodoAppDelegate.py

import objc
from Foundation import NSObject
from InfoWindowController import InfoWindowController


class ToDoAppDelegate(NSObject):
    @objc.IBAction
    def showInfo_(self, sender):
        InfoWindowController.sharedInfoWindowController().showWindow_(sender)

main.py

# Import all submodules,  to make sure all
# classes are known to the runtime
import CalendarMatrix  # noqa: F401
import InfoWindowController  # noqa: F401
import SelectionNotifyMatrix  # noqa: F401
import TodoAppDelegate  # noqa: F401
import ToDoCell  # noqa: F401
import ToDoDocument  # noqa: F401
import ToDoItem  # noqa: F401
from PyObjCTools import AppHelper

AppHelper.runEventLoop()

setup.py

"""
Script for building the example.

Usage:
    python3 setup.py py2app
"""

import glob

from setuptools import setup

images = glob.glob("Images/*.tiff")
icons = glob.glob("Icons/*.icns")

plist = {
    "CFBundleShortVersionString": "To Do v1",
    "CFBundleIconFile": "ToDoApp.icns",
    "CFBundleGetInfoString": "To Do v1",
    "CFBundleIdentifier": "net.sf.pyobjc.ToDo",
    "CFBundleDocumentTypes": [
        {
            "CFBundleTypeName": "To Do list",
            "CFBundleTypeRole": "Editor",
            "NSDocumentClass": "ToDoDocument",
            "CFBundleTypeIconFile": "ToDoDoc.icns",
            "CFBundleTypeExtensions": ["ToDo"],
            "CFBundleTypeOSTypes": ["ToDo"],
        }
    ],
    "CFBundleName": "To Do",
}

setup(
    app=["main.py"],
    data_files=["English.lproj"] + images + icons,
    options={"py2app": {"plist": plist}},
    setup_requires=["py2app", "pyobjc-framework-Cocoa"],
)