April 20, 2010

Cocoa to Cappuccino – Spatially Formatting Text Fields

Filed under: Uncategorized — frameworker @ 6:18 am


I’m using pdf images as the background of electronic forms. The purpose of doing this is to make the electronic form feel just like the familiar paper one. It’s user friendly. Also the electronic forms can perform calculations automatically and accurately.


One of the cases that inevitably has to be handled is when a string has to be entered, with its characters equally spaced in a sequence of contiguous boxes, like this:


Cocoa has this nifty NSString method, drawAtPoint, that lets you do this when the field is not active. (But when it is active you can enter the string – unformatted – in the text field, which exactly covers the boxes in the background, so it all feels quite natural.)

- (void)drawRect:(NSRect)rect
    if ((![self focus]) && ([self format] == kWideFormat))
        // drawWideString calculates the bounds for each character
        // and calls a routine to draw it using drawAtPoint.
        [self drawWideString: [self stringValue]];
        [super drawRect:rect];


Unfortunately, there is no -[CPString drawAtPoint] method in Cappuccino, so this approach can’t be used.

I must confess that this “deficiency” left me with some confusion about how to procceed to implement the WIDE string behavior in Cappuccino.

I wondered if there was a way to do it using Canvas or CSS, or if Cappuccino text support for this kind of thing might be “just around the corner.”

And the sledgehammer approach of creating a sequence of single character text fields to display the inactive text, to be swapped-out with a regular text field while editing, seemed inelegant.

But a recent conversation with @saikatc at #shdh37 convinced me that the “buffered” approach was, in fact, a reasonable way to do this. And as it is with so many things in life, once the approach was determined, it started happening.


The one CPTextField subclass I use throughout the forms project overrides becomeFirstResponder / resignFirstResponder to bracket the active field with controlTextDidBeginEditing / controlTextDidEndEditing calls, something that neither Cocoa nor Cappuccino does, but which is critical to knowing when to swap the static text array in and out with the regular text field. Having this mechanism obviates the need for a special CPTextField subclass for these WIDE fields!

The Begin/End Editing messages are passed to the Text Field’s delegate, a widget subclass, which sends setFocusedDisplayFormat/setUnfocusedDisplayFormat messages to the object being activated/inactivated.

It’s important to note that the approach used here takes advantage of fact that the text field covers the char-array. If this were not the case, it would be necessary to buffer the char-array’s stringValues, so they wouldn’t be displayed, while the text field was active. That would be confusing.

So to avail ourselves of this pattern, we create a WIDE Widget subclass which switches the display of the static array on and off.

Here’s the code for that class.

PDQWideWidget descends from the concrete PDQTextWidget class and adds an array for the equally spaced characters.

// PDQWideWidget.j

@import "PDQTextWidget.j"

@implementation PDQWideWidget : PDQTextWidget

	CPMutableArray chars @accessors; // The array of equally spaced characters

Init calls super, then creates the array.

- (id) init
	self = [super init];

	if (self)
		// Do any initialization here!
		chars = [];

    return self;

makeView is overridden to create the array of one-char text fields. The order in which the constituent fields are created is crucial. The real CPTextField is created last, so it will be on top and receive mouse events. And the widget is put into “unfocused” mode.

- (void)makeView:(CPView)itsSuperview
	[self buildChars:itsSuperview];

	[super makeView:itsSuperview];

	[self setUnfocusedDisplayFormat];

buildChars builds the array of one-char text fields using the maxLen parameter to determine how many to create that will cover the widgetRect.

- (void)buildChars:(CPView)itsSuperview
	var frameRect = [self widgetRect];

	var width   = frameRect.size.width;
	var height  = frameRect.size.height;
	var x	    = frameRect.origin.x;
	var y	    = frameRect.origin.y;

	var cellWidth = width/[self maxLen];

	for (var index = 0; index < [self maxLen]; index++)
		var left = x + cellWidth * index;
		var charFrame = CGRectMake(left, y, cellWidth, height); // x,y,w,h

		var newCharField = [[CPTextField alloc] initWithFrame:charFrame];

		[self initCharField: newCharField];
		[newCharField setStringValue: @""];
		[newCharField setDelegate:self];
		[itsSuperview addSubview:newCharField];
		[chars addObject: newCharField];

Each charField must be initialized to (among other things) not accept events nor become a responder.

- (void) initCharField:(CPTextField)aCharField
	[aCharField setBordered:NO];
	[aCharField setBezeled:NO];
	[aCharField setEditable:NO];
	[aCharField setEnabled:NO];
	[aCharField setSelectable:NO];
	[aCharField setAlignment: CPCenterTextAlignment];
	[aCharField setBackgroundColor:[CPColor clearColor]];
	[aCharField setDrawsBackground:YES];
	[aCharField setVerticalAlignment:CPCenterVerticalTextAlignment];
	[aCharField setFont:[PDQAbstractWidget getFont]];

When the text field is focused, compose and set its stringValue from the char-array. The TextField will be displayed over the char-array, masking it.

- (void)setFocusedDisplayFormat
	// Build the stringValue from the char-array

	var itsStringValue = @"";

	for (var index = 0; index < [chars count]; index++)
		var aCharField = [chars objectAtIndex: index];
		var aChar = [aCharField stringValue];
		itsStringValue += aChar;

	[[self attachedControl] setStringValue:itsStringValue];

When the text field loses focus, or is first created, unpack stringValue into the char-array, then clear stringValue. The equally spaced chars will be displayed, but the empty TextField covering it, will not.

- (void)setUnfocusedDisplayFormat
	// Set the chars from stringValue and then clear it.
	var itsStringValue = [[self attachedControl] stringValue];
	var count = [itsStringValue length];

	for (var index = 0; index < count; index++)
		var aCharField = [chars objectAtIndex: index];
		[aCharField setStringValue: [itsStringValue characterAtIndex:index]];

	[[self attachedControl] setStringValue:@""];



Blog at