As you may have noticed, Xcode’s NSWindow
does not actually have a single representedURL
; it has two, the project and the current in-project file.
So how did Apple achieve that?
First things first, you can start figuring that out by yourself by asking your window’s contentView
for its superview. Although undocumented, enough apps seem to be playing around with this (including Xcode) that I presume Apple would be reluctant to change something important here.
Second, I’m not going to actually show you any code just yet. Perhaps in a future blog post; I did start writing something, but it’s unfinished.
Third, there is no third: let’s get to it.
Hacking tools
Playing with contentView
‘s superviews can only get you so far (although, you may be able to hack something anyway). Injecting code in form of plugins into Xcode is possible, as proven by JugglerShu’s XVim.
But there’s a nicer way.
Say hello to F-Script. It’s a nice scripting language and let’s-call-it mini-IDE in itself, but there’s something far more powerful it can do.
On F-Script
It has a full fledged object browser that uses introspection to figure out what the hell exists in the view hierarchy. It also has a nice view picker so you can easily access the exact view that you’re playing with. Python may be neat for programming, but this one is just smashing for debugging GUI apps.
F-Script is not intended to be used as a standalone dev environment however; you’re supposed to put it into your app and use it at runtime. Kind of like what you can do with Python already… except this one comes with a runtime debugging GUI out of the box (and the aforementioned view picker!), and is pretty dedicated to playing with Cocoa and Objective-C (while Python can play with it through its PyObjC bridge – powerful, but not the best way to do it; also, method names become lengthy and weird when written in Python).
Note that I, for now, have absolutely no experience with F-Script as a programming language, and I don’t intend to learn it too much for now, just as I don’t intend to learn all nuts and bolts of GDB. It may be nice, but for now, it’s far, far, far more amazing as a debugging tool.
Okay, so where does playing with Xcode come in?
Unfortunately, not in the form of injecting the debugging code into Xcode and playing with it. This is the loveliest part of F-Script, however; you can inject it into any app and inspect how it works, see runtime class definitions, send messages to existing objects, etc. But not with Xcode, because of some magic that Xcode is doing causing F-Script to crash Xcode.
Side note: injection of F-Script is done via gdb. The F-Script Anywhere automator workflow intended to be put into ~/Library/Services
actually consists of figuring out frontmost app’s process ID, constructing some gdb commands and running gdb. gdb commands used involve attach
to existing process, then calling Objective-C method -[NSBundle load]
on /Library/Frameworks/FScript.framework
, and finally calling +[FScriptMenuItem insertInMainMenu]
and detach
ing. Quite ingenious!
Finding the beast
Let’s get back to business. Although Xcode crashes, view inspector of F-Script works long enough to let us know the name of the class implementing the custom view Apple uses in the titlebar for displaying two “represented URLs”. A-ha! DVTDualProxyWindowTitleView
, we’ve found you!
Now, where could this be defined? Let’s explore various private frameworks found in Xcode using class-dump
(install it from MacPorts). And voila! Xcode.app/Contents/SharedFrameworks/DVTKit.framework
. Using class-dump
we can also see that it’s used in… DVTDualProxyWindow
, a subclass of NSWindow
. Wonderful!
Now, just trying to load this framework into standalone F-Script.app
fails miserably and returns NO
. At least for me. otool -L
told me which frameworks this one depends on… so I loaded them first.
About the beast
Finally, DVTDualProxyWindow
is a subclass of NSWindow
which apparently overrides -setTitle:
to do nothing, overrides -setRepresentedURL:
to be used for the ‘project’ URL, and defines new method -setSecondaryRepresentedURL:
to add the ‘document’ URL. Both of these methods are just talking to DVTDualProxyWindowTitleView
. We don’t really care what happens behind the scenes; it’s just a regular view. But let’s see if it works.
Open F-Script app. Right click on the toolbar and choose ‘Customize’. Aside from customization sheet, a new window appears titled ‘Custom Buttons’. Pick one of ‘CustomX’ buttons, and select ‘Block…’. (Of course, prior to that, in the toolbar customization sheet, drag the picked button to the toolbar… you know, so you can click it.)
Now, paste in the following code.
Testing the beast
[:selectedObject |
(NSBundle bundleWithPath:'/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework') load.
(NSBundle bundleWithPath:'/System/Library/PrivateFrameworks/DataDetectorsCore.framework') load.
(NSBundle bundleWithPath:'/System/Library/PrivateFrameworks/DataDetectors.framework') load.
(NSBundle bundleWithPath:'/System/Library/Frameworks/SecurityInterface.framework') load.
(NSBundle bundleWithPath:'/System/Library/Frameworks/Carbon.framework') load.
(NSBundle bundleWithPath:'/System/Library/Frameworks/CoreServices.framework') load.
errorPointer := FSObjectPointer objectPointer.
(NSBundle bundleWithPath:'/Applications/Xcode.app/Contents/SharedFrameworks/DVTKit.framework') loadAndReturnError:errorPointer.
" printing out an error:
errorPointer at:0.
"
"win := ((DVTDualProxyWindow alloc) init)."
win := DVTDualProxyWindow alloc initWithContentRect:(125<>513 extent:383<>175)
styleMask:NSTitledWindowMask + NSClosableWindowMask + NSMiniaturizableWindowMask + NSResizableWindowMask
backing:NSBackingStoreBuffered
defer:NO.
(win setRepresentedURL:(NSURL fileURLWithPath:'/Applications/Xcode.app')).
(win setSecondaryRepresentedURL:(NSURL fileURLWithPath:'/Applications/Xcode.app')).
"Instantiate a button, put it in the window and configure it"
button := NSButton alloc initWithFrame:(247<>15 extent:90<>30).
win contentView addSubview:button.
button setBezelStyle:NSRoundedBezelStyle.
button setTitle:'Boo'.
button setKeyEquivalent:'\r'.
"An example of using F-Script blocks to handle click on button. Ignore it."
conversionScript := [(form cellAtIndex:2) setStringValue:(form cellAtIndex:0) floatValue * (form cellAtIndex:1) floatValue].
" From docs: "
"The [...] notation creates an object of class Block which represents a block of code that can be executed later (Block is an Objective-C class provided by the F-Script framework). In our block, we simply get the values of the fields in the user interface objects, perform the computation (simply involves multiplication) and put the result in a UI element.
"
"An example on how to add handling for button click. Ignore it."
"Make the script the target of the button.
The script will be evaluated when the user presses the button"
button setTarget:conversionScript.
button setAction:#value.
"Show window"
(win orderFront:nil).
]
Click on the ‘Run’ button, and type in ‘nil
‘. (You don’t really care about the selectedObject
, but I did not study F-Script long enough to avoid it.)
Hopefully this gives you enough idea to debug with, explore with, as well as an idea on how to implement dual-representedURL windows. Good luck and have fun! 🙂