Todo

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

Sources

CalendarMatrix.py

import objc
import Cocoa

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_(
                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

from __future__ import print_function
import Cocoa
import objc
from ToDoDocument import ToDoDocument

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_(
                    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_(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())
        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):
        # XXX: Not sure if the native code works correctly if the return value
        # from super != 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)

        # XXX: The calls to retain may not be necessary.
        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

from objc import super
import Cocoa

RowSelectedNotification = "RowSelectedNotification"

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

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

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

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

ToDoCell.py

import objc
import Cocoa

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
from ToDoCell import ToDoCell
from ToDoItem import ToDoItem
from SelectionNotifyMatrix import RowSelectedNotification, SelectionNotifyMatrix

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(ToDoDocument, self).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 d 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 selfcalendar:
            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_(
                    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
from objc import super

# 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(ToDoItem, self).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
\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(ToDoItem, self).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

from PyObjCTools import AppHelper
import objc;objc.setVerbose(1)

# Import all submodules,  to make sure all
# classes are known to the runtime
import CalendarMatrix
import InfoWindowController
import SelectionNotifyMatrix
import ToDoCell
import ToDoDocument
import ToDoItem
import TodoAppDelegate

AppHelper.runEventLoop()

setup.py

"""
Script for building the example.

Usage:
    python3 setup.py py2app
"""
from setuptools import setup

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

plist = dict(
    CFBundleShortVersionString="To Do v1",
    CFBundleIconFile="ToDoApp.icns",
    CFBundleGetInfoString="To Do v1",
    CFBundleIdentifier="net.sf.pyobjc.ToDo",
    CFBundleDocumentTypes=[
        dict(
            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=dict(py2app=dict(plist=plist)),
    setup_requires=[
        "py2app",
        "pyobjc-framework-Cocoa",
    ]
)

Resources