Monthly Archives: June 2010

Changing the default behavior of built-in Cocoa controls

Apple has many task specific controls built into Cocoa. They are all well designed and have most of the functionality a user and a developer expect. One of this controls is the NSSearchField. This control has a special design which allows the user to recognize the provided functionality with ease. It is so well-known that Apple uses the design even on there website. It has support for menus (e.g. for recent search items), auto completion, a cancel button, and so one. Although this is mostly feature complete, there are sometimes cases where you like to extend it. In this post, I will show how to add another visual hint to this control when a search term isn’t found. The aim is to change the background of the underlying text edit to become light red to visual mark the failed search.

Understanding how Cocoa works

The NSSearchField class inherits from an NSControl. NSControls are responsible for the interaction with the user. This implies displaying the content in a NSView, reacting to user input like mouse or keyboard events and sending actions to other objects in the case the status of the control has changed. Usually a control delegate the first two tasks to a NSCell. The main reasons for this are to persist on good performance even if there are many cells of the same type (like in the case of a table) and to be able to exchange the behavior of the control easily (like in the case of a combobox which also allow typing in a text field). With this information in mind we know that we need to overwrite the drawing routine of the cell (NSSearchFieldCell) to achieve our goal. The implementation is straight forward and shown in the following:

@interface MySearchFieldCell: NSSearchFieldCell
{
  NSColor *m_pBGColor;
}
- (void)setBackgroundColor:(NSColor*)pBGColor;
@end

@implementation MySearchFieldCell
-(id)init
{
  if (self = [super init])
    m_pBGColor = Nil;
  return self;
}
- (void)setBackgroundColor:(NSColor*)pBGColor
{
  if (m_pBGColor != pBGColor)
  {
    [m_pBGColor release];
    m_pBGColor = [pBGColor retain];
  }
}
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
  if (m_pBGColor != Nil)
  {
    [m_pBGColor setFill];
    NSRect frame = cellFrame;
    double radius = MIN(NSWidth(frame), NSHeight(frame)) / 2.0;
    [[NSBezierPath bezierPathWithRoundedRect:frame
          xRadius:radius yRadius:radius] fill];
  }
  [super drawInteriorWithFrame:cellFrame inView:controlView];
}
@end

The user can set a custom background color by using setBackgroundColor. Also it is possible to reset the background color by passing Nil to this method. The method drawInteriorWithFrame draws a rounded rectangle on the background and forwards the call to the super class afterward.

Replacing the cell class of a control

The next step is tell the control to use our own cell class and not the default one. Although there is a setCell method defined in NSControl it is not as easy as one might think. Creating an instance of MySearchFieldCell and passing it to setCell after the NSSearchField is created will not have the expected effect. The reason for this comes from the fact that the cell is initialized when the control is created. This includes setting all properties and targets for the actions. If one replaces the cell afterward these setting will be get lost. Later on, I will show a method how to keep this configuration, but for now we will start with an easier approach.

When the control creates a cell object it asks a static method for the class name to use. This method is called cellClass. If we overriding this method with our own one, we are able to return MySearchFieldCell. The following code demonstrates this:

@interface MySearchField: NSSearchField
{}
@end

@implementation MySearchField
+ (Class)cellClass
{
  return [MySearchFieldCell class];
}
@end

Now, if you use MySearchField instead of NSSearchField when creating search fields, you are done. Unfortunately this isn’t always possible. First you may not be able to inherit from NSSearchField for whatever reason and second this will not work when you are use the Interface Builder (IB) from Xcode. There you can’t easily use your own version of NSSearchField, but you have to stick with the original one. Before we proceed the obligatory screenshot:

The power of Archives

What we need is a method of setting our own cell class even when the control is already instantiated. In Cocoa it is possible to Archive and Serialize any object which implements the NSCoding protocol. Archiving means that the whole class hierarchy, with all properties and connections, is saved into a stream. Xcode makes heavy use of this in the nib file format where all the project data of the IB is written in. This alone doesn’t help us much, but additional to the archiving and unarchiving work, it is possible to replace classes in the decoding step. The relevant classes are NSKeyedArchiver and NSKeyedUnarchiver. NSKeyedUnarchiver has a method setClass:forClassName which allow this inline replacement and the following code shows how to use it:

NSSearchField *pSearch = [[NSSearchField alloc] init];
/* Replace the cell class used for the NSSearchField */
[NSKeyedArchiver setClassName:@"MySearchFieldCell"
      forClass:[NSSearchFieldCell class]];
[pSearch setCell:[NSKeyedUnarchiver
      unarchiveObjectWithData: [NSKeyedArchiver
        archivedDataWithRootObject:[pSearch cell]]]];
/* Get the original behavior back */
[NSKeyedArchiver setClassName:@"NSSearchFieldCell"
      forClass:[NSSearchFieldCell class]];

Basically this creates an archive of the current NSSearchFieldCell of the NSSearchField, which is instantly unarchived, but with the difference that the NSSearchFieldCell class is replaced by MySearchFieldCell. Because the archiving preserve all settings the new created class will have the same settings like the old one. The last call to NSKeyedArchiver will restore the default behavior.

Conclusion

This post should have removed some of the mysteries of the control and cell relationship in Cocoa. Additional to a simple derivation approach, a much more advanced way for setting the cell of a control was shown. This allows the replacement of any class hierarchy without loosing any runtime settings. If you need such replacements much more often or working with the IB, you should have a look at Mike’s post which shows a more generic way of the archive/unarchive trick.