Because Cappuccino is a reimplementation of Cocoa for the web, most Mac apps apps can be converted to Cappuccino with very few architectural changes. In this tutorial we will convert the handy RoundedBox widget by accomplished Cocoa dev Matt Gemmel to Cappuccino.
The final app we will create can be tried here: Cappuccino Rounded Box Sample App.
RoundedBox is a Cocoa control. It’s a subclass of NSBox
which draws a particular box design with rounded corners, an editable title bar and an optional gradient background.
The Cocoa version comes with a simple demo Xcode project. The files we will be interested in are:
AppController
RoundedBox
MainMenu.nib
We will not convert CTGradient.m
because Cappuccino has a built in CPGradient
class.
RoundedBox
is the main class we are interested in. The AppController
and MainMenu
nib compose the sample application included with the project, which we will also recreate.
Here’s what we need to do at the most basic level.
.m
and .h
files into single .j
files.#import
and #define
into equivalent @import
statements and globals.NSObject
-> CPObject
, IBAction
-> @action
.(NSColorWell *)a
-> (CPColorWell) a
.If you want to skip right ahead to the final result it’s available on GitHub.
We’ll start by creating AppController.j
based on AppController.m
and AppController.h
.
// AppController.m:1
#import "AppController.h"
@implementation AppController
In Cappuccino #import
is @import
. That said, rather than replacing this line with @import "AppController.h"
we will just omit it. A single .j
file takes on both the role of the header and the implementation.
We’ll inspect the header file and retain the bits we care about in the main .j
file. Here’s the part of AppController.h
we’re interested in, followed by our new AppController.j
:
// AppController.h:4
#import "RoundedBox.h"
@interface AppController : NSObject
{
IBOutlet RoundedBox *box;
IBOutlet NSColorWell *gradientStartColorWell;
IBOutlet NSColorWell *gradientEndColorWell;
IBOutlet NSColorWell *backgroundColorWell;
IBOutlet NSColorWell *borderColorWell;
}
Cappuccino:
// AppController.j:1
@import "RoundedBox.j"
@implementation AppController : CPObject
{
@outlet RoundedBox box;
@outlet CPColorWell gradientStartColorWell;
@outlet CPColorWell gradientEndColorWell;
@outlet CPColorWell backgroundColorWell;
@outlet CPColorWell borderColorWell;
}
As promised the Cappuccino code is nearly identical to the Cocoa code.
We moved all the bits from the @interface
right into the implementation, specifically: an import statement, the superclass declaration and the ivar declaration block. The types of the ivars have been transformed to Cappuccino equivalents. Note that there is no pointer type in Objective-J so we remove the *
signifying a pointer.
The rest of the code comes from AppController.m
:
// AppController.m:6
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication
{
return YES;
}
Cappuccino:
// AppController.j:13
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(CPApplication)theApplication
{
return YES;
}
While this method has no effect in a web browser app, it continues to illustrate the two most common changes necessary:
NS
to CP
. In Cocoa the standard prefix for classes, hailing from NextStep days, is NS
. In Cappuccino the prefix is CP
.*
pointer operators.Here’s another point of interest in AppController.m
:
// AppController.m:38
- (IBAction)changeBackground:(id)sender
{
if ([[(NSMatrix *)sender selectedCell] tag] == 0) {
Cappuccino:
// AppController.j:43
- (@action)changeBackground:(id)sender
{
if ([[sender selectedRadio] tag] == 0) {
(NSMatrix *)
cast. Objective-J is dynamically typed. And, as we will see next, the sender is actually not a matrix in Cappuccino.selectedCell
with selectedRadio
. Cappuccino does not use ‘cells’. Instead the NSMatrix
is automatically converted to a CPRadioGroup
containing two regular radio button controls.The rest of AppController.j
is done the same way. We replace Cocoa classnames with Cappuccino classnames, we drop casts and change pointers to references.
You can review the final version of the Cappuccino code here: AppController.j
.
The AppController in this app just drives the sample app. The actual control is defined by RoundedBox.m
and RoundedBox.h
.
// RoundedBox.m:27
#import "RoundedBox.h"
#define MG_TITLE_INSET 3.0
Cappuccino:
// AppController.j:27
var MG_TITLE_INSET = 3.0;
#import "RoundedBox.h"
. We will deal with the RoundedBox.h
file the same way we did in AppController.j
, by moving all relevant bits into a combined RoundedBox.j
.#define
to a file global. This global will only be visible within RoundedBox.j
.Here is the part of RoundedBox.h
we are interested in:
// RoundedBox.h:30
@interface RoundedBox : NSBox {
BOOL _drawsTitle;
float borderWidth;
NSColor *borderColor;
NSColor *titleColor;
NSColor *gradientStartColor;
NSColor *gradientEndColor;
NSColor *backgroundColor;
BOOL drawsFullTitleBar;
BOOL selected;
BOOL drawsGradientBackground;
NSRect titlePathRect;
}
Cappuccino:
// RoundedBox.j:30
@implementation RoundedBox : CPBox
{
BOOL _drawsTitle;
float borderWidth;
CPColor borderColor;
CPColor titleColor;
CPColor gradientStartColor;
CPColor gradientEndColor;
CPColor backgroundColor;
BOOL drawsFullTitleBar;
BOOL selected;
BOOL drawsGradientBackground;
CGRect titlePathRect;
}
We replace the NSRect
type with a CGRect
instead of a CPRect
. In Cappuccino, CGRect
and CPRect
are the same thing, but CGRect
is preferred.
We don’t need to touch initWithFrame:
nor dealloc
, apart from changing an NSRect
type to a CGRect
.
The dealloc
method isn’t important in Cappuccino, Cappuccino being garbage collected. The method won’t be called and even if it was all the release
statements would be no-ops. Still, there is no harm in leaving it in.
// RoundedBox.j:56
- (void)setDefaults
{
_drawsTitle = YES;
[[self titleCell] setLineBreakMode:NSLineBreakByTruncatingTail];
[[self titleCell] setEditable:YES];
Cappuccino:
// RoundedBox.j:66
- (void)setDefaults
{
_drawsTitle = YES;
[[self titleView] setLineBreakMode:CPLineBreakByTruncatingTail];
[[self titleView] setEditable:YES];
[[self titleView] setDelegate:self];
In Cocoa, the NSBox
superclass has a method called titleCell
. Again, Cappuccino doesn’t use cells - everything is view based. The title of a CPBox
is just a regular text label (a CPTextField
), which we can get at with the titleView
accessor.
We’ll also set the delegate of the text label to support title editing. This is required because the full CPTextField
behaves a little differently than the cell based title in Cocoa, and we need to make sure it has a delegate in case editing is started through a direct click.
// RoundedBox.m:76
- (void)awakeFromNib
{
// For when we've been created in a nib file
[self setDefaults];
}
Cappuccino:
// RoundedBox.j:89
- (void)awakeFromCib
{
// For when we've been created in a nib file
[self setDefaults];
}
In Cappuccino we have cib
files instead of nib
files, so the awakeFromNib
method name needs to become awakeFromCib
.
// RoundedBox.m:90
- (void)mouseDown:(NSEvent *)evt {
if (NSPointInRect([self convertPoint:[evt locationInWindow] fromView:nil], titlePathRect)) {
_drawsTitle = NO;
[self setNeedsDisplay:YES];
NSRect editingRect = NSInsetRect([[self titleCell] drawingRectForBounds:titlePathRect],
MG_TITLE_INSET + borderWidth,
MG_TITLE_INSET);
editingRect.size.width = [self frame].size.width - (2.0 * editingRect.origin.x);
[[self titleCell] editWithFrame:[self convertRect:editingRect toView:nil]
inView:[[self window] contentView]
editor:[[self window] fieldEditor:YES forObject:[self titleCell]]
delegate:self
event:evt];
}
}
Cappuccino:
// RoundedBox.j:102
- (void)mouseDown:(CPEvent)evt {
if (CPPointInRect([self convertPoint:[evt locationInWindow] fromView:nil], titlePathRect)) {
[[self window] makeFirstResponder:[self titleView]];
}
}
Having a full text field as the label gives us some nice benefits here. The Cappuccino code is much shorter. We don’t manually need to set up the title cell for editing nor disable title drawing. We just activate the regular text field editing behaviour.
// RoundedBox.m:107
- (BOOL)textShouldEndEditing:(NSText *)fieldEditor
{
_drawsTitle = YES;
if ([[fieldEditor string] length] > 0) {
[self setTitle:[fieldEditor string]];
} else {
NSBeep();
[self setNeedsDisplay:YES];
}
return YES;
}
- (void)textDidEndEditing:(NSNotification *)aNotification
{
[[self titleCell] endEditing:[[self window] fieldEditor:YES forObject:[self titleCell]]];
}
Cappuccino:
// RoundedBox.j:108
- (void)controlTextDidEndEditing:(CPNotification)aNotification
{
_drawsTitle = YES;
var stringValue = [[self titleView] stringValue];
if ([stringValue length] > 0) {
[self setTitle:stringValue];
} else {
[self setNeedsDisplay:YES];
[[self titleView] setStringValue:[self title]];
}
}
We’ll use the appropriate CPTextField
delegate method here.
There is no CPBeep()
in Cappuccino. We could have recreated the behaviour using CPSound
and an appropriate sound sample if we had wanted to.
// RoundedBox.m:126
- (void)resetCursorRects {
[self addCursorRect:titlePathRect cursor:[NSCursor IBeamCursor]];
}
Cappuccino:
// RoundedBox.j:121
- (void)resetCursorRects {
// [self addCursorRect:titlePathRect cursor:[CPCursor IBeamCursor]];
}
Cappuccino does not support cursor rects (nor tracking areas) today, so we’ll have to disable this feature.
The drawRect:
method works pretty much the same in Cappuccino as in Cocoa. There are two things we have to pay attention to:
(0, 0)
is the upper left coordinate of a view, and as the y coordinate increases we move downwards. This means that when converting from Cocoa we normally need to manually flip the drawing coordinates.CPTextField
to render our title we don’t need to explicitly draw it.For the sake of brevity, we’ll skip this code, but it’s all available in the final project.
Let’s briefly examine the final major change, swapping out CTGradient
:
// RoundedBox.m:174
if ([self drawsGradientBackground]) {
// Draw gradient background
NSGraphicsContext *nsContext = [NSGraphicsContext currentContext];
[nsContext saveGraphicsState];
[bgPath addClip];
CTGradient *gradient = [CTGradient gradientWithBeginningColor:[self gradientStartColor] endingColor:[self gradientEndColor]];
NSRect gradientRect = [bgPath bounds];
[gradient fillRect:gradientRect angle:270.0];
[nsContext restoreGraphicsState];
}
Cappuccino:
// RoundedBox.j:169
if ([self drawsGradientBackground])
{
// Draw gradient background
var nsContext = [CPGraphicsContext currentContext];
[nsContext saveGraphicsState];
[bgPath addClip];
var gradient = [[CPGradient alloc] initWithColors:[[self gradientStartColor], [self gradientEndColor]]],
gradientRect = [bgPath bounds];
[gradient drawInRect:gradientRect angle:90.0];
[nsContext restoreGraphicsState];
}
Note that we reversed the angle to match the flipped coordinate system.
You can review the final version of the Cappuccino code here: RoundedBox.j
.
Imagine how much work it must be to convert all these little widget placements and resizing masks from the Cocoa MainMenu.nib
to Cappuccino.
Think again! The original nib will work just fine with Cappuccino.
We’ll use the nib2cib
tool to get the cib file we need:
cd cappuccino-rounded-box
cp -Rf ../Cocoa/Source/English.lproj/MainMenu.nib Resources/
nib2cib
That’s all there is to that. Our project is done.
To run the source code version, you will need to install the latest version of the Cappuccino framework (master
branch as of this writing, version 0.9.6 once it’s available.) You can do this using the capp
command:
git clone git://github.com/slevenbits/cappuccino-rounded-box.git
cd cappuccino-rounded-box
capp gen -f --force
open index.html
To use cappuccino-rounded-box
as a Framework in your own app, simply copy RoundedBox.j
into your Frameworks folder and include it in your Cappuccino AppController.j
in the regular manner:
@import <RoundedBox.j>
Do you have any old Mac apps that you ported to run on the web with Cappuccino? Let me know on Twitter. Happy coding!