Archive for the ‘Style’ Category

Welcome, Designers

Monday, March 10th, 2008

My head has been reeling all weekend after watching the iPhone SDK presentation. The excitement I felt the first time I saw an iPhone has returned full-force after it was crushed by Steve Jobs during last year’s WWDC keynote address. I still think he owes all of us an apology for trying to pass off Ajax techniques as an SDK like we’re stupid. Anyway, I’m super happy and optimistic about all the new technology we get to play with and like many others, I can’t help but think about the future. There’s one thing in particular that I can’t get off my mind.

There are going to be lots of new Cocoa developers and many of them will come from the field of design, not software development. Some will have never programmed before in their life. This happened with HTML and CSS when the web became popular. It’s going to happen in the world of iPhone development and Mac desktop application development will naturally follow.

This influx of new developers means that the usability of the Cocoa’s APIs are going to be put to the test like never before. Bugs like NSView’s autoresizing not working as advertised or the fact that the methods to customize the drag and drop highlights of an NSTableView are private and not legally accessible to developers are not going to sit well with this new generation of Mac devs, who will demand control over every aspect of their visual design.

It should be possible for the Cocoa engineers to give developers complete control over the visual properties and interactive behaviors of their classes. I don’t mean in that convoluted creating a custom cell way, it should be easy — HTML and CSS easy (in fact, why don’t we use style sheets?). If there are points where this is technically impossible, it’s a design flaw of the framework and should be fixed. If it means writing a new NSTableView, so be it.

Sorry, you’re screwed

I keep thinking of an article I read about the history of CSS and this part in particular:

Meanwhile, writers of Web pages were complaining that they didn’t have enough influence over how their pages looked. One of the first questions from an author new to the Web was how to change fonts and colors of elements. HTML at that time did not provide this functionality - and rightfully so. This excerpt from a message sent to the www-talk mailing list early in 1994, gives a sense of the tensions between authors and implementors:

In fact, it has been a constant source of delight for me over the past year to get to continually tell hordes (literally) of people who want to — strap yourselves in, here it comes — control what their documents look like in ways that would be trivial in TeX, Microsoft Word, and every other common text processing environment: “Sorry, you’re screwed.”

The author of the message was Marc Andreessen, one of the programmers behind NCSA Mosaic. He later became a co-founder of Netscape and by then his views - if they ever were his views - on formatting had changed.

I doubt that any Cocoa engineer has such a flippant attitude towards designers. After all, this is OS X — the best looking operating system on the market. I just hope that they will take these types of issues into more serious consideration when mapping out their priorities. Sure, advertising “New Table Views!” in Cocoa isn’t as sexy a feature as Core Animation, but it’ll make everyone’s life easier in the long run. Imagine this question popping up on the Cocoa-dev list: “How do I customize the background color of a column in an NSTableView”. Now think of the answer…

Styling an NSTableView (DTTAH)

Friday, February 22nd, 2008

NSTableView is a great class for displaying long lists of data, but its default implementation is kind of boring looking. This post will show you how give your table views extra sex appeal. I’m categorizing it as a “don’t try this at home” tutorial because it sometimes involves overriding private NSTableView methods. Be warned that Apple can and will change the private framework code without notice and that some of the techniques used in this tutorial may no longer work. The tutorial and the example project are meant to be educational and exploratory. They’re not necessarily “right”, so use them wisely.

If anyone out there knows how to get comparable results without using NSTableView private methods, please please leave a comment.

The Example Project

drag.jpg

The CustomTableView Xcode project can be dowloaded here.

If you’re new to Cocoa or subclassing NSTableView, the tutorial will be most useful if you download the project and follow along in the source code. The comments will help explain implementation details. The project has 3 classes:

  • MyController (the application’s main controller and the table’s delegate and data source)
  • MyTableView (the NSTableView subclass)
  • MyCell (the NSCell subclass)

It also adds categories to NSColor and NSBezierPath for custom alternating background colors and rounded rectangles.

In the sample project, customizations are made to the following visual properties of the table:

  • Alternating background colors
  • Selection highlight
  • Drag image
  • Drop highlight on rows
  • Drop highlight between rows

The table also uses an NSCell subclass to draw an icon and two rows of text. Let’s start with NSCell.

NSCell

Table views use the NSCell class to draw content in their rows. The first step to customizing the look of your table is to create an NSCell subclass and override the method

-(void)drawInteriorWithFrame:(NSRect)theFrame inView:(NSView *)theView

In the example project, the MyCell class divides the NSRect passed in the frame argument into three smaller rectangles that it uses to position the image, title and subtitle. The image and text are then drawn into those rectangles using NSImage and NSString’s respective -(void)drawInRect methods.

After you make your subclass, you need to let the table view know about it. You can set the cell for the columns of the table using NSTableColumn’s method

-(void)setDataCell:(NSCell *)theCell

In the example project, the MyController class sets the data cell for the table’s column with the following lines of code in awakeFromNib:

MyCell * aCustomCell = [[[MyCell alloc] init] autorelease];
[[oMyTableView tableColumnWithIdentifier:@"theTableColumn"] setDataCell:aCustomCell];

NSTableView

After you have an NSCell subclass, you may also want to customize some of NSTableView’s visual properties. You can set the background color, row height, whether or not the table should draw horizontal or vertical lines and the color of the lines using the NSTableView inspector palette in Interface Builder. Other adjustments take a little bit of code.

Alternating Background Colors

If you choose to display alternating background colors, the default colors are light blue and white. A simple way to customize the alternating background color scheme in your application is to add a category to NSColor that overrides the method

+(NSArray *)controlAlternatingRowBackgroundColors 

In your implementation of this method, return an NSArray containing the two colors you want your tables to use.

Selection Highlight

In the example project, MyTableView draws a rounded rectangle in the selected rows. If you want to draw your own selection highlight, override the following method in your NSTableView subclass:

-(void)highlightSelectionInClipRect:(NSRect)theClipRect

This method is asking you to draw the highlights for the rows that are currently selected within the “clip rect”, or the area of the table that is visible. One way to do this is to:

  1. Use NSTableView’s method
    -(NSRange)rowsInRect:(NSRect)theRect;

    to get the range of row indexes that are currently visible in the clip rect.

  2. Use NSTableView’s method
    -(NSIndexSet)selectedRowIndexes;

    to get a list of all of the selected rows in the table.

  3. Go through the list of visible row indexes. If an index is included in the list of selected indexes, get the NSRect for that row with the NSTableView method:
    -(NSRect)rectOfRow:(int)theRowIndex;
  4. Draw your custom highlight style inside this rectangle.

If your application is running on Tiger or any previous OS and you only override this method, you’ll notice that the table still draws its default highlight on top of your drawing. The second thing you must do to draw a custom selection highlight in this case is override the private method:

-(id)_highlightColorForCell:(NSCell *)cell
{
    return nil;
}

Returning nil in this method will “stop” the table view from drawing the default highlight rectangle (weird, huh?).

If you only want to change the highlight selection color instead of drawing a custom highlight, you can simply override the method

-(id)_highlightColorForCell:(NSCell *)cell

to return the color of your choice.

Drag Image

If your NSTableView subclass supports dragging, you can create a custom drag image by overriding the method

- (NSImage *)dragImageForRowsWithIndexes:(NSIndexSet *)theRowIndexes
			    tableColumns:(NSArray *)theTableColumns
				   event:(NSEvent *)theEvent
				  offset:(NSPointPointer)theOffset

In this method make an NSImage, draw what you want into the image and return it. You can get your NSTableView subclass to give you an NSImage with the content of the cell drawn inside by calling super’s implementation of the method.

In the example project, MyTableView composites the NSImage returned by super onto another NSImage with a rounded rectangle drawn inside. The new NSImage is returned as the drag image.

Drop Highlights

Drop highlights in NSTableView are notoriously fugly and they are not “legally” customizable. In the example project, MyTableView overrides some private methods to draw a black rounded rectangle to highlight drags over rows and a line with a small accent circle (like NSOutlineView) to highlight drags between rows.

For Tiger and earlier, there are two private methods that your NSTableView subclass can override to draw custom drop highlights on and between rows:

-(void)_drawDropHighlightOnRow:(int)theRowIndex;
-(void)_drawDropHighlightBetweenUpperRow:(int)theUpperRowIndex
			     andLowerRow:(int)theLowerRowIndex
				atOffset:(float)theOffset; 

In both cases, use the NSTableView method

-(NSRect)rectOfRow:(int)theRowIndex

to retrieve the rectangle of the row you want to highlight.

In Leopard, things get a little more complicated. There are several private methods that NSTableView might use to draw the drop highlight between rows. Through trial and error, I found that overriding the following method does the trick:

- (void)_drawDropHighlightBetweenUpperRow:(int)theUpperRowIndex
			      andLowerRow:(int)theLowerRowIndex
				    onRow:(int)theRow
				 atOffset:(float)theOffset

A new NSTableView feature in Leopard is the drop highlight color. Like the rest of the drop highlight visual properties, it is only customizable by overriding a private method. If you don’t override this method, the default implementation will draw a light blue background over the row that is the current drop target. If light blue isn’t your thing, you can override

+(id)_dropHighlightBackgroundColor;

to return the color of your choice. If you want your table to have the same highlights running in Leopard as it does in Tiger, return [NSColor clearColor].

Do Not Try This At Home!!!

I hope that this post will help you build a beuatiful table view that your users will love to look at, but once again, please please be careful when using private NSTableView methods for selection and drop highlights. While it’s fun and educational to explore the AppKit framework, you absolutely should not override private methods in your shipping app. I’ve filed an enhancement radar in the hopes that Apple will make these methods public:

rdar://problem/5758731

If anyone knows of other techniques that are more legal, please share them in the comments.

Resources

The source code in the CustomTableView example project has detailed comments that should help answer any questions you may have about implementing the techniques described in this post.

Other useful projects with alternate approaches to customizing the visual elements of a table:

If you are new to Cocoa drawing, I’d suggest reading the Quartz 2D Programming Guide as well the Learn Quartz tutorials at CocoaDevCentral.

For more information on NSTableView, start with the Introduction to Table Views Programming Guide. There is an NSTableView tutorial on the CocoaDev wiki as well as one at CocoaDevCentral. These will help with the less superficial parts of programming an NSTableView (like how to really implement drag and drop and data sources).

For more information on NSCell, read the Control and Cell Programming Topics for Cocoa.

If NSTableView isn’t exactly what you’re looking for and you’re targeting OS 10.5+, you might want to look into the new Cocoa class, NSCollectionView.

Also, If you’re targeting OS 10.5+, take a look at the new source list selection style for NSTableView.

Finally, class-dump lets you look at stuff you shouldn’t be looking at.