QuickLook is the Apple technology by which one can get a large, “instant” preview of a file before opening it. You may be familiar with it from the Finder. Select an item in your system and press the Space bar to get a resizable panel that plays movies, music, displays multi-page PDFs, richly formatted text documents and more. It has come to be part of the general user experience starting with OS X 10.5 and for a program like file_wrangler_2 I believe it is a must-have feature. A user will not always remember what a file is by name alone, so some method for taking a quick peek at the file before renaming addresses a feature-request from the first file_wrangler.
Apple provides some good documentation for QuickLook, especially with regard to how to create new QuickLook “generators”, the system-level plugins that teach the Finder how to create those nifty previews. You may notice that things like InDesign documents do NOT give previews in QuickLook because there is no QuickLook generator provided by Adobe to support this function. Illustrator files work because Adobe provides a PDF representation and QuickLook supports PDF content “out of the box.” Look in /System/Library/QuickLook and /Library/QuickLook to see which generators are installed on your system.
With 10.6, Apple officially made the API available to developers. In 10.5 the methods were very different and were found in PrivateFrameworks. I have read some blogs which suggest that certain 10.5 headers were done away with in 10.6, however in my investigations they have simply moved QuickLookUI.framework to the Quartz package.
Apple’s documentation for how to simply by a QuickLook “consumer” I found quite woeful. A consumer, in QuickLook speak, simply uses the existing QuickLook generators and display panel, in exactly the same way as the Finder does. When I say “woeful” I mean “nonexistent” and so we must inspect the Class documentation for:
- QLPreviewPanel: the dark grey, rounded corner “window” that display QuickLook information
- QLPreviewItem: the object that we want QLPreviewPanel to display
- QLPreviewPanelController: a protocol that some object must implement to effectively “take charge” of the QLPreviewPanel
- QLPreviewPanelDataSource: much like an NSTableViewDataSource, a protocol that is responsible for feeding data to the QLPreviewPanel
- QLPreviewPanelDelegate: when the QLPreviewPanel is front and center, it may receive keyboard events and such that it doesn’t understand. The delegate will make decisions about what to do with those events. Also handles the cool “zoom” feature of the panel.
One thing I feel needs to be cleared up is in the QLPreviewItem documentation. Confusingly, the “optional” properties have a qualifier that reads “required property.” So, for example, the previewItemTitle property reads:
“The preview item’s title. This property is optional. (required)”
It does, truthfully, appear to be optional. However, it isn’t immediately clear which part of the documentation is wrong. Was this filed under “optional” by accident, or was it labelled “required” by accident? With previewItemDisplayState, the description sounds important enough to warrant implementation, but it doesn’t seem to be necessary for basic QuickLook needs.
The next issue that was troublesome was how to form a proper NSURL to return from the previewItemURL method, the one truly required method implemented by QLPreviewItem objects. The Cocoa docs don’t say anything except to pass a file NSURL, but some piece of documentation (which I of course can’t find again now) mentioned that the file URL needed to have proper escape characters and UTF8 encoding. This turned out not to be true, and simply using
return [NSURL fileURLWithPath:filepath];
in the FWFileRep class provided QuickLook with its needed information. Formerly, QuickLook wasn’t displaying preview images, but this very simple URL conversion from a very simple string did the trick.
For the rest of the classes, the more I thought of a QuickLook panel as a kind of NSTableView, the easier it was to understand the relationships between objects. What wasn’t obvious was the order in which the methods would be called. Apple typically does a good job of providing a listing of the order in which methods are called, but I could find nothing of that sort for QuickLook.
Apple does provide a useful sample program called “QuickLookDownloader” that implements QuickLook consumer methods. I found this example to be just a bit convoluted as an explanation of what happens, when and why. For example, it seems that as a data source, the Document object is passing DownloadItem objects to QuickLook. However, the .h file for DownloadItem (and in fact all of the .h files are the same) does not declare that it implements the QLPreviewItem protocol, nor does it implement the method – (NSURL *)previewItemURL as required.
It turns out that the Document class appends the necessary methods as a category to DownloadItem. Perhaps I am exposing my programming naivety, but I don’t see why these methods were singled out as not being part of the DownloadItem class proper.
The sample code does a better job of explaining the intent of certain methods, as evidenced in the QLPreviewPanelDelegate documentation. previewPanel:sourceFrameOnScreenForPreviewItem: is moderately self-explainatory, but without any other documentation to formalize its intent, it can be a little tricky to understand why this method may be useful. However, when the source code comments this method as, “This delegate method provides the rect on screen from which the panel will zoom.” it is suddenly crystal-clear what is happening.
So, here’s what I believe to be happening:
- The object of your choosing triggers [[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:nil] by the method of your choosing (a keyboard press, a menu item, whatever…) According to Apple docs, every application shares the singleton instance of QLPreviewPanel. So, in a sense, it doesn’t really matter who calls this method as any other object in our application has equal access to manipulate the panel.
- The QLPreviewPanel goes through the FirstResponder chain looking for someone who responds YES to (BOOL) acceptsPreviewPanelControl:
- That object then becomes the QLPreviewPanelController and is given the hook beginPreviewPanelControl: which allows the controller to set delegate and datasource for the panel.
- The object that is designated the datasource implements <QLPreviewPanelDatasource> methods numberOfPreviewItemsInPreviewPanel: (how many items the panel should display) and previewPanel:previewItemAtIndex: (what those items are). This is very similar to <NSTableViewDatasource> methods numberOfRowsInTableView: and tableView:objectValueForTableColumn:row:
- When the datasource returns a “previewItemAtIndex” object to the preview panel, that object must conform to <QLPreviewItem>. This means, for the sake of this writeup, implementing the one method previewItemURL: which returns an NSURL that is a file path to the item to be previewed
- Side note: it seems that the datasource can also return an NSURL directly, rather than an intermediary object which will in turn send its own NSURL. My own testing confirmed this behavior, but I see nothing in the documentation to suggest this is OK. I don’t think I would rely on this behavior, however.
- Implement previewItemTitle: if you want the preview panel title bar to display something other than a name derived from the file path URL. For example, you may want to append some other piece of information, or return an ALL CAPS title or such.
- The delegate seems to really only need to implement one method, previewPanel:handleEvent: Even in Apple’s source code this is used simply to forward key events to the underlying view, or rather the object responsible for having triggered the panel in the first place.
- Implement previewPanel:sourceFrameOnScreenForPreviewItem: if you want to implement the zoom effect. This is the area of the screen from which the panel will zoom in/out. For example, in a table view with icons, you may zoom out from a particular icon to give the illusion of that icon expanding into the preview panel. Without implementing this method, the panel will simply do a quick fade in/out.
- Implement previewPanel:transitionImageForPreviewItem: to give a start image that will fade into the preview panel. As in the example above, in a table view with icons you can provide the icon image itself as the transition image, completing the zoom and grow effect. One caveat I’m experiencing now is that because the preview item is passed into this method as type id the compiler cannot know at compile time if the preview item responds to particular methods (for example, to fetch an icon image).
One thought on “Making a Cocoa Application a QuickLook Consumer”
“Perhaps I am exposing my programming naivety, but I don’t see why these methods were singled out as not being part of the DownloadItem class proper”
DownloadItem is part of the model (following MVC conventions).
The category appending -previewItemURL is exposing the Model side to View side so it makes sense to separate the two of them. You can reuse the model in other contexts while the -previewItemURL part is specific to Quick Look.
“it seems that the datasource can also return an NSURL directly, rather than an intermediary object which will in turn send its own NSURL”
From documentation: “NSURL QLPreview Additions”: “This category implements the QLPreviewItem protocol which makes instances of NSURL suitable as items for the Quick Look preview panel”. It is confirmed by framework “QLPreviewItem.h”:
@interface NSURL (QLPreviewConvenienceAdditions)