Sunday, April 19, 2009

Cocoa Formatters

Every time I play with Cocoa, I'm reminded of how much I enjoy the framework.  It's elegant, easy to use, and usually provides a small, elegant solution to whatever I'm trying to accomplish.

Now here's what I wanted: Something that would only allow numbers to be entered within a text box while retaining compatibility with 10.4 (to code/test on my PowerBook).

My first instinct was to jump to interface builder, and link up a NSNumberFormatter to the NSTextField, launch the app, and watch what happens.  Let's say I wasn't that impressed...  The NSTextField would hog input until the input was valid.  And I didn't want to update a text box like I used to do.  I only wanted valid input as keys were pressed, not something that the user (myself) would have to think twice about as they try to comprehend an error message.

So, trolling the documents I found a nice method "setPartialStringValidationEnabled:". This is nice.  Would do exactly what I wanted with minimal effort - wait, only available in 10.5...

So overloaded NSTextField in objective C, and tried to intercept the keyUp and keyDown messages from NSResponder.  Hoping that if I removed all non-numerical keys from the stream, my little NSTextField would - hopefuly - not get filled with data.  It didn't work.

However, NSTextField is just a front-end to NSTextFieldCell.  So simply replacing the NSTextFieldCell at runtime with my own super-classed machination should work.  Should.  It didn't respond to NSResponder.  A very interesting thing, but where does the input come from.  Cocoa should be easy - there ought to have an easier way to do this - one that wouldn't make the developers from Apple cringe (if ever they were to see it).

So - again another look at NSFormatter.  MacOS, since 10.0, supports a method called 'isPartialStringValid:'  for subclasses of NSFormatter.  After reading the docs a bit, I implemented a simple formatter where this method would only return NO.  And behold, it was impossible to type anything in the NSTextField!  Success!

So here's the complete implementation of my subclass of NSFormatter.  It simply only allows numbers of the form [+,-][0-9]*[.]+[0-9]*.  For rounding reasons, I've limited the number of digits to 12 and used doubles for storage:


@implementation X_NumberFormatter
- (BOOL)isPartialStringValid:(NSString *)partialString newEditingString:(NSString **)newString errorDescription:(NSString **)error
{
int x;
int l = [partialString length];
BOOL foundDecimal = NO;
int curPostDecimal = 0;
for (x=0; x
{
unichar curChar = [partialString characterAtIndex:x];
if (!(curChar >= '0' && curChar <= '9') && curChar != '.' &&
!(x ==0 && (curChar == '+' || curChar == '-')))
return NO;
if (curChar >= '0' && curChar <= '9')
{
curPostDecimal++;
if (curPostDecimal > 12)
return NO;
}
if (curChar == '.')
{
if (foundDecimal)
return NO;
foundDecimal = YES;
}
}
return YES;
}
- (BOOL)getObjectValue:(id *)anObject forString:(NSString *)string errorDescription:(NSString **)error
{
*anObject = [NSNumber numberWithDouble:[string doubleValue]];
return YES;
}
- (NSString *)stringForObjectValue:(id)anObject
{
if (![anObject isKindOfClass:[NSNumber class]])
return nil;
return [NSString stringWithFormat:@"%1.12g", [anObject doubleValue]];
}
@end