Thursday, July 24, 2008

Hillegass Ch. 2

As I promised in my very first post, I want to show some simple examples exploring Cocoa using PyObjC. Make sure you have the Developer Tools installed (either from a Leopard install disk, or from here---choose the free ADC Online Membership). I'm using the 3rd Edition of Hillegass. It has a few new chapters but otherwise appears very much the same as the 2nd Ed.

1. In XCode, do New Project => Cocoa-Python Application.

2. From the outline view select Resources => and double-click on MainMenu.xib to start Interface Builder.

3. Lay out the GUI. Here we need two buttons labeled "Seed" and "Generate", and an NSTextField. These objects are found in the Library window, which can be navigated using an outline view. The text field is under Views & Cells => Inputs & Values. Just drag them to the Window we are building. Save.

4. Click on the XCode Project window to go back to XCode and create a controller class. From File => New File... select Cocoa => Python NS Object subclass and call it MyController.py. Add the following code to the class replacing the pass statement:

class MyController(NSObject):
myTextField = objc.IBOutlet()

@objc.IBAction
def seed_(self,sender):
NSLog("seed")

@objc.IBAction
def generate_(self,sender):
NSLog("generate")

The file for the class will be under whatever pane was selected in the outline view when you did File => ... above. Move it to Classes. Open PyXAppDelegate.py there, and add an import statement for your class:
import MyController


Save.

5. Create an instance of the Controller in IB by dragging a controller object (blue cube) from the Library to the window MainMenu.xib. Select it (it's labeled "Object"), then go to the menu and do Tools => Identity Inspector (or double-click and press the I). Rename the class "My Controller." The name should auto-complete. IB is aware of what is present in our XCode project. In fact, one advantage of step 4 is that IB can use the decorators to help us set up the connections between the GUI elements. Not only the myTextField outlet but also the two actions "seed:" and "generate:" should be visible. If not try saving.

6. Now just control-drag to set up connections. Hillegass tells you to consider "which object needs to know about the other one," which I found pretty confusing. Instead, think about the direction in which information must flow. It will go from the "seed" button (when pressed) to the "MyController" object. Ditto for the "generate" button. And information will go from "MyController" to the text field. When you do these control-drags a little black window should come up to allow you to select the correct action or outlet. Save.

7. Go back to XCode and do Build And Go. If there is an error, look at Run => Console for hints. One issue I had is that XCode inserted a tab rather than four spaces in the decorator lines above. Check for consistency by doing View => Text => Show Spaces.

Another problem I had was with the decorator @objc.IBAction. When I ran into this before, I added the IBOutlet() call to a completed project (from the old XCode / IB), and I didn't actually use the IBAction part, and the code worked. Here, I am setting up the project from scratch, and it helps a lot to let IB know what methods we have in our controller. But when I made a mistake and used (), like you would for the outlet, I got the error shown in the console:

TypeError: IBAction() takes exactly 1 argument (0 given)


I tried a few things (like adding self as the argument, but self isn't known since we don't have an __init__ here). Then I remembered that decorators don't normally look like that, and the argument objc.IBAction is receiving is just the name of the function. So I removed the parentheses:

          
@objc.IBAction


That works fine. Of course, if you actually want the program to do something, you need to flesh out the class:

class MyController(NSObject):
myTextField = objc.IBOutlet()

@objc.IBAction:
def awakeFromNib(self):
NSLog("MyController-awakeFromNib")
now = NSCalendarDate.calendarDate()
self.myTextField.setObjectValue_(now)

@objc.IBAction:
def seed_(self,sender):
NSLog("seed")
#random.seed(time.time())
random.seed()
self.myTextField.setStringValue_("seeded")

def generate_(self,sender):
NSLog("generate")
number = random.choice(range(1,100))
self.myTextField.setIntValue_(number)

Just remember that PyObjC is an open source project. There is a lot of old (unfortunately undated) documentation around, like this stuff, much of which doesn't seem relevant any more, that no one has the time to weed out. But, there are still a few pearls, like this and this. Anyway, one of my objectives is to work through all this using the modern tools and approaches, and figure it out.