Archive for Cocoa

Adding Filtering to list by subclassing an NSArrayController

In the previous series of articles, we’ve covered getting started, basic Address Book usage, and making an app bundle. This time, we’ll look at adding in some basic filtering to the list of dates.

To add filtering, we need to extend the NSArrayController class. The first step is to create a new python class, ArrayControllerWithSearch. This will extend the standard array controller with a search method.


from PyObjCTools import NibClassBuilder

class ArrayControllerWithSearch(NibClassBuilder.AutoBaseClass):
    searchString = None

    def search_(self, sender):
        self.searchString = sender.stringValue()

This doesn’t do anything particularly interesting, except to store the search string for the other methods to use. The AutoBaseClass means that the class we are going to derive from will be specified in Interface Builder and then allocated dynamically.

The next step is to over-ride the method that actually lists the objects in the array, arrangeObjects_. (recalling that the underscore is required for when Objective C wants a ‘:’).


def arrangeObjects_(self, objects):
    # retrieve a reference to the same method in the base class
    supermethod = super(ArrayControllerWithSearch, 
        self).arrangeObjects_

    if self.searchString is None or self.searchString == '':
        return supermethod(objects)

    return supermethod(list(filterObjects(objects, self.searchString)))

The supermethod variable is actually a function pointer to the arrangeObjects_ method on the super class. This gets used under either case, so it is easier to have the code to access this in one place.

filterObjects will be defined next. The search is going to be rather simplistic in that it will check if the search string is contained within the name of the person.


def filterObjects(objs, searchString):
    lowerSearch = searchString.lower()
    for obj in objs:
        if(lowerSearch in obj['name'].lower()):
            yield obj
            continue

Also, add an import statement for the class in the main application (main.py):


from PyObjCTools import AppHelper

import DateListDelegate
import ArrayControllerWithSearch

AppHelper.runEventLoop(argv=[])

In Interface Builder, subclass NSArrayController in the Classes tab, and call the sub class the same name as your custom controller (ArrayControllerWithSearch). In the Attributes section of the Inspector (with the Classes tab still selected), add a new Action, called search:

From the Instances tab, select the NSArrayController for the list of names. In the Inspector, select Custom Class, and then select the new controller class:

ArrayControllerWithSearch subclass

Add a search control to the form. This will call the search_ method added to the controller.

Search control

Now, Ctrl-drag from the search control to the array controller in the Instances tab. This creates a connection. Select the search: action and then press the Connect button.

Congratulations! You now have a searchable list of birthdays for all your friends and family, integrated into your Address Book.

The complete example is available here and includes a working application bundle in dist/.

Watching NSEvents

In the midst of investigating how events are passed around within Cocoa and I found some interesting information. Based on the Apple Tech Note - Mac OS X Debugging Magic, it is possible to watch all the events as they are sent.

I was skeptical, but pleasantly surprised to discover that this still works if you have a PyObjC app, including one that is sym-linked! (ie. python setup.py py2app -A)

The magical incantation looks something like:


% defaults write com.mysite.MyApp NSTraceEvents YES
% MyApp.app/Contents/MacOS/MyApp

This will launch the app (MyApp) from the command line, with the events being sent back to the command line. Opening the application as per normal will have the events sent to the Console.

The following will turn of the feature:


% defaults write com.mysite.MyApp NSTraceEvents NO

The results look something like (line breaks added for clarity):


2005-07-27 18:35:57.599 MyApp [2790]
In Application: NSEvent:
type=KeyUp loc=(-395,428) time=152170.8
flags=0x180128 win=0 winNum=15994 ctxt=0xf45f
chars='ƒ' unmodchars='f' repeat=0 keyCode=3

Note: the com.mysite.MyApp bit is actually the value of the CFBundleIdentifier in the Info.plist file. See here for details on setting this up.

DrunkenBlog: On Being and Deliciousness, with Wil Shipley

Another brilliant interview over at DrunkenBlog. Go check it out. Wil Shipley speaks on software, Apple, Cocoa and life.

One of my favourite bits:

Making a simple user interface is a thousand times harder than a complicated one. I could literally write a skeleton of Delicious Library in a day, if I weren’t worried about how all the buttons and fields interacted or looked. Adding the fun to the app is the hard thing …

Opening an AddressBook entry with PyObjC

Having done some user testing on my DateList application, I discovered that they (me and my other test subject) expected something to happen when double clicking on an entry. It seems reasonable to me to open the person in Apple’s AddressBook as the action.

There doesn’t seem to be much information on this, however playing around with the birthday calendar (Tiger iCal feature), there is a URL that seems to work:


addressbook://E15E4D1E-AAE1-4AC2-85DB-98BB906EE988:ABPerson

Passing this to the open shell command opens the appropriate person in AddressBook.

The better question is how to construct one of these. To query an AddressBook entry, we first need to find one. AddressBook has a special reference for the current user called me.


>>> from AddressBook import *
>>> book = ABAddressBook.sharedAddressBook()
>>> book
<ABAddressBook: 0x1133d80>
>>> me = book.me()
>>> type(me)
<objective-c class ABPerson at 0xa4ac3034>

Now that we have a ABPerson, we can determine the UID. The UID property is actually on the base class, ABRecord.


>>> myUID = me.valueForProperty_(kABUIDProperty)
>>> myUID
u'E15E4D1E-AAE1-4AC2-85DB-98BB906EE988:ABPerson'

To open a URL, we need to construct a NSURL:


>>> from Foundation import *
>>> url = NSURL.URLWithString_('addressbook://' + myUID)
>>> url
addressbook://E15E4D1E-AAE1-4AC2-85DB-98BB906EE988:ABPerson

Finally, we pass the NSURL to the openURL_ method of the shared workspace. You get one shared workspace per application, and it can be used for useful things like opening urls, files, applications and some other services such as tracking changes.


>>> from AppKit import *
>>> ws = NSWorkspace.sharedWorkspace()
>>> ws
<NSWorkspace: 0x1134910>
>>> ws.openURL_(url)
1
>>>

openURL_ returns YES (1) if the URL was successfully opened, NO (0) otherwise. At this point AddressBook should open and have selected the entry that you have tagged as you.

Part 1 - Creating a basic CocoaBindings app with PyObjC

Python provides a clean, easy to learn, easy to develop in programming language. Cocoa provides a rich, easy to program framework for building applications on Mac OS X. Given that development should be easy, add PyObjC into the mix and the richness of Cocoa is available naturally from within Python. Now to see how easy it is to write a very simple application.

My first steps with attempting to create a Cocoa application with Python involved working through the tutorial on the PyObjC site. This is a very good place to start if you have an understanding of Cocoa. Some understanding of Python will also be helpful.

Note: If you are running on Tiger, you’ll need the latest version.

The most important thing to learn is how to build one of the sample applications:


python setup.py py2app -A

This will create an application bundle to run in the dist directory, using symbolic links to the code. As your source code is linked rather than copied, any changes you make are reflected in the bundle so you don’t need to re-deploy. Just re-run the application and you will be using the new code.

It is worth spending some time playing with some of the samples. The PyObjC team have done a very good job of putting together a wide range of examples. See /Developer/Python/PyObjC/Examples on your system. Try changing the files around a bit, I find it a useful way to learn when I break things by trying to add new stuff.

The first step in making your own application is to create a build script. This contains the information that py2app will use to generate your Cocoa application. A basic version looks like:


from distutils.core import setup
import py2app

setup(
    app=['main.py'],
    data_files=['English.lproj']
)

The app field is the name of the python file that will be called first. In some of the examples this is called __main__.py and in others it is the name of the application. I don’t like underscores so much, so I’m going with main.py.

The data_files field contains a list of directories in which data files are stored. For the moment, the only data file that you need to consider is the NIB files. These specify the interface for your application and are created by Interface Builder. If you wanted to load images and various other data, your entry might look like:


data_files=['English.lproj', 'data', 'images']

The next step is to create the main file, in this case, main.py:


from PyObjCTools import AppHelper

import DateListDelegate

AppHelper.runEventLoop(argv=[])

The first line imports the AppHelper which is then called to run the main event loop for the application. This does a range of interesting Objective C / Cocoa interactions behind the scene, but thanks to PyObjC, we don’t need to worry about that.

The second line imports the python file that we will now create, DateListDelegate.py.


from PyObjCTools import NibClassBuilder

NibClassBuilder.extractClasses('MainMenu')

class DateListDelegate(NibClassBuilder.AutoBaseClass):
    def items(self):
        return []

There are two important things that happen in this script. Firstly, our user interface is loaded using the NibClassBuilder. Secondly, we create a class to use in our interface which is going to provide us with a list of data. For the moment, we simply return the empty list [].

I’m not going to explain in detail how to use Interface Builder. What I am going to do is to work through what is required to make our simple application fly.

From Interface Builder, create a new Cocoa Application and store it as MainMenu in a sub-folder called English.lproj. You may need to create this folder if you haven’t previously.

Next, add an NSTableView to your Window. This control is available in the Cocoa-Data palette. The palette(s) are the bunch of controls that should turn up in the top-right of your screen.

Cocoa Data palette

If you select Test Interface from the File menu in Interface Builder, you should see your window with a completely empty box. Not particularly compelling yet, but one step at a time.

You can now run your application from the command line. Save the Nib and build a linked version. It is easier to use linked for now as we will be changing the python files a bit and it saves re-deploying each time you run the application.


% python setup.py py2app -A
% open dist/main.app

We now need to let Interface Builder know how to talk to the Python class we created earlier in DateListDelegate.py. In the MainMenu window, jump to the Classes tab and scroll left lots until you see the NSObject class. With that selected, from the Classes menu chose Subclass NSObject, and name your new class: DateListDelegate.

It is important that this class name exactly matches the class name that you create in Python, as some Cocoa magic happens behind the scenes to glue this all together.

With your new class highlighted, choose the Instantiate DateListDelegate option from the Classes menu. You should now have a funky blue cube in the Instances tab of the MainMenu window.

We need to have our class managed for us, so we’ll connect it to the application. To do this hold down Control then click and drag from the File’s Owner instance to the DateListDelegate instance. You should see the Outlets tab of the Connections view of the Inspector. From the Inspector, select the Connect button. This will connect our delegate class with the delegate property of the main application.

In order to join together the control on the Window with our Delegate class, we need a Controller object. This is where CocoaBindings really help us out, as you just need to drag a NSArrayController from the Cocoa-Controllers palette to the Instances tab of the MainMenu window.

Building blocks

Now we get up to the fun part, gluing it all together!

Firstly, we need some test data to play with. In a future article, I’m going to look at populating this from an external source, but for now we are just looking at the user interface, so we can fake it.

Open up the DateListDelegate.py file and update the items method to look something like:


def items(self):
    return [
        {'date': 1980, 'name': 'John Doe'},
        {'date': 1970, 'name': 'Amanda Smith'},
        {'date': 1960, 'name': 'Bill Branson'}
    ]

The data representation is how the Key-Value pairs, used by CocoaBindings, are constructed in Python. This is something that will be covered in more detail when we look at generating the structure from real data. If you want more information, see the Key-Value Coding section of the Apple developer site, or look through the examples in PyObjC.

The keys in our example are date and name, so we’d better let our Controller know that these are the fields it is going to be looking after.

In the Attributes section of the Inspector (Tools->Show Inspector) with the NSArrayController selected, there is an Add button. Add two keys, and rename them to the names that we have assigned in the data we are providing, date and name.

Not much use, unless our controller knows how to access our data. This is achieved from the Bindings view of the NSArrayController in the Inspector.

Drop down the contentArray property, and select “File’s Owner (NSApplication)” as the Bind to: value.

Then enter the following into the Model Key Path:


delegate.items

You should now see the following in the Inspector:

Binding the delegate

If you try running the application now, you’ll notice we are not quite there yet. Our controller knows about the data, our table knows about the array that populates it, but the columns don’t know what to show.

Double click on the title area of the first column in the NSTableView that is on the Window. This selects the actual table column, and also allows you to enter in a heading. Call the first one Name.

Then in the Bindings view in the Inspector, drop down the value property and select name from the Model Key Path: combo box. You should notice that our key names are pre-populated in this combo box, thanks to the binding to the controller.

Repeat the process for the second column, binding that to the date key.

Our application can now be tested, simply by running it from the command line:


% open dist/main.app

Try clicking on a column title and your list should automatically sort, both by name and by date. All without writing any sorting code, or any event code for the click to select columns.

Completed application

To build a distributable version of the application, re-build without the symbolic link option:


% python setup.py py2app
% open dist/main.app

This will create an application bundle that can be deployed to a Mac OS X machine that doesn’t include PyObjC. This means that your user has no way of knowing that you’ve built a Cocoa app without writing a line of Objective C code.

Where to from here? It is worth looking through the examples included with PyObjC as they cover a wide variety of programming topics in Cocoa. In the next article, we’ll look at adding some actual features so it deserves to be called an application.

The source code for the sample application from this tutorial is available here.

Additional articles

Part 2 - Integrating Address Book

Part 3 - Making it look like an application

Bonus link

Birthday Notes - finished, open source app, based on these articles