3

I'm trying to create the effect of an NSComboBox with completes == YES, no button, and numberOfVisibleItems == 0 (for an example, try filling in an Album or Artist in iTunes's Get Info window).

To accomplish this, I'm using an NSTextField control, which autocompletes on -controlTextDidChange: to call -[NSTextField complete:], which triggers the delegate method:

- (NSArray *)control:(NSControl *)control
            textView:(NSTextView *)textView
         completions:(NSArray *)words
 forPartialWordRange:(NSRange)charRange
 indexOfSelectedItem:(NSInteger *)index;

I've gotten this working correctly, the only problem being the side effect of a dropdown showing. I would like to suppress it, but I haven't seen a way to do this. I've scoured the documentation, Internet, and Stack Overflow, with no success.

I'd prefer a delegate method, but I'm open to subclassing, if that's the only way. I'm targeting Lion, in case it helps, so solutions don't need to be backward compatible.

Dov
  • 15,530
  • 13
  • 76
  • 177

1 Answers1

4

To solve this, I had to think outside the box a little. Instead of using the built-in autocomplete mechanism, I built my own. This wasn't as tough as I had originally assumed it would be. My -controlTextDidChange: looks like so:

- (void)controlTextDidChange:(NSNotification *)note {
    // Without using the isAutoCompleting flag, a loop would result, and the
    // behavior gets unpredictable
    if (!isAutoCompleting) {
        isAutoCompleting = YES;

        // Don't complete on a delete
        if (userDeleted) {
            userDeleted = NO;
        } else {
            NSTextField *control = [note object];
            NSString *fieldName = [self fieldNameForTag:[control tag]];
            NSTextView *textView = [[note userInfo] objectForKey:@"NSFieldEditor"];

            NSString *typedText = [[textView.string copy] autorelease];
            NSArray *completions = [self comboBoxValuesForField:fieldName
                                                      andPrefix:typedText];

            if (completions.count >= 1) {
                NSString *completion = [completions objectAtIndex:0];

                NSRange difference = NSMakeRange(
                                         typedText.length,
                                         completion.length - typedText.length);
                textView.string = completion;
                [textView setSelectedRange:difference
                                  affinity:NSSelectionAffinityUpstream
                            stillSelecting:NO];
            }
        }

        isAutoCompleting = NO;
    }
}

And then I implemented another delegate method I wasn't previously aware of (the missing piece of the puzzle, so to speak).

- (BOOL)control:(NSControl *)control
       textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector {
    // Detect if the user deleted text
    if (commandSelector == @selector(deleteBackward:)
        || commandSelector == @selector(deleteForward:)) {
        userDeleted = YES;
    }

    return NO;
}

Update: Simplified and corrected solution

It now doesn't track the last string the user entered, instead detecting when the user deleted. This solves the problem in a direct, rather than roundabout, manner.

Kyle
  • 17,317
  • 32
  • 140
  • 246
Dov
  • 15,530
  • 13
  • 76
  • 177
  • There is no relevant distinction between userDeleted and isAutoCompleting. You can simplify the code further by merging these two into one property. – lhunath Jan 26 '13 at 20:36
  • That's not true. When a user has pressed the delete key, this code does not provide any autocompletion. This is the same way iTunes works. `isAutoCompleting` prevents an infinite loop by disallowing reentry into the function while the control's text is changing. – Dov Jan 26 '13 at 23:57
  • What I'm saying is that both of them end up fulfilling the same goal: When isAutoCompleting is YES, you don't complete. When userDeleted is YES, you also don't complete. You might as well use a single variable "inhibitCompletion", and set it to YES when you set isAutoCompleting or userDeleted to YES, and have the same net effect with less code and checks. – lhunath Jan 28 '13 at 00:28
  • Ah, I understand what you're saying. But they do still get set to NO at different times. `userDeleted` gets turned off after one character has been entered, but `isAutoCompleting` gets turned off only after the text field's value has been updated. I'm not saying that you couldn't rewrite it to use only one variable, but what would the point be? There are two semantic reasons not to autocomplete, and the code above clearly illustrates that. – Dov Jan 28 '13 at 15:49