Confused Development

I develop software and I often get confused in the process. I usually find the answers after a while, but a month later I can't remember them. So from now on, I will write them down here.

Thursday, October 19, 2006

More Fun with Autocomplete

Today I did some more stuff with autocomplete in NSTextView. What I wanted to achieve is have a list of suggestions for the autocomplete, but have the text that will actually be inserted in the view differ from the suggestions. Have a look at the picture below to see what I mean. You can see that each entry in the suggestions list starts out with "iCal Event: ...". The one selected at the moment is "iCal Event: Meeting with Rory O'Connor" - however, the text that gets inserted in the view is just "Meeting with Rory O'Connor".

advanced autocomplete in NSTextView

Now, why is that exciting? Because the way autocomplete works is that NSTextView's completionsForPartialWordRange:indexOfSelectedItem: (or the corresponding delegate method) just returns an array of strings, and those strings will both be shown in the suggestion dropdown box and be string that will actually inserted. So, the first task is to have the insertion be different from the suggestion. This can be achieved by overriding NSTextView's insertCompletion:forPartialWordRange:movement:isFinal:, which is responsible for the actual insertion. You could do something like this:

- (void)insertCompletion: (NSString *)word 
     forPartialWordRange: (NSRange)charRange 
                movement: (int)movement 
                 isFinal: (BOOL)flag
{
    [super insertCompletion: @"not the same"
        forPartialWordRange: charRange 
                   movement: movement 
                    isFinal: flag];
}

This would always insert "not the same", regardless of what the user had chosen from the list. Amazing, but not very useful yet. To make it a little more useful, I guess there are a number of options. One could have a lookup function (e.g. via an NSDictionary) to get from word to whatever we want. Or a simple transformation, to get rid of the leading "iCal Event: ".

However, I chose to go a different route - extending NSString so that I could ask word for the string that should be inserted, along the lines of [word completionString]. Categories will not do that for me, because they don't allow me to define additional instance variables (i.e. the completionString), so I had to subclass NSString. There is a little complication here, because NSString is the abstract superclass of a class cluster, so subclassing isn't as straight-forward as in other cases. I need to override the primitive methods length and characterAtIndex:, and I need to store the actual string. The interface for my NSString subclass looks like this:

@interface MultiString : NSString {
 NSString *completionString;
 NSString *string;
}

- (id)initWithString: (NSString*)string1
 andCompletionString: (NSString*)string2;

- (NSString *)string;
- (NSString *)completionString;

// primitive methods I need to implement:
- (unsigned int)length;
- (unichar)characterAtIndex: (unsigned)index;

@end

The implementation looks like this:

@implementation MultiString

- (id)initWithString: (NSString*)string1
 andCompletionString: (NSString*)string2
{
 self = [super init];
 if (self) {
  string = string1;
  completionString = string2;
 } 
 return self;
}

- (NSString *)string
{
 return string;
}

- (NSString *)completionString
{
 return completionString;
}

- (unsigned int)length
{
 return [string length];
}

- (unichar)characterAtIndex: (unsigned)index
{
 return [string characterAtIndex: index];
}

@end

Having this, I can use MultiString to generate strings with two representations, add them to the autocompletion array, and let insertCompletion:forPartialWordRange:movement:isFinal: return the completionString instead of the "normal" string. This is what it looks like:

- (void)insertCompletion: (NSString *)word 
  forPartialWordRange: (NSRange)charRange 
    movement: (int)movement 
     isFinal: (BOOL)flag
{
 if ([word isKindOfClass: [MultiString class]]) {
  [super insertCompletion: [word completionString]
   forPartialWordRange: charRange 
        movement: movement 
      isFinal: flag];
 } else {
  [super insertCompletion: word
   forPartialWordRange: charRange 
        movement: movement 
      isFinal: flag];
 }
}

You might wonder why I test for the class of word. The reason is that, if the user aborts the autocomplete process (by hitting escape again), none of the elements of the completion arrays will be inserted, but instead the original word from the text view. Now, this word is not of class MultiString, so calling completionString on it would result in an error, and insertCompletion:forPartialWordRange:movement:isFinal: would not successfully return anything.

That's all!

Waiting for a Spotlight Query to Finish (Spotlight and autocomplete)

After a long time of doing other things, I finally got around to working on semiBlog again. In that context, I was playing with integrating Spotlight (through NSMetadataQuery). Now, NSMetadataQueries are asynchronous, i.e. you create your query, you register a call-back method for when then query gives some results, and then you run the query. In other words, after the query starts running, the code doesn't halt and wait for the query to finish - instead, it just continues. Then, whenever the query returns some results, the call-back method is called. Looks like this:

- (void)someMethod
{
    NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
    NSPredicate *pred = [NSPredicate predicateWithFormat: queryString]; 
    [query setPredicate: pred];
    
    [[NSNotificationCenter defaultCenter]
             addObserver: self
                selector: @selector(queryHandler:)
                    name: NSMetadataQueryDidFinishGatheringNotification
                  object: query];
    
    [query startQuery];

    // whatever comes now happens immediately:
    NSLog(@"something happens");
}

- (void)queryHandler: (NSNotification *) inNotification
{
     NSLog(@"The query is finished, do something.");
     // blabla...
}

So far, so good. Now, what I want to do in semiBlog is integrate such a Spotlight query in the autocomplete of an NSTextView. I can manipulate the autocomplete suggestions of NSTextView by overriding completionsForPartialWordRange:indexOfSelectedItem: (or by calling textView:completions:forPartialWordRange:indexOfSelectedItem: in the delegate). Whenever autocomplete is initiated, these methods are called. The NSArray they return determines what will show up in the list of completion suggestions. Looks like this:

- (NSArray *)completionsForPartialWordRange: (NSRange)charRange 
indexOfSelectedItem: (int *)index
{
   return [NSArray arrayWithObjects: @"eins", @"zwei", @"drei", nil];
}
Basic autocomplete Of course, when I want to use Spotlight here (e.g. I type "Knud", initiate autocomplete, Spotlight finds all contacts and events that somehow match the string "Knud", I use the result set for the completion suggestions), I run into problems. I can initiate the query in this method, but I will not get the result here, so that I can construct the return array from it. Instead, the result is handled in the asynchronous call-back. So, what to do? After a bit of searching, I found that run loops are what is needed here (see the documentation for CFRunLoops here). In short, from a current run loop I have to start a new run loop right after I initiate the query (calling CFRunLoopRun()). The current will then wait until the new loop finishes (calling CFRunLoopStop(CFRunLoopGetCurrent ())), which I will let it do at the end of the call-back, after the query results have been processed. So, now the code looks a little like this:

- (NSArray *)completionsForPartialWordRange: (NSRange)charRange 
indexOfSelectedItem: (int *)index
{
 // get the current word. That will be the keyword in our query
 NSString *word = [[self string] substringWithRange: charRange];
 
 // construct the query string from this word:
 NSString *queryString = @"someQueryString";
 
 // construct the objects needed to perform the query
 NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
 NSPredicate *pred = [NSPredicate predicateWithFormat: queryString];
 [query setPredicate: pred];
 
        // register the call-back
 [[NSNotificationCenter defaultCenter]
             addObserver: self
    selector: @selector(queryHandler:)
     name: NSMetadataQueryDidFinishGatheringNotification
      object: query];
 
 [query startQuery];
 
        //start a new run loop
 CFRunLoopRun();
 
        // after the new loop is finished, continue by returning the new suggestions:
 return [self suggestions];
}

- (void)queryHandler: (NSNotification *) inNotification
{
 NSMetadataQuery *query = [inNotification object];

 NSArray *suggestions = // create the suggestions array from the query result
 
 [self setSuggestions: suggestions];
 
        // stop the new run loop
 CFRunLoopStop(CFRunLoopGetCurrent ());
}
Autocomplete based on a Spotlight query

Now, I'm not sure I have grasped this run loop business completely - maybe there are some pitfalls here - , but the code seems to work! :-)

Wednesday, October 18, 2006

Page Source for AJAX Web Pages

Most web browsers have a view page source function that is very handy for debugging web sites or learning from existing ones. However, when using AJAXy stuff, I found out that elements of a web page changed via replace_html (or maybe some other function) actually do not appear in the source. I don't really know why that is the case, but here is a way to get around it:

  • Open the page in Firefox.
  • Firefox has a function called view DOM (or something like that, I'm using the German localization).
  • This will show you the DOM of the page in a nice expandable tree view, including the recently ajaxed nodes.
  • You can select the node you are interested in and choose copy XML.
  • Et voila! There you have the code of the selected node!

Thursday, October 12, 2006

Problems with Spotlight

I finally got around to playing with Apple's Spotlight, which I want to use in semiBlog. Somehow, I find Apple's documentation on the topic a bit lacking. Sure there are lots of documents, and they help, but they also confuse.

E.g. the Query Expression Syntax document tells you that you can write the following query:

kMDItemAuthors == "Steve"wc && kMDItemContentType == "audio"wc

Now, when you try to create an NSPredicate to run in an NSMetadataQuery object with that expression, you will get an error. Why? Because, as I later found out, if you want to do Spotlight queries the ObjC way (using the classes I just mentioned), you have to use a similar, but slightly different query syntax. :-( How stupid is that? I'm sure there are reasons for that (This document explains it all...), but it was a somewhat frustrating process to find out. The same query in an NSPredicate has to look like this:

(kMDItemAuthors LIKE[wc] 'Steve') && (kMDItemContentType LIKE[wc] 'audio')

Another problem I had is that I found it hard to figure out what the kMDItemContentType of various kinds of data like AddressBook entries or iCal events are (I want to ask things like "give me all contacts" or "give me all events". The System-Declared Uniform Type Identifiers document lists a vast number of those types. But - I had two problems here:

  • Contacts: A type public.contact is mentioned as the "Base type for all contacts". That is true, but if you query for kMDItemContentType == 'public.contact' you will probably get nothing. This is because AB contacts actually have the type com.apple.addressbook.person (not mentioned in the document...), which is a more specific type (a little like a subclass). And, asking for the type will only give you the most specific one - it doesn't give you the super-types by inference. Instead, you have to query for the kMDItemContentTypeTree - this attribute contains a collection of all types, from the most specific to the most general. So, either kMDItemContentTypeTree == 'public.contact' or kMDItemContentType == 'com.apple.addressbook.person' will get you what you want (if you want all contacts).
  • Events: The document also mentiones a type public.calendar-event as the "Base functional type for all scheduled events". Annoyingly, this type doesn't seem to be used at all, not even as a super type for com.apple.ical.bookmark (again not mentioned in the document). So, if you are looking for all events, query for kMDItemContentType == 'com.apple.ical.bookmark'.