Hi everyone,I spent the last day or so integrating native OS X open and save panels into my PyGTK application using PyObjC, and I thought I'd share my experience here in the hope that someone may find it useful. The specific relevance to PyGTK is a crash I found that looks due to an interaction between PyObjC and PyGTK, and a shim to avoid it, detailed below.
- Note that I am not an Cocoa / OS X expert, nor an expert on the GTK internals, so take all this with a grain of salt. It works for me but YMMV. Any corrections or
suggested improvements are welcome. * Saving filesAs the NSSavePanel is not responsible for providing a file format selector, I used the AppKit NSDocument to build one for me.
+ve Automatically provides format selection options from data in the Info.plist file. +ve The NSSavePanel can be modified if necessary by the user via the - NSDocument prepareSavePanel: hook
First we define our custom NSDocument, whose name should be referenced by the Info.plist NSDocumentClass key within the appropriate format definition, and which is necessary for the AppKit framework to correctly filter the displayed files.
As we are only interested in using the NSDocument to provide a save dialog,
we overrride the Document's save method and stash the filename instead. (It's a little sloppy to avoid intialising self.chosen_filename in a constructor but that turned out to be more trouble than it was worth for my current purposes.) class MyDocument(AppKit.NSDocument):def saveToURL_ofType_forSaveOperation_delegate_didSaveSelector_contextInfo_( self, absoluteURL, typeName, saveOperation, delegate, didSaveSelector, contextInfo):
assert absoluteURL.isFileURL()self.chosen_filename = absoluteURL.path() # initialise this to None first!
import objc# The interface requires a delegate, which we do not use. Supply a dummy.
class Delegate: # Specify void return, self object,# taking a selector signature of types object, bool, pointer to void
objc.signature('v@:@B^v')def document_didSave_contextInfo_(self, document, didSave, contextInfo):
# Note: Never called: only called by saveToURL_ofType_..., # which we override above. pass delegate = Delegate()# Note: the python method name specifies a selector (which we have annotated # with the appropriate type information expected by Objective-C). Therefore # we cannot change it to, say, 'callback' as that selector takes no arguments. # We would prefer it if we could specify a null-delegate and null- selector
# somehow as that would obviate defining our delegate above. sel = objc.selector(delegate.document_didSave_contextInfo_) _document = MyDocument.alloc().init() _document.chosen_filename = None_document.runModalSavePanelForSaveOperation_delegate_didSaveSelector_con textInfo_(
AppKit.NSSaveAsOperation, delegate, sel, 0) return _document.chosen_filename * Opening filesI chose to use the AppKit NSDocumentController class to create and configure the NSOpenPanel.
+ve Configures file extension filters automatically from data in the Info.plist file. -ve User is apparently unable to configure the NSOpenPanel, e.g. to disable multiple file selection.
Using PyObjC, having defined MyDocument above, this required three lines of code:
import AppKit d = AppKit.NSDocumentController.sharedDocumentController() return [url.path() for url in d.URLsFromRunningOpenPanel()]The returned urls appear to always start with file://localhost/ although I couldn't find confirmation of this in the Apple Documentation.
--------------This was all well and good until I discovered that my application crashed when PyGTK tried to invoke some of its signal handlers during these appkit invocations. Specifically, the tstate variable passed to PyFrame_New upon handing a gobject signal emission within python caused a KERN_PROTECTION_FAILURE (0x0002) at 0x00000008. I assume this is due to some interaction between PyGTK and PyObjC.
To explain how it can occur requires a little background on the event loop in gtk under quartz.
A standard cocoa application receives events asynchronously, which are placed on a queue to be processed by the main run loop in the NSApp singleton (which uses a CFRunLoop object). GTK-quartz bolts on its event loop to this by driving the NSApp's event loop itself and using it soley as a conduit for events. Events relevent to GTK are processed and the rest passed on to the NSApp as if it had received them from the NSApp runloop directly.
However, the NSApp passes control to its main event loop when the Application window is being resized - or when requested to run a modally, which (presumably) the open and save panels do. GTK-quartz would be cut out of the loop here, so to speak, if it didn't also observe the NSApp main event loop's CFRunLoop, allowing GTK to continue to receive events and, e.g., redraw its windows during window resize.
This also, though, allows events that trigger PyGTK callbacks to be processed during a PyObjC call, which, for whatever reason, looks to be behind the crash.
My rough and ready fix has been to prevent this by disabling that monitoring. The crash disappears, at the cost of GTK not redrawing until the resize completes, or otherwise responding during modal event loop episodes. Unfortunately I can't afford to schedule the time to fix the root cause of this issue myself.
regards, Richard.Versions: ige-mac-integration version 0.9.4, python 2.5.2, OS X 10.4, PyGTK 2.16.0, GTK 2.18.2.
Attachment:
disable_gtk_runloop_observer.patch
Description: Binary data