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!

0 Comments:

Post a Comment

<< Home