Search code examples
macoscocoanstokenfield

Mixing tokens and strings in NSTokenField


I want to have an NSTokenField that contains both plain text and tokens. That's the same problem as in this question, but the answers there haven't solved it for me. Maybe I'm missing something, or maybe Apple changed something in the 5 years since those answers were posted.

Specifically, let's say I want to type "hello%tok%" and have it turn into this:

image of text "hello" and token "TOKEN"

In order to try to remove chances for confusion, I always use a custom represented object, of one of the following classes, rather than a plain string...

@interface Token : NSObject
@end

@implementation Token
@end


@interface WrappedString : NSObject
@property (retain) NSString* text;
@end

@implementation WrappedString
@end

Here are my delegate methods:

- (NSString *)tokenField:(NSTokenField *)tokenField
    displayStringForRepresentedObject:(id)representedObject
{
    NSString * displayString = nil;
    if ([representedObject isKindOfClass: [WrappedString class]])
    {
        displayString = ((WrappedString*)representedObject).text;
    }
    else
    {
        displayString = @"TOKEN";
    }
    return displayString;
}

- (NSTokenStyle)tokenField:(NSTokenField *)tokenField
                styleForRepresentedObject:(id)representedObject
{
    NSTokenStyle theStyle = NSPlainTextTokenStyle;
    if ([representedObject isKindOfClass: [Token class]])
    {
        theStyle = NSRoundedTokenStyle;
    }

    return theStyle;
}

- (NSString *)tokenField:(NSTokenField *)tokenField
        editingStringForRepresentedObject:(id)representedObject
{
    NSString * editingString = representedObject;
    if ([representedObject isKindOfClass: [Token class]])
    {
        editingString = nil;
    }
    else
    {
        editingString = ((WrappedString*)representedObject).text;
    }
    return editingString;
}

- (id)tokenField:(NSTokenField *)tokenField
    representedObjectForEditingString:(NSString *)editingString
{
    id repOb = nil;
    if ([editingString isEqualToString:@"tok"])
    {
        repOb = [[[Token alloc] init] autorelease];
    }
    else
    {
        WrappedString* wrapped = [[[WrappedString alloc]
            init] autorelease];
        wrapped.text = editingString;
        repOb = wrapped;
    }
    return repOb;
}

As I'm typing the "hello", none of the delegate methods is called, which seems reasonable. When I type the first "%", there are 3 delegate calls:

  1. tokenField:representedObjectForEditingString: gets the string "hello" and turns it into a WrappedString representation.
  2. tokenField:styleForRepresentedObject: gets that WrappedString and returns NSPlainTextTokenStyle.
  3. tokenField:editingStringForRepresentedObject: gets the WrappedString and returns "hello".

The first two calls seem reasonable. I'm not sure about number 3, because the token should be editable but it's not being edited yet. I would have thought that tokenField:displayStringForRepresentedObject: would get called, but it doesn't.

When I type "tok", no delegate methods are called. When I type the second "%", tokenField:representedObjectForEditingString: receives the string "hellotok", where I would have expected to see just "tok". So I never get a chance to create the rounded token.

If I type the text in the other order, "%tok%hello", then I do get the expected result, a round token followed by plain "hello".

By the way, the Token Field Programming Guide says

Note that there can be only one token per token field that is configured for the plain-text token style.

which seems to imply that it's not possible to freely mix plain text and tokens.


Solution

  • I asked myself whether I had seen mixed text and tokens anywhere in standard apps, and I had. In the Language & Text panel of System Preferences, under the Formats tab, clicking one of the "Customize..." buttons brings up a dialog containing token fields. Here's part of one.

    enter image description here

    Here, you don't create tokens by typing a tokenizing character, you drag and drop prototype tokens.

    To make one of the prototype tokens, make another NSTokenField and set it to have no background or border and be selectable but not editable. When your window has loaded, you can initialize the prototype field using the objectValue property, e.g.,

    self.protoToken.objectValue = @[[[[Token alloc] init] autorelease]];
    

    You need to set up a delegate for each prototype token field as well as your editable token field. In order to be able to drag and drop tokens, your delegate must implement tokenField:writeRepresentedObjects:toPasteboard: and tokenField:readFromPasteboard:.