Creating a simple NSPredicateEditorRowTemplate

NSPredicateEditor is a great class, provided you figure out how it works. There’s a fair amount of documentation as to what’s going on, but if you ever want to do something beyond what’s possible through configuration in Interface Builder, you’re probably going to spend several hours playing with things until you get it right.

Hopefully, I can help cut down that time.

What’s going on under the hood

NSPredicateEditor is a subclass of NSRuleEditor. However, they really don’t behave the same way at all, so knowing that doesn’t really give you much insight (that I’ve found).

NSPredicateEditor uses a template mechanism for displaying an NSPredicate. These templates are instances of NSPredicateEditorRowTemplate (go figure). Each row template (with one exception) displays a single NSComparisonPredicate.

The exception to this is the built-in row template for handling NSCompoundPredicates. We’re going to ignore this template in this post and focus on customizing templates for comparison predicates.

When the NSPredicateEditor gets a new predicate object (via -setObjectValue:), it has to figure out what row templates it needs. It’s a pretty simple process: The editor simply asks each one of its row templates whether it recognizes the NSPredicate. (-matchForPredicate:) If the template responds affirmatively, then the editor will -copy the template (this is why row templates conform to <NSCopying>) and invoke -setPredicate:.

-setPredicate: is simply going to extract the predicate’s leftExpression, predicateOperatorType, and rightExpression. These values will then be set into the UI.

The UI

One of the interesting things about row templates is that they do not inherit from NSView. In other words, the actual NSPredicateEditorRowTemplate object is never (indeed cannot be) displayed to the user. So what is it the user is seeing? It’s the row template’s -templateViews.

By default, an NSPredicateEditorRowTemplate provides 3 template views: an NSPopUpButton, a second NSPopUpButton, and either a third NSPopUpButton or some sort of user-interactable view, like an NSDatePicker or an NSTextField. The first popup lists all of the possible left-hand NSExpressions, the second holds the operators, and the third view holds the right-hand NSExpressions.

These views are received by the NSPredicateEditor and shown in the UI. Their styles are manipulated to make them have the “rounded rect” appearance, and their frames are changed to make them fit in the row. The main attribute that’s useful to configure is the frame width of the right-hand view. If the third view is an NSDatePicker, you may alter the width of the view to allow room for specifying a time in addition to the date. (By default, an NSDatePicker only allows date selection)

Customizing the UI is a simple matter of overriding the -templateViews method:

- (NSArray *) templateViews {
  NSMutableArray * templateViews = [[super templateViews] mutableCopy];

  //at this point, you have a mutable array of views.
  //you can add views, remove views, alter appropriate properties of existing views, etc.

  return [templateViews autorelease];
}

However, you need to be careful with this. -templateViews is called more than once, so if you’re instantiating a new view each time this is invoked, you may end up allocating too many views and not know which one is actually being shown on the UI. For this reason I prefer a “lazy allocation” method:

- (NSView *) myExtraView {
  if (myExtraView == nil) {
    myExtraView = [[MyExtraView alloc] init...];
    ...
  }
  return myExtraView;
}

- (NSArray *) templateViews {
  NSMutableArray * templateViews = [[super templateViews] mutableCopy];

  [templateViews addObject:[self myExtraView]];

  return [templateViews autorelease];
}

Now I know that the myExtraView object will only be allocated once per instance of my row template, and I can always go access the view directly to retrieve information about what the user has manipulated.

<caution>

If you’re localizing this NSPredicateEditor, then you must consider this:

  • When specifying the translated value of a row template, the number of positional specifiers in the translated value must be equal to the number of views returned from -templateViews. If not, the editor will fail to localize that row.

In other words, if you have a custom -templateViews method that adds a fourth view, then your translated string for this row must have %1$@, %2$@, %3$@, and %4$@ (with the translated values interspersed appropriately). %1$@ will be replaced with the first view in the array returned from -templateViews, %2$@ will be replaced with the second view, etc.

</caution>

On a final point regarding -templateViews, I’ve found that I rarely have an occasion to alter the first or second views returned by super. Your mileage may vary, but I’ve generally found that it’s best to leave them alone.

Building a custom row template

Now that we understand the basics of what’s going on, let’s build a simple NSPredicateEditorRowTemplate. To keep things simple, we’re going to build a row template that allows us to compare against an NSTimeInterval.

The Interface

@interface DDTimeIntervalRowTemplate : NSPredicateEditorRowTemplate {
    NSPopUpButton * unitPopUpButton;
}

- (id) initWithLeftExpressions:(NSArray *)leftExpressions;

@end

It’s pretty simple. We inherit from NSPredicateEditorRowTemplate, we provide a custom initializer (since we’ll be instantiating the template programmatically), and we have one instance variable for holding on to our extra NSPopUpButton.

The Implementation

@implementation DDTimeIntervalRowTemplate

- (id) initWithLeftExpressions:(NSArray *)leftExpressions {
    NSAttributeType rightType = NSDoubleAttributeType; //NSTimeInterval is a typedef'd double
    NSComparisonPredicateModifier modifier = NSDirectPredicateModifier; //don't need "ANY" or "ALL"
    NSArray * operators = [NSArray arrayWithObjects:
                           [NSNumber numberWithUnsignedInteger:NSLessThanPredicateOperatorType],
                           [NSNumber numberWithUnsignedInteger:NSLessThanOrEqualToPredicateOperatorType],
                           [NSNumber numberWithUnsignedInteger:NSGreaterThanPredicateOperatorType],
                           [NSNumber numberWithUnsignedInteger:NSGreaterThanOrEqualToPredicateOperatorType],
                           [NSNumber numberWithUnsignedInteger:NSEqualToPredicateOperatorType],
                           [NSNumber numberWithUnsignedInteger:NSNotEqualToPredicateOperatorType],
                           nil];
    NSUInteger options = 0;
    return [super initWithLeftExpressions:leftExpressions
             rightExpressionAttributeType:rightType
                                 modifier:modifier
                                operators:operators
                                  options:options];
}

Again, this should be fairly straight-forward. We’re creating a row template with the left expressions given to the initializer. These left expressions will be compared against a user-entered double value. We’ll be doing a direct comparison (simply put: <left expression> <comparison operator> <user-entered value>), and we’re supporting the standard numeric operators: <, <=, >, >=, =, and !=. The options bit is only useful for string comparison (it’s how you specific case insensitivity or diacritic insensitivity, etc). Once we’ve got everything specified, we’ll let super's initializer finish everything off for us.

Continuing…

static NSString * unitNames[] = {@"seconds", @"minutes", @"hours", @"days", @"weeks"};
static NSInteger unitIntervals[] = {1, 60, 3600, 86400, 604800};
#define numberOfUnits() (sizeof(unitNames)/sizeof(unitNames[0]))

- (NSPopUpButton *) unitPopUpButton {
    if (unitPopUpButton == nil) {
        unitPopUpButton = [[NSPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO];

        NSMenu * unitMenu = [unitPopUpButton menu];
        for (int i = 0; i < numberOfUnits(); ++i) {
            [unitMenu addItemWithTitle:unitNames[i] action:NULL keyEquivalent:@""];
        }
    }
    return unitPopUpButton;
}

- (void) dealloc {
    [unitPopUpButton release];
    [super dealloc];
}

This method will be our lazy initializer for our unit popup button (to ensure that we only allocate one button per instance of the row template). The button has 5 items in its menu, each representing a different unit of time measurement. We also have a static NSInteger array that reflects the number of seconds in each unit (ie, one day is 86,400 seconds).

One thing that’s interesting here is that we’re initializing our NSPopUpButton with NSZeroRect. We can do this because NSPredicateEditor will adjust the origin of the view to be in the appropriate spot relative to the other views and the size to fit the content. In other words, the frame will be taken care of for us.

And of course, since we’ve allocated an object, we must be sure to release it in our -dealloc method.

- (void) getBase:(NSTimeInterval *)base unit:(NSInteger *)unit fromTimeInterval:(NSTimeInterval)timeInterval {
    if (base == nil || unit == nil) {
        [NSException raise:NSInvalidArgumentException format:@"base and unit cannot be nil"];
    }

    for (NSInteger unitIndex = numberOfUnits() - 1; unitIndex >= 0; unitIndex--) {
        if (timeInterval >= unitIntervals[unitIndex]) {
            *base = timeInterval / unitIntervals[unitIndex];
            *unit = unitIndex;
            return;
        }
    }
}

- (NSTimeInterval) timeIntervalFromBase:(NSTimeInterval)base unit:(NSInteger)unitIndex {
    if (unitIndex >= numberOfUnits()) {
        [NSException raise:NSInvalidArgumentException format:@"unitIndex beyond max allowed bounds (%d)", numberOfUnits()];
        return 0;
    }

    return (base * unitIntervals[unitIndex]);
}

These 2 methods are simply convenience methods. The first method will convert an NSTimeInterval into a unit and a base. For example, if given 172800 as the interval, it will return a base of 2 and a unit of 3, to represent 2 days.

The second method is simply the inverse. Given a base and a unit, it will turn it into an absolute NSTimeInterval.

Now for the fun stuff!

- (NSArray *) templateViews {
    NSMutableArray * views = [[super templateViews] mutableCopy];

    [views addObject:[self unitPopUpButton]];

    return [views autorelease];
}

This is about as simple as it gets. When we’re asked for what views to display in the interface, we’re going to return the three views provided by super (two popup buttons and an NSTextField) and add a fourth button of our own (the popup button denoting a unit of time).

- (NSPredicate *) predicateWithSubpredicates:(NSArray *)subpredicates {
    NSPredicate * p = [super predicateWithSubpredicates:subpredicates];
    if ([p isKindOfClass:[NSComparisonPredicate class]]) {
        NSComparisonPredicate * comparison = (NSComparisonPredicate *)p;

        NSExpression * right = [comparison rightExpression];
        NSNumber * value = [right constantValue];

        NSInteger unit = [[self unitPopUpButton] indexOfSelectedItem];

        NSTimeInterval newInterval = [self timeIntervalFromBase:[value doubleValue] unit:unit];
        value = [NSNumber numberWithDouble:newInterval];
        right = [NSExpression expressionForConstantValue:value];

        p = [NSComparisonPredicate predicateWithLeftExpression:[comparison leftExpression] 
                                               rightExpression:right 
                                                      modifier:[comparison comparisonPredicateModifier] 
                                                          type:[comparison predicateOperatorType] 
                                                       options:[comparison options]];
    }
    return p;
}

This method is one of the two at the heart of our custom template. Via this method, we’re translating what’s on the UI into an NSPredicate. For the most part, we’re going to let super handle the implementation, with one minor alteration. By invoking super on this, we’re going to get back an NSPredicate of the form <left expression> <operator> <value of textfield>. We need to extract the rightExpression, who’s constantValue will be an NSNumber representing what was in the textfield. It is the doubleValue of this NSNumber that becomes the base, and the index of our unitPopUpButton is our unit. Once we have those two values, we can run them through our conversion method, then rebuild our comparison predicate. This new comparison predicate will be exactly the same as the predicate returned by super, with the single change of having the constantValue of the rightExpression modified.

Finally…

- (void) setPredicate:(NSPredicate *)newPredicate {
    if ([newPredicate isKindOfClass:[NSComparisonPredicate class]]) {

        NSComparisonPredicate * comparison = (NSComparisonPredicate *)newPredicate;

        NSExpression * right = [comparison rightExpression];
        NSNumber * value = [right constantValue];

        NSTimeInterval base = 0;
        NSInteger unit = 0;
        [self getBase:&base unit:&unit fromTimeInterval:[value doubleValue]];

        value = [NSNumber numberWithDouble:base];
        right = [NSExpression expressionForConstantValue:value];

        [[self unitPopUpButton] selectItemAtIndex:unit];

        newPredicate = [NSComparisonPredicate predicateWithLeftExpression:[comparison leftExpression] 
                                                          rightExpression:right 
                                                                 modifier:[comparison comparisonPredicateModifier] 
                                                                     type:[comparison predicateOperatorType] 
                                                                  options:[comparison options]];
    }

    [super setPredicate:newPredicate];
}

@end

This is the final method that we need to implement. This method will take an NSPredicate and extract various values from it to reflect in the UI. Like its inverse -predicateWithSubpredicates:, we’re going to let super handle most of the work. However, we need to do a bit of manipulation first. We’re going to extract the constantValue of the comparison predicate’s rightExpression. This NSNumber is the absolute time interval, in seconds. We’re going to take that time interval and run it through our conversion method to turn it into a base and a unit. The unit is used to select the appropriate item in the unitPopUpButton, and the base is put back into the NSPredicate. When we pass this new predicate up to super, the base will be placed inside the text field (the third view returned from -templateViews).

Using this template

Now that we have this template, we have to actually use it. Fortunately, it’s pretty simple:

NSPredicateEditor * editor = ...; //some NSPredicateEditor. perhaps an IBOutlet?
NSMutableArray * rowTemplates = [[editor rowTemplates] mutableCopy];

NSArray * leftExpressions = [NSArray arrayWithObject:[NSExpression expressionForKeyPath:@"duration"]];
DDTimeIntervalRowTemplate * intervalTemplate = [[DDTimeIntervalRowTemplate alloc] initWithLeftExpressions:leftExpressions];
[rowTemplates addObject:intervalTemplate];
[intervalTemplate release];

[editor setRowTemplates:rowTemplates];
[rowTemplates release];

This should be fairly easy to understand. We’re simply adding an instance of DDTimeIntervalRowTemplate to the list of templates available to our NSPredicateEditor. This row template recognizes one left expression, the keyPath @"duration". This means that all predicates generated by this row template will be of the form duration <operator> <time interval>. Pretty easy.

Localizing this template

To localize this row template, we’ll need the following somewhere in a comment:

NSLocalizedStringFromTable(@"%[duration]@ %[is, is not, is greater than, is less than, is greater than or equal to, is less than or equal to]@ %@ %[seconds, minutes, hours, days, weeks]@", @"Predicate", @"the text of the duration template")

This will generate a rather lengthy strings file (30 different key-value pairs), with each value having four positional specifiers (as was mentioned above), one for each of the views returned from -templateViews. For a more complete explanation of how localizing a predicate editor, see my earlier post on that very topic.

Wrap-Up

So there you have it! This may be a rather simple and contrived example, but a useful one. Hopefully this will help you as you wrap your brain around NSPredicateEditor and its supporting classes. There are still a couple gotchas, but we’ll learn about those in another post.

  1. lego-ninjago reblogged this from funwithobjc
  2. funwithobjc posted this
Short URL for this post: http://tmblr.co/Zt522y1Y7NRE
blog comments powered by Disqus