Application Technologies • 1:01:31
Spotlight, the powerful, easy-to-use search technology in Mac OS X, supports a wide range of file formats and helps users quickly find anything on their computers. Learn how to create a Spotlight plugin and incorporate searching within your application using Spotlight's Query APIs to deliver an improved user experience.
Speakers: Jonah Petri, Toby Paterson, Christy Warren
Unlisted on Apple Developer site
Transcript
This transcript was generated using Whisper, it has known transcription errors. We are working on an improved version.
Good morning, everybody. Welcome to session 108, Taking Advantage of Spotlight. My name is Jonah Petri, and I'm an engineer on the Spotlight team. And today, we're going to be taking you through a few things. We've got a lot to cover. So here's what we're going to do. The first thing we're going to cover is Spotlight, a little bit of an update on what we have in Leopard and some review of our existing API. We're going to cover document lineage, which is a new feature that we are trying out in Leopard.
We're also going to cover Quick Look, which is a way to preview your custom document formats. And you're also going to cover Spotlight Help, which is a way for you to index features of your application. So let's get started. So with Spotlight in Leopard, we wanted to extend what we'd done in Tiger. And in order to do that, we decided that the first thing we wanted to do was to show some recent items.
And the first thing that you see when you bring up the up the window. We show you the five most recent items in every category. So those would be the five most recent documents or the five most recent images that you've looked at, for instance. We also wanted to cover a little bit more advanced searching.
There were many requests that people really wanted to get a little bit more precisely at the documents they wanted. And so we've put this advanced search interface-- let me blow that up a little so you can see it. What this allows you to do is specify arbitrary attributes to search on.
And it also lets you do more full Boolean searching in the UI. You can see here that the search that I've done is for items on my computer where either the keywords contain yellow or the keywords contain green. And I found some images where there's maybe a green snake and maybe something about Yellowstone Park. So we think this allows you to do that. And you can also do some more powerful searching and maybe get at the documents that you want more precisely.
We've also allowed you to do server searching in Spotlight. This means that anything that you're connected-- any Leopard servers that you're connected to over AFP, and that includes personal file sharing, you can search by selecting the Servers tab in the Where pop-up. And that Where pop-up appears when you press the Custom button in the Spotlight window.
Another thing that we've put into Spotlight is integrated previews. So let's say you're browsing around a couple images. You can see I used that advanced search UI again to search for things where the keywords contain leopard. And I've got a bunch of pictures of leopards, but I can't really tell if that's the one that I want.
So we allow you now to pop up sort of an integrated preview, and this uses the QuickLook architecture that you'll see later, to preview your documents in line without opening the application. So we've added a very often-requested feature for an advanced query syntax. This is just an example, but we let you do arbitrary, true and false, and Boolean expressions, and we also allow you to search specific attributes in the query syntax.
This syntax might be somewhat familiar to those of you who've used Google or other search engines. It's sort of an evolved language there. So this example I'm showing how to search for, anything that matches tiger, where the kind is not an image. And so I've come up with a movie, and maybe some email messages, and some PDF documents.
We also allow you to do arbitrary Boolean searches. So this is an example with or. You can also use and, and you can also use parentheses. So this is I'm finding anything that's a PDF or that has the keyword leopard. Sort of a strange search maybe, but that's just a good example of an or. So we've taken the UI definitely places, and we've got further to go with that before we ship leopard.
But what we need from you most of all is we need to allow the user to search your document formats more richly. And we need you to allow them to preview them more richly. And in order to do that, we have to understand your document format. And for that, you need to tell us a little bit about that. And the way that you tell us about your document format is by writing a spotlight plugin.
And the way that these things work is these are CFPlugins that sit on your hard drive, and they declare to us, spotlight, a set of handled file formats in the form of UTI types. And when they're called, they extract metadata from a given file and allow us to add that metadata to the spotlight index.
And so we're going to go over how to build one of these. But some first, some general rules. If you're going to be building a spotlight plugin, you should publish metadata that facilitates user searches. Some published metadata that the user might search for. Sort of obvious. Also, allow for richer previews of the documents using the metadata.
So make sure you publish authors or keywords or these sorts of things that the user might see in a preview of their document to understand that that really was the document that they were looking for or maybe to compare two documents. Please don't use your spotlight importer as a database for your private data. That just slows down everybody else's searches, and we're not really guaranteeing that you're going to get everything back, so it's just not a good idea.
Also, please don't use your spotlight importer as a database for your private data. That just slows down everybody else's searches, and we're not really guaranteeing that you're going to get everything back, so it's just not a good idea. Also, please don't use it for icon previews or other blobs of data that it can't be searched. Also, remember that too much noise can confuse the user, so be judicious in what data you publish. Also, there's no user interface allowed in these things. We disallow connections to the Windows Server, and so you won't be able to pop up any authentication dialogs.
or any other kind of UI, for that matter. So now that we've gone over some rules, let's take a look at what these things are actually composed of. Metadata in the Spotlight system is in terms of attributes and values. There are many predefined attributes that you can take advantage of, and we urge you to take advantage of these because it allows for richer previewing between documents.
Some of the predefined attributes you see there, we cover title, authors, keywords, projects. There's also document type-specific attributes such as pixel height or width or maybe audio bit rate. And you can look at the full list of attributes. There are many, many, many of them in mditem.h, and that's in the metadata framework under the core services framework.
We also allow you to store localized attributes. These are attributes that appear differently to different users depending on their current language settings. For instance, an application's name might be localized in multiple, in different localizations, and you might want to store that in the display name. And so you can see here that you can use a CFDictionary with key value pairs for each language code.
And in this example, I'm using address book, and you can see that we've localized it in German and English and Italian. And you can also see that the empty string is used as a default localization to fall back on in case the user is using a language that you have not localized for.
So, we also ship many pre-built importers on the system. So if your application deals in terms of one of these document types, you may not have any work to do. We ship, you know, you can see this is not an exhaustive, list, I don't think, but it covers a lot of different data types. So you might want to look to see if you're already covered. Maybe you don't have any work.
But I suspect that many of you that have a custom format have not, we've not written an importer that covers it yet. So here's the steps that you need to do to write an importer. The first step you need is to start with the Spotlight plugin template in Xcode, then edit the Info.plist that comes with the plugin, implement the getMetadataForFile function, and then install your importer. And please test it a little bit before you release it on the world.
So the Spotlight plugin template in Xcode is pretty easy to find. You can see it right up there. It's in the new project dialog. And this lays down a lot of pre-built glue code for you that you basically don't need to touch. Stuff that's sometimes easy to get wrong, and we've just done the work for you. So please take advantage of that.
Once you've done that, edit the Info.plist. The key that you need to pay attention to the most is the UTI type that your importer handles, and that's declared in the LSItemContentTypes key. This is a pretty well-commented XML file, so you should see where everything goes. You should list your UTI or UTIs that your document importer is going to cover there.
It's also possible to define your UTI here, but we really urge you to define your UTI in your applications P-list if possible, because UTIs are fairly mature at this point, and that's really the proper place to be putting them. Once you've done that, edit the other informational fields like bundle name and version, and be careful not to just use your search and replace for everything com.apple to com.yourcompany, because there are some fields that need to be com.apple. Look in the P list. Don't use the big hammer. Just tread carefully.
Then once you've done that, implement the code. There's this stubbed-out function that comes with the template, getMetadataForFile. You're passed a path to the file that we want you to extract the metadata from, as well as a CFMutable dictionary to add the attributes to. Please be careful to extract the metadata efficiently. Be aware that your metadata plugin is being run in the background while the user may be using their machine.
And so you don't want to have an impact on the user experience. So you have to be careful in terms of CPU usage, disk usage, memory footprint, and all other sorts of performance-related considerations. Once you've done that, please install your importer. You can install it in two places. We allow you to install it in Library Spotlight, which would be where you would put it if you were doing a standalone importer. Or maybe if you wanted to put it there as part of... a larger install package.
We also allow you to put it within your application. This is useful for drag installs, or it's actually a little bit more friendly overall. So we like to recommend that you do that if you can. So the precedence that importers are loaded in, and first one wins, by the way, is Library Spotlight, then the system-installed importers in System Library Spotlight, and then your app-bundled importers. Once you've done that, you should be able to run your importer.
And test to make sure that new documents that are created are actually imported properly. So a couple tips. Please use the standardized attribute names. This really does allow for users to more... Richly compare documents between each other and also search more precisely. Take advantage of the text content.
This is really useful for contents of text documents, as you can see from the label, but it also might be used for things that you might not have thought of, like maybe the comments field in a calendar event, in a calendaring program. Or maybe if you have large collections of song lyrics. All of those can go in that field.
Another tip is to, if you can, delete the schema.xml and schema.strings if you don't need them. You'll see in those files the types of things that they cover. It has to do with advanced attributes, I'm sorry, custom attributes, as well as defining custom document types. But if you haven't edited those, chances are you can just delete them. And please check your performance with mdimport-p. mdimport is a tool that we ship that allows you to test your importers. You can type mdimport and then a file name and it will import that file.
It's a command line tool, by the way. And the -p option allows you to give it a folder hierarchy of files and it will import that entire folder hierarchy and then report performance statistics for you. We really want you to be careful about performance because these things really do impact user perceived performance on the system. So again, a couple things to be aware of.
Please be aware of how much memory and CPU you're using. And don't allocate huge buffers just to read large, large, large amounts of data only to pick out a small section to use for the metadata. Please don't blindly reuse your code. We do encourage you to reuse your code, but be aware if it's going to inflate hundreds and hundreds and hundreds of objects into the system only to extract a small amount of metadata that may have a large impact.
And the reason really why we want you to be careful about your performance is that if we're importing - if we're doing an initial scan of the user's drive, you may have thousands of documents of your document type on the user's drive and you have to scale your importer to those thousands and thousands of documents without hurting the user too much. So just please be aware of performance in this case.
So, if you have a custom file format, please write an importer. This will really help us integrate your documents into Spotlight and it will help the users find your documents and they'll thank you for that for sure. Otherwise, please put useful metadata into your documents. This allows us to present richer previews to the user and if you're going to use our APIs to present previews, it allows you to present richer previews to the user.
Please preserve metadata in existing file formats. And what I mean by this is, let's say, by way of example, you're writing OpenOffice and you're opening up Microsoft Word documents. If the user has bothered to enter an author or a title or comments into the document properties window, when you're reading in that file and writing it out again, take care to preserve those so that we don't lose those.
Because that's really user data and please don't lose user data, I guess, is the message. And please come visit us in the lab. Many people last year came to our importer session and then came over to the lab and had a draft of their importer written before they left. And so we really think that we can help you and we're happy to help you with this. So, next.
We're going to do a quick overview of the Spotlight API before we move on to document lineage. So, what do we allow you to do with the Spotlight API? Well, first we allow you to access metadata for a file. This is useful if you want to throw up a properties dialog or maybe a get info panel. And the way that you do this is using the MD item ref object.
And you can look up the MD item ref API reference in the documentation. It's pretty thorough. An MD item ref in this case represents a file in the metadata database. And you can ask it about its title or its subjects or its keyword or project, whatever metadata it has on it.
So in order to retrieve metadata from a file, there are three simple steps. The first, you have to create an MD item ref for the file. Then you have to obtain a list of available attributes and then retrieve the metadata. I'm going to walk you through this pretty quickly.
So MD item create is the function you use to create an MD item. Surprise. And you use a CFString to represent the path to the item. Once you have your item ref there, you call MD item copy attribute names to get the list of names of attributes that that MD item ref has and then call MD item copy attributes to get out a CFDictionary of attribute names to values.
And that's going to look something like this. Now, of course, some of you may be saying, well, that's all well and good, but that's not very pretty. I can't show that to the user. They'll say, KMDItemContent, what the heck is that? So we also allow you to get pretty localized display names for the attributes. And the way that you do that, what I mean by that is instead of KMDItemAuthors, you can get authors.
And in order to get that, you use mdschema.copyDisplayName for attribute. We also allow you to get localized descriptions of the attributes. And you please use mdschema.copyDisplayDescription for attribute. And that will return something like localized name of the file for KMDItemDisplayName in case the user wants to get a little bit more explanation about what this attribute really means.
So that's how you retrieve metadata. Now on to what we really built Spotlight for, which was searching. There's a couple ways to implement search. The first is really simple. It's a one-liner in code. It allows you to open the Spotlight window and present it as if the user had typed that search string that you provide into the Spotlight window. This is very fast to implement.
The second is to use the AppKit-level NSMetadata Query API. This is a Cocoa API that's bindable and works very nicely if you are using that level of API. We also allow you to use a core foundation-level MD Query API. That's the lowest-level API and the one that I'm going to be going into the most depth for here, but please come to us with any questions that you have.
So opening the Spotlight window. Like I said, this is a one-liner, H-I search window show with just a string to search. And this will open the Spotlight window as if the user had just typed it in themselves. This is great if you want really simple integration. You can see this in TextEdit and AddressBook.
And this is good to just present a quick search to the user if you think that's a convenient thing for that point in your application. The disadvantage to this is that you have no possible interaction with the Spotlight window after this. It's sort of fire and forget. The search is up and you can't figure out what the user chose or what they might have done with the result set.
So if you want to do any of that, you have to use one of the other two APIs. The first one I'm going to talk about is the NSMetadataQuery API. This uses NSPredicate, which some of you may be familiar with, which is an expression evaluating true or false across the entire set of objects on your system.
And this finds the files matching the provided predicate and also does things like stores the results and groups the results by their attributes and also is a bindable model object, as I mentioned. So you can just hook this right up in Interface Builder and very quickly have a working Spotlight searching window.
So a couple more details. You can also iterate this manually if you don't want to use bindings. The results are available by using these result count and result at index. And you could imagine a for loop that iterates the result set pretty quickly. And this NS Metadata Query also has a value list. And what I mean by this is that this allows you to say, hey, my 15 documents that were a result of my search only have three authors. So what are those three authors? Or I have 250 MP3s that match my search, but they cover three genres.
And so you could list the genres in sort of an iTunes-esque view. So this allows you to quickly evaluate certain statistics about your result set. And you can sort also. So also very useful. And you can group them by their attributes values. So you can ask for, given a genre, show me the list of MP3s within this search that match that genre.
So onto the MD Query API. So the key object to be aware of here is the MD Item Ref, which we covered before. In this case, it also represents a result from the query. And an MD Query Ref, which represents a query string, as well as the results that resulted from that query.
So the query language that we use for the MD Query API is a simple C-like expression language. You see the attributes are on the left and the expressions are on the right. In this case, I'm searching for anything whose content type is public.rtf. This will be pretty familiar to any of you that use C, which I assume is all of you. We allow you to use strings and numbers and dates. We support many operators.
For strings, we have equals and not equals, and star can be used as a wildcard in your patterns. And for numbers and dates, we support all the sorts of arithmetic operators that you might expect. We also allow you to do logical groupings and/or parentheses, also logical not. And another little detail is that star can be used to mean any metadata if it's on the left side of the expression. Here's an example where star matches Boston. And what this will find is any items whose metadata matches Boston.
So once we've covered that, there's a couple things you can do with string match modifiers. This is specific to string matching. You can't use this for number matching or anything. And we use this because exact match is often not what users expect. There are three supported modifiers that you can use. The first is C, and this means that the comparison is done case-insensitively. This means that text edit, all lowercase, would match text edit, uppercase.
D also similarly means diacritic insensitive. So resume without any diacritic marks will match resume with the accents on both Es. The third one is a little more subtle. It's W, and this means word-based matching. This means that we compare the pattern that you provided against each word in the value list of the item that we're evaluating.
So in this case, utility would match disk utility because there's a word that looks like utility. But parity is important. So if you're using a word that looks like utility, you're going to have to match it with the value of the item that you're evaluating. So in this case, utility would match disk utility because there's a word that looks like utility. But parity is important.
So if you're using a word that looks like utility, you're going to have to match disk utility with the value of the item that you're evaluating. but Paris would not match comparison. Even though Paris is in there in the middle of the word, it's not a whole word itself.
And the W modifier is implied for KMDAtemTextContent in Tiger as well as in the developer preview that you have today, but it might not be so in the future, so be aware of that when you're structuring your queries. So here's a couple example queries. The first one's pretty easy. This is just find all documents where the number of pages is greater than or equal to 100. You can also search in arrays.
We have array-valued attributes in Spotlight. So in this case, KMDAtemAuthors is an array-valued type. You can have many authors for a document. So this will find any item where one or more of the authors is Jonah Petri. So the way that we decide if something matches or not in an array situation is if any of the items in the array match, then we consider the whole attribute to have matched. And the bottom one is the typical Spotlight menu query.
If you typed... WWDC into the Spotlight menu, this is the query it would produce. So let me walk you through this pretty quickly. The star on the left, remember, means search all metadata for WWDC star, so any words that start with WWDC, and CDW, so case-insensitive, diacritic-insensitive, and word-based. And also the same thing for text content.
Text content is not implied by star. That's a little subtle, but you can see that in any of the queries that we have. So this is basically what the Spotlight menu does. So this is basically going to find any document where WWDC is the start of any word anywhere, which is sort of why Spotlight is cool.
So here's a couple more queries. Just quickly, I'm not going to spend much more time on this, but this is maybe to whet your appetite for what you can do in your app. So here's a query that will find all images with a DPI of greater than or equal to 300 and have an alpha channel. Maybe you've got a 3D app and you're looking for textures. The second one will find all the sound effects, matching thunder from one to five seconds in duration. Good for an audio editing app, right? All right, so hopefully that gave you some incentive to use this API.
So on to how to actually use it. The first thing you need to do is to create an MD Query ref, then register a callback for your updates, and then execute the query. Creating an MD Query ref is pretty easy. MD Query create with the query string as the second parameter.
Then register a callback for these specific notifications. There are three notifications that Spotlight sends to the Notification Center as it evaluates the query. The first one is the KMD Query Progress Notification, and this is sent while results are being gathered by the server. The second one is the KMD Query Did Finish Notification, and this is sent once when the query is completed.
And the third one is the KMD Query Did Update Notification, and this lets you know that some aspect of the query has updated after it's finished. This is only really sent when you've asked that the query update you after it's finished with its initial results gathering. One thing to be aware of is that new in Leopard, because we're allowing these servers and other high-latency sources to be searched, it's possible to get updates on your query before some stores in your query have finished.
So this shouldn't matter to too many people, but just be aware of that. And again, that update notification is only sent when you've asked for it. And the way you ask for it is while you execute the query, calling mdquery execute. You can see there, we had our query ref from before.
And the first parameter is the query you want to execute, and the second parameter has these flags. And if you pass the KMD Query Synchronous flag, that means the call is going to block until all the query results are done. Or otherwise, it would return immediately and only do the notification callbacks for you.
The other flag you can pass is the KMD Query Wants Updates. And that's the one I mentioned before that means that your query is going to be live, meaning that if the user creates a new document that matches your query, you will get notified that a new document has been added. Or if the user alters a document such that it doesn't match your query anymore, that document will fall out of the result set and you'll get one of these update notifications.
So the way that you iterate the result set is pretty easy. You call mdQueryGetResultsCount, and that will return a count for how many results there are. Then you call mdQueryGetResultAtIndex inside a for loop to get each item in the query, and then you process the items probably using the MD item API as we discussed before.
There's one thing to be thinking about when you're doing this is that if you're doing this on a thread separate from the one that the query is running on, you have to be careful about thread safety. And the way that we allow you to do this is that we have these... So what I mean by that, actually, is that the results that can change out from under you, and this is not a thread-safe object. We're not going to try to, I don't know, have this count update all the time.
It's just, you know, you have to be careful. And so the way that we allow you to be careful is we have this mdQueryDisableUpdates and mdQueryEnableUpdates calls that you can make. And if you bracket your loop with these calls, you can be sure that the result set won't be changing while you're iterating it. So that's how you prevent threading errors. So a couple things that you can do with mdquery that are sort of advanced. The first thing you can do is you can use search scopes. And this scopes your search to a specific subdirectory or set of subdirectories. That's mdquery/searchscope.
You can also do sorting. All this is covered in the API, by the way. Please come and see us in the lab if I'm going too fast. You can also use sorting, and that's the fourth parameter MDQuery creates, you can see there, is a CFRARef of attribute names. And it's also possible if simple ascending search is not what you want, to provide your own custom sort comparator using MDQuery set sort comparator.
The other thing that you can do with the sorted attributes is that you can use them to prefetch attributes. This is very good for performance if you know that you're going to be getting at these attributes to use them every single time you go through your array. What I mean by that is that if you were going to be using MD item copy attributes to copy the attributes out of your item as you're processing each one, each call that you do with that involves a message down to the Spotlight server and all the way back up to you. That can be very performance intensive.
If you use your sort attributes as prefetched attributes, you can basically avoid that round trip and it's very, very fast to get at these attributes. The way you get at the prefetched attributes is using, when you're in your for loop, use MD query get attribute value of result at index. It's kind of a mouthful, but that'll return you, in this case, the KMDI item content type if you had added that to your sort attributes. You can see there. value is now, say, public.data.
So some quick performance tips, how to make Spotlight the fastest you can in your app. The data store is optimized for the typical Spotlight menu case that we went over before. Case and diacritic insensitive search is fast. Word prefix search is fast, and that means you have a star on the end of the word, not at the beginning. And the word-based search is fast. So that query that we went over for WWDC for the Spotlight menu case before is a very fast query, and if you structure your queries after this query, I think you'll find that you get very good performance out of Spotlight.
If you're not finding good performance out of Spotlight, maybe you're doing some of these things. The first thing to be aware of is that file system attributes are not indexed by Spotlight. Now, those are the ones that start kmditemfs. Maybe kmditemfs name is a good example. These are not indexed, which means that we have to fall back to a traditional file system walk to find these things for you. So if you were searching for kmditemfs name, maybe you should be searching for kmditem display name instead.
The other thing to be aware of is that path scoping can be very slow, because we have to resolve the path for every item before we can check to see if it's in that subdirectory hierarchy that you provided. And you may think that you're doing us a favor by scoping to a small subdirectory, but really check your performance with the whole drive, because that actually may be faster.
The exception to this rule is a little counterintuitive. The scoping to... a query to the root of a volume is actually very fast. So just something to be aware of. And if you're having any performance problems with Spotlight, or if you just want some advice, please come to the lab, because we're happy to help you.
So now I'm going to quickly cover document lineage and what this is. This is a new feature that we're trying out for Leopard. And this is a way for you to find multiple copies of a document within your system. And what I mean by way of example is, let's say I'm writing a novel.
And so I've got this novel that I'm typing up in TextEdit. And I'm writing and I'm writing and I'm writing. And I decide, hey, this is pretty good. Let me call this my first draft. And so I do a save as in the TextEdit interface. And I save another copy of my document. And I call it finished first draft. Well, now I have two copies of this document. Maybe they're different. Maybe I continue to work on my first copy. And then maybe I decide I want to send my finished first draft to my editor for her to look at.
And so I email it to her. And it comes back. And now I've got a third copy with her comments. And so you can see this sort of workflow develops a family of documents that aren't exact copies of each other. But they're definitely related. And even further than that, maybe I've integrated those changes from the commented first draft into my original document.
And so you can see there's this complicated relationship in this document family. And we really think it's going to be great if users could find these documents as a family, as a group. And we want to enable this in Leopard. But what we need to enable it is a little bit of help from you.
And what I mean by that is that We need some help from you in the Save As case or when you are moving documents around in a way that we can't understand implicitly from what you're doing on the file system. And so in order to get this cooperation, we built it into NSDocument. So if you're using NSDocument for your application, you are basically all set.
If you want to get this functionality in the Leopard preview, you have to turn on this default. Defaults write dash G. NSDocument should write lineages, yes. And come see us in the lab if you want to work with this. In other cases, we need you to use the MDLineage APIs in order to mark this file as needing lineage tracking.
And the way that you do that, for instance, in the initial save case, is you need to create a lineage for the document using MDLineageCreate, and that will create a new lineage, a new unique lineage for this document. And then you set it on the file that you just wrote out. And please do this only for user documents. We've tried opting everything in the system into the lineage system, but it ends up being a pretty big burden, so we don't really want to do that.
So if you're saving a user document, do something like this to mark the file as belonging to this new lineage. And then that lineage will track the file as it moves around in the system. The other time that we need your help is if you're doing a save as.
We need to mark this new file that's been created somewhere on the file system as being related to the original file in order for them to be found as a group later. And the way that you do that is that you use MDLineageCreate branch from file. To create a new lineage that is marked as a branch from the original file. And then you set that new lineage on the document. And this will allow us later to mark these in the UI somehow as being related to each other.
And allow users to find them. So, as I said before, NSDocument has full-lineage support built into it, so if you're using that, you don't need to worry about this. If you're not using NSDocument and you're doing safe-save style writing out of documents where you write out a temp file and then you move it over the file that was originally there so that you have atomic saves, please use this FSReplaceObject API. And the reason that you might want to use this is that not only will it preserve the lineage on the document, but it will also preserve other things on the document, such as other extended attributes or access control lists that may be set.
And I know a lot of you have written your own code because we haven't provided a function that did just this before, but this is really good that we're providing this now in Leopard. This works for document bundles as well as documents as well as plain files. And it basically works as atomically similar to what we're doing in the API.
And so you don't really have to figure any of this stuff out anymore. If you want more information on that, come to the lab and we can direct you to the right person. But if you can't use either of these, then you've got to manually maintain the lineage.
And the way that you do that is just another little lineage API. If you're writing out your temp file, before you move it over the original file, call MDLineageCreateFromFile. This will copy the lineage off of the existing file, the old file that hasn't been deleted yet. And then once you've moved the file over, call MDLineageSetOnFile, which will replace the lineage on the new file with the lineage of the original file. And that will maintain the lineage and allow these documents to be found as a family later on.
So I think the bottom line for that is if you can, please use FSReplaceObject or NSDocument because it does all of this for you for free. But we want to provide these APIs so that you can do it in your app if you've got your own custom code that's doing saving. So with that, I'm going to turn this over to Toby for a look at Quick Look.
Thanks, Jonah. Hi, I'm Toby Paterson. I manage the Quick Look team, and unsurprisingly, I'm going to tell you a little bit about Quick Look today. So we're going to cover the basic architecture of Quick Look. We're going to talk about a little bit of terminology. We're going to find out what a Quick Look plugin does and how you can write one.
First, what is Quick Look? Quick Look is a technology for providing document previews that we're building into Leopard. You saw an introduction to the Quick Look preview panel if you were at the keynote session yesterday, the State of the Union. We're going to talk a little bit about what goes behind that, how we start with the document, feed it into the Quick Look system, and pop it out the end in the preview panel. The goal of Quick Look is to help you find your documents. We're going to talk about how to build documents more quickly. And to that end, we're building the panel into Spotlight, into Time Machine, and into Finder. There's other places where Quick Look can help out.
If you're using File Sync to synchronize your iDisk on .Mac with your local computer, then occasionally you may notice that you get conflicts if you modify the same file on two different computers. Quick Look can help provide more information in the conflict resolver panel than we give you today in Tiger.
So we kind of bandied the term previews about a little bit generically, but we actually have two very specific definitions that we talk about. A thumbnail is a static image that gives you a hint as to the contents of the document. It's not too rich, but it provides a bit more information than just the document icon.
Thumbnails are used inside a Spotlight. In the results list, we display the little icon next to all of the items. And if you click on the info button, we expand inline to give you a larger icon and present a little bit more information, a little bit more metadata about the document.
Finder, of course, uses thumbnails inside in the icon view. A preview is designed to give the user a more faithful representation of the document. It's a richer format, PDF, QuickTime movies, etc. We want the user to be able to navigate through their document. If it's a keynote presentation, we want them to be able to navigate through the slides to find exactly the one that they're looking for. But we draw the line at editing. If you want to edit it, you open it up. Previews are displayed in the context of the QuickLook preview panel.
[Transcript missing]
So, the alternative, if you don't want to pay the cost of those trade-offs, or if your document is not a bundle, is that you can write a Quick Look plugin to create thumbnails and previews on demand. A Quick Look plugin is a CFPlugin. It implements a simple interface. There's two functions: one to create a thumbnail, one to create a preview. And you use API in the Quick Look framework to provide those thumbnails and previews to the client.
Your plugin is loaded into the Quick Look Daemon. The Quick Look Daemon serves to provide a bit of a sandbox for you. No offense, but we don't particularly want to load all of your code and all of your libraries into Finder, into Spotlight, into Time Machine, into all of these places.
And you probably don't want to be running in there yourself. It's a little bit hostile if everybody's all packing in there, competing for resources, so on and so forth. So we've created this Quick Look Daemon that will fire up to load your plugin and run you in a safe, known environment. When somebody comes along and asks for a thumbnail or for a preview, we'll find your plugin, we'll load your plugin, and we'll ask it to give us a thumbnail or a preview.
So, how do you write a Quick Look plugin? There's four basic steps. You start with the Xcode template, you edit the Info.plist, you implement the generate thumbnail, generate preview functions, and you install your plugin. If you've written a Spotlight plugin before, this should be pretty familiar for you. The actual mechanism is identical.
So let's start with the Xcode template. We provide a basic boilerplate for you so that all you need to do is just go and fill in some of the blanks. The boilerplate has an Info.plist, it's got an implementation of Ion Known, all of the factory methods, so on and so forth.
There's two blanks that you have to fill in. The first of these is the Info.plist to associate your plugin with your document type. So you just specify one or more UTIs in the LS item content types there in your Info.plist. The next blanks that you have to fill in are the two simple functions to create a thumbnail and to create a preview on demand. We give you the URL of the document that we're asking for a preview or thumbnail for, content type.
We give you the URL of the document that we're asking for a preview or thumbnail for, content type. of the document and a few other options in the case of a thumbnail. How big do we want the thumbnail to be? You then turn around when we call you and use the Quick Look framework to provide us with your thumbnails or with your previews.
In the case of the thumbnail, if you've got a CG image ref somewhere, if you're using Core Image or something like that to produce it, you can just give us the image directly. Alternatively, we can give you a CG context into which you can do all of your drawing. When you're done your drawing, you call flush context on up, and that's your cue to us to tell us that the thumbnail's done. We can take that, create an image, and pass it off to the client.
Similarly, for previews, there's two functions to create a CG context into which you can draw. The first of these creates an image, and that's an image context. That's appropriate if your document's just sort of a single-page type of thing. Alternatively, if you're something like Keynote or OmniGraffle where you can create multiple pages, you use the PDF.
You can create a PDF context and use the usual begin page, end page, and create a multi-page document. You get your context, you do your drawing into it, just like normal, and when you're done, you flush the context. That tells us to take the preview, package it up, and deliver it off to the preview panel.
Installing your plugin? Also pretty straightforward. We use launch services so you can stick it in a well-known location, like Library Quick Look if you don't have an application. If you have an application, we encourage you just to stick it in the application bundle, as Jonah suggested for the Spotlight plugins. Now that being said, that's actually not working right now.
So in the seed build that you have today, you need to explicitly tell us where to look for that preview using the default that you can see on the screen there. Don't worry too much about the details. In the sample code associated with this session, we've got a sample of a Quick Look plugin, which documents in pretty good detail what you need to do to get yourself up and running with this.
I'm going to talk a little bit about some advanced stuff now. Canceling requests, concurrency, and a nod towards performance. Canceling requests is pretty straightforward. Let's say the user's opened up the Finder window, Finder's come off and asked us for 10,000 thumbnails, and then the user closes the window.
We've got our own fairly advanced request management, so we're not going to fire off all 10,000 requests at once, but we do try and sort of manage multiple ones a handful at a time just to get greater parallelism. If your thumbnail generation or if your preview generation takes a bit of time, there's ways that you can either poll us to find out whether it's actually worthwhile continuing to do the... request.
Alternatively, you can receive an asynchronous notification by implementing two optional methods in the QuickLook plugin interface, which we'll call out of band in another thread to let you know, hey, pack up, go home, nobody's interested anymore. Out of the box, we're going to assume you're not reentrant. It's just the safe thing to do.
We would very much like you to be reentrant, and so if you are, just add this key into your Info.plist, and that tells us that we can run multiple instances. If you're plugin in the same QuickLook daemon, that's going to help the throughput. It's going to reduce the system overhead, and everybody's going to be happier in the end because of that.
Conversely, we assume the frameworks that you use are reentrant. Now, in the event that you're using a framework that other generators might likely be using, which are not reentrant, you need to tell us about that by adding this needs-to-be-run-in-the-main-thread key into your Info.plist. And that's our cue to know that we should only run you in your own quick look daemon, and we won't load you up with anyone else who has a similar flag set.
On the performance front, thumbnails need to be really, really, really fast. If it takes a long time to generate thumbnails, we're going to make finder wait. Users are going to get aggravated. Everybody's unhappy. So you need to make a trade-off there between the quality of the thumbnail that you generate and the time it takes to generate it.
Previews, on the other hand, they just need to be fast. We're not going to fire off hundreds of these simultaneously. It's usually done in response to a user action. The Quick Look preview panel itself supports asynchronous loading. So we can do that in the background, but you still, you don't want to make the user wait too long staring at the spinning progress button there. And so we think concurrency is a good thing. We encourage you to support parallel requests by being reentrant.
Asynchronous cancellation so that we can stop things when we don't need them anymore. So just a quick recap here then. The things you should remember. There's two kinds of Quick Look previews, thumbnails and previews in varying degrees of richness. The native Quick Look preview types that we support are images, text, PDF, HTML, rich text, QuickTime movies, QuickTime audio.
You can pre-generate your previews and your thumbnails into your Quick Look previews. You can pre-generate your document bundle. Or you can write a Quick Look plugin to generate them on demand. So with that, I'm going to hand over to Christy Warren, who's going to tell you about Spotlight for help. Thank you.
Thank you, Toby. I'm here to talk about Spotlight for Help. How many of you attended the State of the Union yesterday? About half of you. I'm Christy Warren. I'm the lead engineer on this feature. And what is Spotlight for Help? Well, it's a shortcut to the UI. It lets you access and invoke your user interface via text search. How many of you have launched like Photoshop or Pages or Word and couldn't find that menu right away and had to go hunting through your menu bar? Or dug through preferences.
I know I can set the font inside a BB Edit, but for the life of me, I can't find where to find that default font. So there's all this great UI I want to use, but I can't find it. So we made this tool that lets you-- Find it! Now, this works for menus out of the box for both Carbon and Cocoa. Let me give you a quick demo before we go on.
So here's this novel that Jonah started. It was a dark and stormy night. And I'm going to continue with, he looked at me, but then I go, you know, maybe I have a spelling problem here. So I'm going to go to, I could go hunting through the menus going, hmm, where's the spelling commands? Or I could click here and bring up our new search the UI field.
And here if I type in spelling, it'll populate the list below with various commands for spelling. And if I arrow down, it'll indicate where it is in the UI. And as I arrow through, it'll select different items. So in this case, I want to do spelling. And I hit enter. And it'll bring up the spelling dialing. So it's not only a way to find your UI, it lets you access and execute it very quickly. So those of you who are keyboard jockeys could have a lot of fun with this. So let's go back to the slides.
Spotlight, for help, goes beyond just searching menus. It can actually search your entire UI, your preferences panels, windows, palettes, anything in your application, with some restrictions, which we're going to deal with later. Developer adoption is required to search other UI, however. So at a minimum, you need to run this analysis tool and generate a custom index that's built and stored with your application. This builds a persistent model of your UI, and it can be used to uncover these deeply buried features in your application.
So to create an index, you run your application under Xcode, and there's a new item in the debug menu called Run Executable and Index UI for Help. For this seed version, it doesn't do anything yet, but for GM, it'll actually do the application probing. I'm going to give you a preview of that right now. So please switch to the demo machine.
So here's a little application I wrote that gives some examples of hidden UI. I don't recommend you design your app like this, with a lot of ugly controls and stuff. But stuff like this happens. Here's a tab view that mirrors a preference panel in some applications. There's a top-level tab view that you can select. And there's even a nested tab view.
So how am I going to find my way around? So I'm going to quit that. And in Xcode, I'm going to use our-- where is it? Under Debug, Run Executable and Index UI for Help. And when I select that, it's going to launch our application. And look, I'm not doing anything.
What it does is it enumerates all the visible UI, which starts with the menu bar. And for every menu item, it fires off that menu item and sees if a window appears or something else interesting that it can explore. In this case, it found this tab view window, and it's going through and trying all the things to see if they bring up other dialogs. So it can do a complete search of your application. So it's running through. It'll finish in a second.
And now that it's done, we'll quit the app. And you'll see down here, it added a file to your project. This is the index file. And this will get automatically built with your application and stored in the resource bundle. So it's fairly easy to do. And now when we run the application again... Now if I type "help" and I type, let's say the word "common," you'll see various entries came up.
And if I arrow through them now, you saw it brought up the window without doing anything and pointed me to one of the controls, "common." Now it went to another one, another one. Now it brought up the other window, and it'll even go within the tab view and reveal these things.
So, I think that's pretty cool. Alright, let's go back to slides. So what do you need to do in order to create an index for your app? Well, you need to be friendly to the indexer. You need to make your application accessible. And this has benefits beyond just supporting Spotlight for help. It helps users that may have difficulty using computers in various ways. And if you have a Carbon app, there's two things we want you to do, which is to modernize event handling and to move to HIView.
So support help indexing, there's kind of two issues you need to deal with. One is dangerous UI. Here I have my household application, and it gives me three options. Erase my hard disk, launch rockets, or open the doggy door. Well, clearly, when I'm running this app analyzer, I don't want to do the first two.
So we need a way to disable that kind of UI temporarily while the analysis is going on. Another related thing is, most apps launch, they put up a default document, and that's kind of what we expect. If you put up a gallery or license screens or something like that, we need a way to temporarily not do that and put up a default document window instead.
So how can we do this? Well, one simplistic way is we can create a help index compile flag and use this flag to conditionally compile out undesired code. However, this is clumsy to test and maintain. So we decided to help you and give you some Apple events in Core Event Class Apple Event.
And these are sent, when the app launches under this analyzer, it'll tell it to set up for app analysis and you can respond to this, turn off all the undesirable UI. And then at the end it'll tell you that it's done with this so you can restore your app to its normal state.
And how many of you used Apple Events before? Just a few of you? Well, the good news is this is all the code you need in order to do this if you're a Cocoa app. You basically get a shared Apple Event handler and you wire up selectors and targets to handle these two events. So you could literally copy this code as boilerplate and paste your code into the handle setup and handle finish. And you're done. You can do all your work from here.
Now for accessibility, one of the trickiest parts is many UI don't have strings associated with them, such as a text field. In these cases, when you search for "author," you would like it to allow you to go right to that text field rather than have it highlight the word "author," because that's not very helpful.
So we can figure this out for simple cases such as a text field immediately to the right of the string. However, to work well, you should implement these two accessibility attributes: the title_UI_element attribute and the serves_as_title_for_UI_element attribute. And this will be good for users who need accessibility as well.
Now, to improve indexing, out of the box, if I search for "spell," it'll find only the words containing "spelling." However, to give us more stuff to search, if you implement these attributes, like the description attribute, then we can search anything within that description. And that's very helpful. So for details on how to get accessibility, please visit this link.
So for Carbon, if you want this app analyzer to work with your app, you're going to need to modernize your event handling a little bit. The problem is if you just do getNextEvent, handle the event yourself, and process it, We don't get a chance to kind of see what's going on and make the deductions we need to do in order to build our index. So you should either do run application event loop or install event handlers on menu item selection. And you can see this at this site.
Finally, If you move your app to HIView, we will get higher fidelity indexing of the UI. For example, those tab views and stuff that we saw for Cocoa, that only works for Cocoa. But, you know, easily, but if you implement HIB, you know, in the future as we move on, we'll be able to do that for your Carbon apps as well. So please, you know, consider doing this.
So to wrap up, Spotlight for Help is a new way to access and learn the UI. It works your Carbon and Cocoa out of the box, and hopefully this is a very modest effort that you have to do to allow you to search other UI within your application. So please run the tool and generate a custom index when it's available, and implement some basic availability and play well with the indexer if you can.
So to wrap up, we have a call for action. If you have a custom document format, please write a Spotlight plugin and a Quick Look plugin so you can work with those cool new UIs. If you are not an NS document-based application, you know, you have a custom document type, please adopt the document and lineage APIs and at minimum use FS Replace object so that we can track all the metadata and other information as we go along.