Making a Cocoa Application a QuickLook Consumer

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:

  1. 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.
  2. The QLPreviewPanel goes through the FirstResponder chain looking for someone who responds YES to (BOOL) acceptsPreviewPanelControl:
  3. That object then becomes the QLPreviewPanelController and is given the hook beginPreviewPanelControl: which allows the controller to set delegate and datasource for the panel.
  4. 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:
  5. 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
    1. 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.
    2. 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.
  6. 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.
    1. 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.
    2. 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).

file_wrangler_2 and the FWFileRepManager class

This week has been spent working on the FWFileRepManager class. It is, essentially, a controller class for all things FWFileRep-related (formally CDFileRep, as seen in a prior post), which includes handling the list of file paths one may be interested in, which is a unique and separate list to the list of files one may be interested in. In other words, the list of things you add to the editor window and the list of contents for those things are two unique beasts.

Consider dragging in a batch of files and folders from your desktop (yes, file_wrangler_2 allows for an arbitrary mix of files/folders from any location on your system). One may drag in more than intended, like an extra folder for example. In the FWFileView we just see a list of everything inside every folder of everything that was dragged. We could sort by file path, then select everything that is in the unintended folder, then delete those from the file list. Alternatively, we should be able to modify the original intention directly, not just clean up the result.

file_wrangler_2 now has a facility for deleting entire file paths from a list. So, drag in 25 folders, but you want to remove a few of them after-the-fact? No problem. An editor panel for working with the things you dragged in is available, with one line item per “thing added”. In the main window, you may see a list of 10,000 files, but in this window you will only see the list of 25 things. Select those things you didn’t intend to drag and delete. Anything matching that signature are filtered from the main file list with no reloading of the files necessary.

So far, I’m really happy with the overall speed of file_wrangler_2 and am working hard to make sure that it only does exactly the work asked of it. This requires a fairly constant, deep consideration of what a user’s intention is at any given moment. Building an internal core that is finely tuned to the specific tasks at hand, while remaining flexible for future growth and and expansion is proving challenging, to say the least.

“How much optimization is too much?” is a question with which I struggle daily.

Considerations of the file_wrangler_2 Base Class

CDFileRepresentation is the core, base class upon which file_wrangler_2 is built. For every file and folder a user of the program wants to potentially rename, one CDFileRepresentation stands in for that object. New file names are often derived from metadata of each individual file and folder of interest in a renaming session. For example, one may want the “modification date” or some sort of EXIF data inserted in the template-derived new file names. We don’t want to hit the filesystem repeatedly to request this information for 30,000 files over and over and over again. However, we need the information and CDFileRepresentation objects will cache this data for us.

The CDFileRepresentation class has to be able to instantiate itself quickly, providing the “most likely useful” information to the user immediately, but also needs to minimize its hits to the filesystem when the user requests certain types of data. Apple provides numerous ways of obtaining file metadata, including Cocoa’s NSFileManager’s attributesOfFileAtPath:error: , Core Foundation’s LSCopyItemInfoForRef(), and the Spotlight metadata via MDItemCreate().

I’ve been quite surprised at the amount of overlap in the data obtainable through these various methods, and also a touch disappointed that one or two items of interest (like, checking of an item is an Application or not) are excluded from some methods, but not others and so on. Basically, there doesn’t seem to be the “one true way” to obtain the information about a file that matches the user’s mental model of what is going on.

So, this means hitting the filesystem in at least two different ways to obtain all information that is of interest. An implementation pattern that Apple encourages is that of “lazy loading.” Basically it just means “don’t do the work until you’re asked for the information” and it has proven to be very successful in the design of the CDFileRepresentation class. Before implementing lazy loading, in initial testing under semi-idealized conditions, 1774 CDFileRepresentation objects took 2.7 seconds to instantiate.

After looking at the types of data a CDFileRepresentation needs to be able to model for the user, some ivars could be grouped by “most efficient method of extraction”. By breaking the code apart and extracting like-data upon request of any any arbitrary piece of data (creation date, for example), we can control how much time the user spends waiting for any given request. This has the nice effect of amortizing wait time over the length of any given file_wrangler_2 session. This also helps us to never load certain types of data the user may never request.

After these and other optimizations (for example, switching to the C struct LSItemInfoRecord to obtain certain booleans) I was able to reduce instantiation time up to 16x. It now takes 0.17 seconds to instantiate the 1774 objects and an additional (one-time only filesystem hit) of an additional 0.19 seconds to obtain the full metadata for those objects (additional requests are completed in about 0.09 seconds).

I will, of course, continue to research ways to reduce these times even more, but there will be a limit to what can be done and still provide the functionality everyone needs. This all begs the question, “How fast does it need to be?” and I would like to tackle that in a future post when I have more built around this class.

file_wrangler_2 notes and sketches

I feel I’ve been talking a lot of theory, but not showing anything for my work of late. Today is photo day. Perhaps you will glean a piece or two of information from these snapshots of my file_wrangler_2 design notes. They are (intentionally) fairly small and low-resolution as the intent is simply to reify the work I’m doing, not to show any specific design decisions. Picture #7 is probably the closest to the latest UI designs, and the rest are all observations of similar products and ruminations on file_wrangler’s role in a user’s workflow.

That is a “well” in the bottom 1/4 of the sketch into which “panels” may be placed, dragged, rearranged, twiddled, futzed, modified, and set. “Panels” refer to discrete blocks of interface that represent specific renaming functionalities. More on the specifics, later.

file_wrangler_2, further UI work

I believe I’ve narrowed in on the basic building blocks of the new file_wrangler_2. I’ve been designing and re-designing and re-re-designing, trying to get a handle on how to make the ideas in my mind’s eye fit with reality. Some ideas turned out to be overly ambitious, some were unwieldy, some were just plain BAD ideas.

Ultimately, what I came to realize was that I was defining the “vocabulary” for working with file names; and in the future this may be a similar vocabulary for work with other aspects of a group of files. There are a few things we need in an interface to make that happen:

  1. Define the Scope of the Action
    What is the target of the user’s actions? Yes, its a batch of files, but what SPECIFICALLY does she want to do with those files? Give them new file names? New folder names? Both? and thinking into the future… modify some aspect of metadata? Set the spotlight comments?… Dare I dream to FTP them? This starts to get into the question of, “What is the scope of file_wrangler?” but its liberating to dream of the potential and possibilities.
  2. Define the Target(s) of the Action
    As with the original Filewrangler, the file list is front and center and big, big, big. This is the reason someone is using the program; this is what is most important. There are other aspects of the interface that necessarily need some room on-screen, but we can still emphasize the file list’s importance by promoting it to the top of the window.
  3. Filter the Target(s)
    The same logic that allows certain renaming functions can also be used to target or exclude certain files in the file list. We COULD force the user to pre-filter before bringing files into the program, but that just seems antagonistic. The whole point of the program is to help you “wrangle” your files, which implies the files at that moment are unorganized. Some utility to refine the choice is absolutely necessary.
  4. Define the Data for the Scope
    In the case of file_wrangler_2 (at least, initially) the data is the file name. Tools for common renaming needs tend to be broken into a few concepts:

    • global and local name changes (make the whole name UPPERCASE vs. put the date in this exact spot)
    • custom or extracted data (insert a specific word vs. insert the modification date)
    • specific and relative changes (put a sequence before the name vs. insert the date before the 5th occurrence of the letter “V”, wherever it may occur)

    Further, we need to consider the basic actions performed on any data in any program in all of the course of history: CRUD (create, read, update, delete) which is really just CRD (update is a create and delete combo-action).

These four aspects of the program have essentially driven the design of the UI layout. The window is divided into four sections that correspond with the above mentioned concepts. As much as possible, I am using Apple’s Human Interface Guidelines to drive design decisions and reduce the learning curve. I believe its metaphors and physical behavior will be instantly familiar to most Macintosh users.

Open Apple’s Dictionary program on OS X and look at the Dictionary/Thesaurus scope buttons. This is my intention for defining the scope in file_wrangler_2.

Look at Filewrangler 1 to see how file lists are presented, but let us also look at the Finder to see how one will sort a file list. I envision a right-click on the column headers to add/remove additional data columns. Just as the Finder does, I also envision allowing direct editing of individual file names.

Setting the filters and setting the renaming choices is the big trick, but we can get a sense of how to handle such things by looking at Automator, but with a twist. I am working on “Build-a-Filter” and “Build-a-Name” wells into which filtering and renaming options are dropped, rearranged, and otherwise work a bit like Lego blocks. Each block represents a very focused, very discrete function of the program and the order of the blocks defines the order of those elements within the name. With each additional block, the interface becomes more dense, yet almost by definition its density increases only at the rate the user defines.

Eventually I came to realize that every user of every renaming program has different ideas about what is important and how best to perform renaming actions. I can only hope to satisfy a specific percentage with the first release, then expand my user base with dot releases. This realization was surprisingly liberating for the design process. I can NEVER make EVERYONE happy, but I will certainly do my best to build an excellent program with a consistent vision.