Macdoc is the new Mono API documentation browser build entirely with MonoMac (Cocoa bindings for .NET). It has been recently shipped as part of the latest MonoDevelop beta for Mac users where it replace the excellent GTK+ version we use everywhere else.
It was my first time hacking on any MonoMac app and along the way I came up with a couple of pieces of code that, I think, could be used as general recipes for MonoMac development.
Apple docs being severely lacking (or plainly useless) in some aspect of Mac development, some recipes also covers some general Mac constructs. MacDoc being a NSDocument-based application, recipes are given with respect to that style of Mac coding.
So, among the menu today we have:
- Answering Open URL commands
- Escalating privileges
- Uncompressing .xar archives
- Fighting WebView or how to handle image requests yourself
- Redirecting printing to a WebView in a NSDocument application
Answering Open URL commands
Any Mac application after being started can receive an number of external signal called Apple events that are sent by other processes to ask the application to do something.
One of these Apple Events called
GURL (the four chars code for “Get URL”) is a way for the application to receive URL request it can open from the outside world. So, for instance in MacDoc case, we respond to
monodoc:// that MonoDevelop send to update the documentation page we are showing to the user.
To answer these Apple events, there are a number of existing API. One of them is Carbon (part of the low level suite of Apple API) and an example of its usage can be seen in MonoDevelop and the GTK+ version of MonoDoc (e.g. MacInterop folder in MonoDevelop Mac addin).
But more interesting in the context of Cocoa application, there is a Objective-C wrapper called
NSAppleEventManager around these C calls that is available as part of MonoMac master in the
If we return to our “Get URL” example, to let the system know that you can handle certain types of URL, you first need to set up some lines in your application
Info.plist file. This will let tools like
open (and more generally anything using the Launch Service API) automatically load your application when it’s used with an URL your are able to open. In MacDoc case, I paste below the relevant
Info.plist lines for the two
monodoc:// URL schemes we support:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Now to catch an event, we decorate a callback method with the
[Export] attribute and an Objective-C selector name. In MacDoc case we define the handler this way:
1 2 3
I will show in a minute how you can process the arguments of this callback to extract the URLs. For now, let’s see how we register this callback with the event system via the
1 2 3 4 5 6 7 8
As you can notice, the call to
NSAppleEventManager is made inside the
NSApplicationDelegate.WillFinishLaunching override. In the Cocoa application startup cycle, this method is called when all default handler have been setup but no event has been processed yet making it a good candidate to register our own event handler.
Now, let’s see how we can extract the received URLs from the event data. Apple events can have complex data attached them ranging from simple number/string to nested list of them.
For the “Get URL” event, the URLs are stored in a simple list of string which you can process in normal for loop:
1 2 3 4 5 6 7 8 9 10 11 12
As you may have noticed, elements index inside a list are 1-based and not 0-based like we are used to.
Mac OS X being a UNIX operating system, it has a clear separation of privileges based on the traditional user mode system where
root is the only account that can do anything on the system.
Although most applications are just fine running under a normal user, you may sometimes need to escalate privileges if at some point you want to, for instance, write files to a protected system directory.
There are two ways to do so, either upgrade the running process to a better user or launch an external process with better privileges than the calling one. Apple preferred way seems to be the second option (although they won’t really like an app that needs to be fully run as
root in their Store anyway).
For Linux users, this is akin to using a
sudo GUI (e.g.
gksudo). Apple provides its own way to do the same thing with a C API of their own called SecurityFramework (and it’s Objective-C brother Security Foundation).
SecurityFramework, is a very much stupid low-level C API and although there is an Objective-C wrapper, it’s just an excuse for “Sorry we can’t do any better”.
Thankfully, we have the
AuthorizationExecuteWithPrivileges shortcut call that make launching an external root process a bit easier. Be careful though because it was deprecated in Lion.
The following class and its
LaunchExternalTool method let you start an external application as if run by the root user:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
Uncompressing .xar archives
Xar is an extensible archive format using an XML document to keep its inner filesystem information. It’s used by a lot of stuff distributed by Apple like, in MacDoc case, all their documentation bundles.
Xar is available by default on every Mac OS X installation and it has a very simple and straightforward API available in the libxar library making it really easy to bind.
In our case, we just needed to be able to decompress a Xar archive where we wanted which is easily achieved with the following class and its
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
Process is pretty simple really, the only catch being that xar extract archive filesystem based on the current working directory which is why we adapt this setting to the given parameter. You thus need to be careful if for some reason you want to execute more than one call concurrently.
Fighting WebView or how to handle image requests yourself
WebView is the widget which can display any kind of rich HTML using Webkit. It has some integration with the embedder, letting it decide how the WebKit engine should handle some operation (e.g. link navigation). What it doesn’t let you do though is handle linked content request yourself i.e. any resource that needs to be downloaded separately (images, external scripts, , …).
What we couldn’t act on however were the internal images referenced from documentation as WebView doesn’t let you catch Web request and feed on your own data. Thus we needed a way to trick the system to let us inject our own bytes at some point.
As I said earlier, for convenience we try to inline as much stuff as possible, so taking that reasoning to images, we needed to do the same thing for them. Enter the Data URI scheme which let you embed raw data (or well, base64 encoded data) directly as content.
What we can thus do with our WebView is to hack on the DOM when the page has loaded to detect places where a
<img /> tag referencing an internal image resource is used and fill that tag with raw data taken from our store.
If we want to manipulate the DOM, we need it to be properly initialized. A way to do that is to hookup our method to the
FinishedLoad event which tells when the DOM is ready for consumption for the currently loading document (remember that loading a page is asynchronous in a WebView).
The method itself which fetch image tags and fill them with our data is given below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
The two application-specific parts of this method are how we recognize a tag pointing to an embedded image (in our case their
src attribute value is prefixed with “source-id”) and the call which get us a
Stream for the image (here we get it from our documentation bundle). The rest is mostly stream reading boilerplate.
Redirecting printing to a WebView in a NSDocument application
Having a NSDocument-based application means a couple of standard operation are made easier for you to use. One of them is printing which simply require a couple of plumbing with most of the heavy lifting left to Cocoa.
What you may want to do however is to only print a part of your UI, generally the one that display the actual document content which is not something Cocoa can guess for you.
In MacDoc case, the
WebView that show the API documentation is what we are interested in printing so let’s see how can redirect global print request to that specific part of the application.
First, we need to register an action on the Print menu item that points to our subclass of
NSApplicationDelegate. In the action implementation, we will simply redirect the call to the currently activated document:
1 2 3 4
Now by default, this will actually try to print the currently focused widget in your document UI which is most of time not something you want (feels weird to print a button).
As told earlier, most of the time what you rather want is to print a specific part of the UI. Fortunately, almost all widgets that display rich content also have an implementation of a print operation.
WebView is no different and this the widget we are going to use as an example.
So, to redefine the printing behavior of a document, the first thing you need to do is to override the
PrintOperation method in your
Then, your simply fetch the current
FrameView displayed by your
WebView and proxy the print operation through it. This could be something like this:
1 2 3 4 5 6
NSDictionary parameter is a raw representation of a
NSPrintInfo so it’s enough to simply instantiate the later with the former. The returned print operation will contains what’s necessary to print the rendered HTML shown by the