Todo
====

* :download:`Download example <PyObjCExample-Todo.zip>`

A more complex NIB based applications. This is a document-based application.

The code is a translation into Python of an example project in
`Learning Cocoa`__ from O'Reilly

.. __: https://www.oreilly.com/library/view/learning-cocoa-with/0596003013/


.. rst-class:: tabber

Sources
-------

.. rst-class:: tabbertab

CalendarMatrix.py
.................

.. sourcecode:: python

    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)

.. rst-class:: tabbertab

InfoWindowController.py
.......................

.. sourcecode:: python

    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)

.. rst-class:: tabbertab

SelectionNotifyMatrix.py
........................

.. sourcecode:: python

    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
            )

.. rst-class:: tabbertab

ToDoCell.py
...........

.. sourcecode:: python

    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

.. rst-class:: tabbertab

ToDoDocument.py
...............

.. sourcecode:: python

    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)

.. rst-class:: tabbertab

ToDoItem.py
...........

.. sourcecode:: python

    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)

.. rst-class:: tabbertab

TodoAppDelegate.py
..................

.. sourcecode:: python

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

.. rst-class:: tabbertab

main.py
.......

.. sourcecode:: python

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

.. rst-class:: tabbertab

setup.py
........

.. sourcecode:: python

    """
    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"],
    )

