How to create a star rater with Cappuccino
Update: REVISED: How to create a star rater with Cappuccino
After the Atlas demo, interest in Cappuccino has picked up again. In this tutorial I’d like to how to create a star rater with Cappuccino. A star rater is a control/widget that lets the user choose a visual rating using stars. Komodo Media has shown techniques to do this with css in 2005, 2006 and 2007.
The Approach
We will be rebuilding Example 2 of Part Deux. The main idea will be to set two CPViews into a parent CPView. One to control the current selected amount of stars and a second for
for hover effect. If you don’t have any experience with Objective-J, I recommend Learning Objective-J, though you should still be able to follow along.
Precosiderations
Cappuccino does not support sprits, therefor we have to split the background-image from Example 2 into tree separate 25x25 images.
empty.gif
set.gif
active.gif
Building the Control
When writing Cappuccino controls/widgets, it is often useful to start by subclassing CPView and start from there. Our control will feature the actual rater as well as an indicator, that shows the current selection state.
The Basic StarRater
The StarRater displays a set of empty stars and allows to select the amount of stars with the mouse pointer.
@import <Foundation/CPObject.j>
// will hold our images
var starEmpty, starSet, starActive;
@implementation StarRater : CPView
{
int _value; // the number of selected stars
int _hoverStars; // the number of stars where the mouse hovers
CPView _starFrame; // displays the selected stars
CPView _hoverFrame; // displays the stars when the mouse hovers over the control
id _delegate; // a callback to update when the stars changed
}
+ (void)initialize
{
starEmpty = [[CPImage alloc] initWithContentsOfFile: "Resources/StarRater/empty.gif" size: CPSizeMake(25, 25)];
starSet = [[CPImage alloc] initWithContentsOfFile: "Resources/StarRater/set.gif" size: CPSizeMake(25, 25)];
starActive = [[CPImage alloc] initWithContentsOfFile: "Resources/StarRater/active.gif" size: CPSizeMake(25, 25)];
}
- (id)initWithFrame:(CPRect)frame
{
self = [super initWithFrame: frame];
// FIXME: set the mousepointer
self._DOMElement.style.cursor = "pointer";
// set our background to a pattern of the empty star image
[self setBackgroundColor:[CPColor colorWithPatternImage:starEmpty]];
// set the initial values for our control
_value = 0;
_hoverStars = 0;
// construct the Frames
_starFrame = [[CPView alloc] initWithFrame:[self bounds]];
[_starFrame setBackgroundColor:[CPColor colorWithPatternImage:starSet]];
_hoverFrame = [[CPView alloc] initWithFrame:[self bounds]];
[_hoverFrame setBackgroundColor:[CPColor colorWithPatternImage:starActive]];
// set the size star and hover frame. (both to 0 width)
[self _redraw];
[self _redrawHover];
[self addSubview: _starFrame];
[self addSubview: _hoverFrame];
return self;
}
// setter and getter for the delegate
- (void)setDelegate:(id)aDelegate
{
if (_delegate == aDelegate)
return;
_delegate = aDelegate;
[self _update];
}
- (id)delegate
{
return _delegate;
}
// setter and getter for the value
- (void)setValue:(int)value
{
if(_value == value)
return;
_value = value;
// if the value was set,
// we want to update the delegate a notification
// and redraw our frames accordingly.
[self _update];
[self _redraw];
}
- (int)intValue
{
return _value;
}
// inform the delegate about the current value.
- (void)_update
{
if(_delegate && [_delegate respondsToSelector:@selector(updateWithIntValue:)])
[_delegate updateWithIntValue:_value];
}
// notify the delegate about temporary (hover) changes
- (void)_updateTemporary
{
if(_delegate && [_delegate respondsToSelector:@selector(updateTemporaryWithIntValue:)])
[_delegate updateTemporaryWithIntValue:_hoverStars];
}
// redraw our child frames.
- (void)_redraw
{
[_starFrame setFrameSize:CPSizeMake(_value * 25, 25)];
}
- (void)_redrawHover
{
[_hoverFrame setFrameSize:CPSizeMake(_hoverStars * 25, 25)];
}
// mouse movement handler
- (void)mouseDown:(CPEvent)anEvent
{
// compute the offset relative to this frame
var offset = [self convertPoint:[anEvent locationInWindow] fromView:nil].x
// set a left threshold. If we click less then 5px, we won't select a star.
if(offset < 5) [self setValue:0];
// otherwise we set our value
else [self setValue:Math.ceil(offset/25)];
// setting hoverStars to 0 and redrawing them gives
// a visual feedback about the change.
_hoverStars = 0;
[self _redraw];
[self _redrawHover];
}
// when the mouse enters or left the view,
// we want to reset the visual hover aid.
- (void)mouseEntered:(CPEvent)anEvent
{
_hoverStars = 0;
[self _update];
[self _redrawHover];
}
- (void)mouseExited:(CPEvent)anEvent
{
_hoverStars = 0;
[self _update];
[self _redrawHover];
}
// on mouse move, we construct a visual hovering effect.
- (void)mouseMoved:(CPEvent)anEvent
{
var offset = [self convertPoint:[anEvent locationInWindow] fromView:nil].x
if(offset < 5) starValue = 0;
else starValue = Math.ceil(offset/25);
if(starValue == _hoverStars) return;
_hoverStars = starValue;
[self _updateTemporary];
[self _redrawHover];
}
@end
This gives us a pretty star rater, but it doesn’t show what we selected. As we want to display a message, we will subclass the CPTextField and implement the updateWithIntValue: and updateTemporaryWithIntValue: methods, which will be called from our StarRater.
The Indicator Label
Displays the current selected value to give the star rating a certain meaning and assist in the selection.
@import <Foundation/CPObject.j>
@implementation ActiveTextField : CPTextField
{
var _values;
}
- (id)initWithFrame:(CPRect)aFrame
{
self = [super initWithFrame:aFrame];
// initialize the values we want to display
_values = [ @"No selection",
@"Failed",
@"Not good",
@"Average",
@"Good",
@"Very good",
@"Excellent" ];
return self;
}
- (void) updateTemporaryWithIntValue:(int)value
{
[self updateWithIntValue:value];
}
- (void) updateWithIntValue:(int)value
{
[self setStringValue:_values[value]];
}
@end
To wrap it all up we put our custom control into a handy CPView.
Unification View
Having two separate controls, who depend on each other would cause us to write quite some boilerplate every time we want to use them. The solution is to create a control that unifies both.
@import <Foundation/CPObject.j>
@import "StarRater.j"
@import "ActiveTextField.j"
@implementation StarRaterView : CPView
{
CPView _rater;
}
- (id)initWithFrame:(CPRect)aFrame
{
self = [super initWithFrame:aFrame];
// setup the rater and the indicator
_rater = [[StarRater alloc] initWithFrame:CGRectMake(0, 0, 6 * 25, 25)];
var indicator = [[ActiveTextField alloc] initWithFrame:CGRectMakeZero()];
// let the indicator compute it's size.
[indicator sizeToFit];
// get the height of the indicator to vertical align it with the rater
var height = CGRectGetHeight([indicator frame]);
[indicator setFrameOrigin:CGPointMake(CGRectGetMaxX([_rater frame]) + 10, (25-height)/2.0)];
// set the width of the indicator so that it can hold all labels.
[indicator setFrameSize:CGSizeMake(100,height)];
[_rater setDelegate:indicator];
[self addSubview:_rater];
[self addSubview:indicator];
return self;
}
@end
Displaying our Star Rater
Finally we rewrite the applicationDidFinishLaunching: in the AppController.j from Cappuccino Starter Package to test our new Star Rater Control.
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask],
contentView = [theWindow contentView];
// capture Mouse move events
[theWindow setAcceptsMouseMovedEvents:YES];
// use our custom control
var rater = [[StarRaterView alloc] initWithFrame:CGRectMakeZero()];
[rater setFrameSize:CGSizeMake(250,25)];
[rater setAutoresizingMask:CPViewMinXMargin | CPViewMaxXMargin | CPViewMinYMargin | CPViewMaxYMargin];
[rater setFrameOrigin:CGPointMake((CGRectGetWidth([contentView bounds])
- CGRectGetWidth([rater frame])) / 2.0,
(CGRectGetHeight([contentView bounds])
- CGRectGetHeight([rater frame])) / 2.0)];
// add our rater to the window
[contentView addSubview:rater];
[theWindow orderFront:self];
}
Demo and Download
Here is the non-iframed version and you can also download the source files.
Conclusion
Developing with Cappuccino is not that easy if you have no experience with Cocoa and obj-c as knowledge with these two gets you started much faster. Cappuccinos greatest challenge is it’s weak documentation and makes the learning-curve of Cappuccino and obj-j very steep, especially to those of us who are not experienced desktop-application developer. I can highly recommend the IRC channel #cappuccino on irc.freenode.net, where you will usually get your questions answered pretty quick.
I hope you got an idea on how Cappuccino applications are written. The obj-j syntax might look a little strange at first, but if you give it a try you will get used to it pretty quick.
We have written a lot of code, compared to the 87 lines the Example 2 of Part Deux took. The verbosity of obj-j is not to neglect, we have still a lot more code. Can we still justify the solution? I think we can. We created a ready to use component we can plug into any cappuccino application and it actually does a little bit more then what we set out so solve.
Discuss
This is my first time to explain some code and I would be grateful for suggestion, hints and especially comments. What did you like, what didn’t you like? What did you miss? What would you have done different?
Subscribe
Digg
del.icio.us
Twitter
StumbleUpon
Reddit