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.

November 9th, 2005 at 1:54 am
Could you please show the final code? I’m still lost.
November 9th, 2005 at 6:33 am
My code includes the following for selecting a file to export to. The first function is called from a menu item. The second function is called back when the sheet is closed.
Not many of lines of code required, but it took me a lot of reading and tinkering over a few days to get there. The confusing bit for me was getting the callback to work properly, but then callbacks can be confusing in themselves.
The version I’m using requires Python 2.4 to be installed.
November 9th, 2005 at 11:46 am
I’m getting close, this code brings up the sheet but hangs after file
choice (and doesn’t show the default)
def doSaveSheet_(self, sender):
sp = NSSavePanel.savePanel()
sp.setRequiredFileType_(‘txt’)
sp.setNameFieldLabel_(‘Export As:’)
sp.beginSheetForDirectory_file_modalForWindow_modalDelegate_didEndSelector_contextInfo_(NSHomeDirectory(),
‘default.txt’,NSApp().mainWindow(),self,’didEndSheet:returnCode:contextInfo:’,0)
return
def didEndSheet_returnCode_contextInfo_(self, sheet, returnCode, info):
if returnCode == NSCancelButton:
return
didEndSheet_returnCode_contextInfo_ = objc.selector(
didEndSheet_returnCode_contextInfo_, ‘v@:@ii’)
# sheet.filename() contains the selected file
#print sheet.filename()
November 9th, 2005 at 6:03 pm
There were two mistakes I made in the Python 2.3 examples. I have updated the main post with the fixes.
The first is that the objc.selector line needs to be at the same indent level as the method that you are annotating.
The second concerns the arguments passed to objc.selector. I missed the ‘signature=’ key.
The following is a working (and tested) example:
Hope this helps.
July 13th, 2007 at 7:49 am
[…] Unfortunately, the documentation for how to do this is scattered around. Examples are mentioned in passing in the introduction and How to wrap an Objective-C class library documents, so I got the idea that the selector and signature functions in the objc module were involved. So I fired up the Python interpreter and typed print objc.selector.__doc__ and print objc.signature.__doc__. But the most helpful reference was Jonathan Saggau’s Showing a NSSavePanel as a sheet, a blog posting that described solving a similar problem with an NSSavePanel callback. […]
January 30th, 2008 at 5:39 am
Actually, I think that the type signature being used is wrong but it works because you aren’t accessing the third parameter ‘contextInfo’. which is supposed to be pointer to void but your signature states is an integer.
According to the NSOpenPanel documentation, the callback signature is:
-(void)openPanelDidEnd:(NSOpenPanel *)panel returnCode:(int)returnCode contextInfo:(void *)contextInfo
So that’s a selector called by an object returning a void ‘v@:’ with a signature consisting of three parameters: an object, and integer, and a pointer to void. According to the Apple Type Encodings then
(http://developer.apple.com/documentation/Cocoa/Conceptual/ObjectiveC/Articles/chapter_13_section_9.html#)
the correct signature would be:
@objc.signature(‘v@:@i^v’)
and not:
@objc.signature(‘v@:@ii’)
But as I said, since you don’t try to access ‘contextInfo’ it probably doesn’t matter in this case. But this does prove something however…that I have the same “I need to know why’ disease you do
;-)
February 2nd, 2008 at 1:43 pm
that doc link is busted… I think Apple rearranged their site or something. I’ve been looking for the doc of this string for a while now. Any ideas where it might be now?
February 25th, 2008 at 9:55 pm
Try here for updated docs.
August 18th, 2009 at 2:12 am
That updated link didn’t work for me. I think the article is now at http://developer.apple.com/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html