Archive for Cocoa

Showing a NSSavePanel as a sheet

Using a sheet to display a save panel adds for a significant user interface improvement. Based on the simplicity of a range of other Cocoa features, I was surprised at how tricky it was to get working.

The issues had nothing to do with Cocoa per-se, but with the PyObjC bridge.

Creating the panel and creating the sheet is as follows:


sp = NSSavePanel.savePanel()
sp.setRequiredFileType_('ics')
sp.setNameFieldLabel_('Export As:')

sp.beginSheetForDirectory_file_modalForWindow_modalDelegate_didEndSelector_contextInfo_(
    userDocumentFolder(),'default.ics',
    NSApp().mainWindow(),self,
    'didEndSheet:returnCode:contextInfo:',0)

The tricky bit is the asynchronous callback, which is passed in as an Objective C selector: 'didEndSheet:returnCode:contextInfo:'

In Python terms, the following method does the trick:


def didEndSheet_returnCode_contextInfo_(self, sheet, returnCode, info):
    if returnCode == NSCancelButton:
        return

    # Save the file returned by sheet.filename()
    print sheet.filename()

I only had one slight problem. The application crashed when the user clicked a button on the sheet.

The issue is that the Python bridge has no idea as to the type of the arguments in the method listed as the callback from the sheet. When the callback occurs from Cocoa, it goes searching for a method which matches a specific type signature, as documented in NSSavePanel.

An explicit type signature is required for the method to be discoverable. PyObjC provides a few ways of doing this.

Note: the name of the callback function is up to programmer, which is why the type signature is important.

The Python 2.4 version (using decorators) looks like:


from objc import *

@objc.signature('v@:@ii')
def didEndSheet_returnCode_contextInfo_(self, sheet, returnCode, info):
    if returnCode == NSCancelButton:
        return

    # Save the file returned by sheet.filename()

With the standard version like:


from objc import *

def didEndSheet_returnCode_contextInfo_(self, sheet, returnCode, info):
    if returnCode == NSCancelButton:
        return

    # Save the file returned by sheet.filename()
    print sheet.filename()
didEndSheet_returnCode_contextInfo_ = objc.selector(
        didEndSheet_returnCode_contextInfo_ , signature='v@:@ii')

With the type information added in, the callback can locate the method on the object and all is well in the world. Files are saved, memory protected, everybody is happy.

Almost.

I had a simple question, where did this type string come from?

Searching on Google had resulted in two different strings. Both worked. However they were quite different.


@objc.signature('v16@4:8@12i16i20')
@objc.signature('v@:@ii')

Somewhere, there had to be an explanation for all of this.

The PyObjC documentation provided some good hints. The intro mentions the different syntax for specifying the type signature, and provides an example.

Using a convenience method on AppHelper, the type information does not need to be explicitly included in the code for the endSheet case:


from PyObjCTools import AppHelper

# Python 2.4
@AppHelper.endSheetMethod
def didEndSheet_returnCode_contextInfo_(self, sheet, returnCode, info):
	pass

# Python 2.3
def didEndSheet_returnCode_contextInfo_(self, sheet, returnCode, info):
    pass
didEndSheet_returnCode_contextInfo_ = AppHelper.endSheetMethod(
        didEndSheet_returnCode_contextInfo_)

This third option provides cleaner code, but still didn’t explain where type signatures come from.

(I must have been a difficult child, a solution isn’t enough, the need to know why/how is the motivator)

The key came in reading through the documentation on wrapping an Objective C class. When the documentation refers to a ‘raw Objective-C method signature’, it means what it says. The string appears to be Objective-C’s internal representation of the type signature.

Apple provides documentation on the syntax. (Note: Try here for updated docs)

Probably a bit further into Objective-C than I wanted to delve, but much was learnt along the way. A good abstraction allows you to peek through it. I’m happy to learn that PyObjC provides that flexibility, and that most of the time, it isn’t needed.

Pimp My Code, Part 5: In Python

Wil Shipley continues his Pimp My Code series with an article reviewing what should have been a very simple method to return a path inside the Application Support folder.

Well worth a read, both for the stylistic as well as the comedic commentary.

From my perspective, it is also a useful source of learning about Cocoa from one of the better programmers.

In Python, the final example looks something like:


from Foundation import *

def applicationSupportFolder():
    return NSSearchPathForDirectoriesInDomains(
        NSApplicationSupportDirectory, NSUserDomainMask, True
    )[0].stringByAppendingPathComponent_(
        NSProcessInfo.processInfo().processName())

Note: The code has been reformatted to (attempt to) fit your screen.

Saving a file using NSSavePanel

Saving a file was something I’d been avoiding for a while, as I hadn’t been able to find the right control in Interface Builder to make it happen. This had led me to believe it was going to be difficult and was delayed for a long time.

Today, I bit the bullet and spent some time to figure this out. My suspicion was that there existed a control that would do the work for me, as file handling is quite consistent from a UI perspective in Mac OS X. Reading through Cocoa Programming didn’t provide any enlightenment, so I went back to the XCode documentation.

By this stage of my Cocoa learning, I really should have known better. Apple’s documentation has solved too many problems for me to be consulting it as a last resort. Unfortunately, I suspect it is a lesson I will have to keep learning.

Apple provides this information about the NSSavePanel class. Finding this class was my ‘aha!’ moment for the day. No wonder I couldn’t find it in Interface Builder, it isn’t an IB control in the normal sense.

In Python, the simple version looks something like:


from AppKit import *

def saveFile_(self):
    sp = NSSavePanel.savePanel()
    sp.setRequiredFileType_('ics')

    if sp.runModal() < = 0:
        return

    print 'Filename: ' + sp.filename()

This will throw a standard file save modal dialog box and ask for a filename to save. There are a bunch more options on the NSSavePanel class, which I may explore later. The interesting part will be making the save panel appear in a sheet.

Something to explore later.

Re-opening the main NSWindow

I was faced with what seemed a very simple problem, when I closed the window on my application, I didn’t have a way to open it again. For a document based application, it isn’t too much of an issue, the framework allows you to open or create a new document.

Mac OS X is a bit divided at the moment on this functionality. There are some single window apps that behave this way, and others that close themselves when you close the main window. System Preferences is an example of this.

For my application, I wanted it to have a single main window, and I wanted the user to be able to click on the dock icon and have it re-open if it wasn’t already there. Same as Mail, Address Book, iTunes and others.

The trick to making this happen is the applicationShouldHandleReopen:hasVisibleWindows: method of your application delegate. This method is called with the NSApplication and a flag to indicate whether or not any windows are visible for the application.


def applicationShouldHandleReopen_hasVisibleWindows_(self, theApp, hasVisibleWindows):
    if hasVisibleWindows:
        return True

    for win in theApp.windows():
        # Autosave Name set in .nib
        if win.frameAutosaveName() == 'mainWindow':
            win.makeKeyAndOrderFront_(NSNormalWindowLevel)
            return False

    return True

To be honest, my first version was a bit simpler again, the for loop looked remarkably like:


    win = theApp.windows()[0]

Too many years programming and I couldn’t leave the [0] alone. Given that there could be more than one window in my application, I needed a way to identify the window that I needed to open. Cocoa provides such a feature, the AutoSave name for a window. This property can be set in Interface Builder.

Simply give your main window a name, I chose ‘mainWindow’, and search for a window with that name.

Once you have a handle on the NSWindow, you need to call makeKeyAndOrderFront:. This method makes the window the Key window, which gives it keyboard focus and makes it visible. The method requires a window level and given that the main window isn’t doing anything strange, I used NSNormalWindowLevel.

The test for visible windows is a bit of a shortcut. If there is already a visible window, I don’t want any extra ones showing up, so I return True and let Cocoa deal with the Dock click (or Application open) as it normally would. If I did open a window, then I return False, as there is nothing further for Cocoa to do.

In the end, a simple solution to a simple problem.

PyObjC article available over at ADC

Apple has recently posted an introduction to PyObjC over at ADC. The article is courtesy of the Red Shed and even includes movies of the Interface Builder bits.

This is a sensational result for the PyObjC team for several reasons. First up, there are likely to be more developers writing PyObjC applications, which will result in an even larger community. Secondly, and perhaps more importantly, the article provides an official looking stamp for the community

My congratulations to all involved, both in terms of making PyObjC a reality and in terms of producing a Ruby on Rails style piece of marketing!