Search code examples
objective-cmacoscocoaactionnstokenfield

NSTokenField not firing action


I have an NSTokenField to add tags to an object (a document). I would like to update the object with new tags the moment a token is added to the token field (when a tokenising character is typed). Unfortunately this does not seem to work. The NSTokenField is connected to an action in my controller but this action method is never called.

I also have a NSTextField connected in the same way to the controller and its action method in the controller is called.

I've also tried this with key value observing:

- (void) awakeFromNib {
    [tokenField addObserver:self forKeyPath:@"objectValue" options:NSKeyValueObservingOptionNew context:NULL];
}


- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if([object isEqual:tokenField]){
        NSLog(@"Tokens changed");
    }
}

but this action is only called when I programatically change the tokens.

How can I be notified when the tokens in the tokenField are changed?


Solution

  • The NSTokenField action selector isn't called the moment a new tag is created. Depending on the setting you've gone with in Interface Builder, it's called either when you hit enter to end editing (Send On Enter Only) , or when you end editing some other way (Send On End Editing). To get the fine control you're after you'll need another approach.


    The blue tags that appear when a tokenising character is added to the token field are called text attachments (instances of NSTextAttachment). One way of working out when tags are being added/removed from your token field is to track changes to the number of these objects contained in the token field's underlying attributed string.

    To get access to the relevant attributed string you need to get hold of the fieldEditor's layoutManager - the object which ultimately supplies the string that appears in the text-view. Once you've got it, each time you get a controlTextDidChange: message, count up the number of text attachments in the string representation of its attributedString. If the number this time around is greater than the number recorded in the previous count, a tag has just been added.

    #import "AppDelegate.h"
    
    @interface AppDelegate ()
    
    @property (weak) IBOutlet NSWindow *window;
    @property (weak) NSLayoutManager *lm;
    @property (nonatomic) NSUInteger tokenCount;
    
    @end
    
    @implementation AppDelegate
    
    // The text in the fieldEditor has changed. If the number of attachments in the
    // layoutManager's attributedString has changed, either a new tag has been added,
    // or an existing tag has been deleted.
    -(void)controlTextDidChange:(NSNotification *)obj {
        NSUInteger updatedCount = [self countAttachmentsInAttributedString:self.lm.attributedString];
        if (updatedCount > self.tokenCount) {
            NSLog(@"ADDED");
            self.tokenCount = updatedCount;
        } else if (updatedCount < self.tokenCount) {
            NSLog(@"REMOVED");
            self.tokenCount = updatedCount;
        }
    }
    
    // About to start editing - get access to the fieldEditor's layoutManager
    -(BOOL)control:(NSControl *)control textShouldBeginEditing:(NSText *)fieldEditor {
        self.lm = [(NSTextView *)fieldEditor layoutManager];
        return YES;
    }
    
    // Iterate through the characters in an attributed string looking for occurrences of
    // the NSAttachmentCharacter.
    - (NSInteger)countAttachmentsInAttributedString:(NSAttributedString *)attributedString {
        NSString *string = [attributedString string];
        NSUInteger maxIndex = string.length - 1;
        NSUInteger counter = 0;
    
        for (int i = 0; i < maxIndex + 1; i++) {
            if ([string characterAtIndex:i] == NSAttachmentCharacter) {
                counter++;
            }
        }
        return counter;
    }
    
    @end