Cocoa to Cappuccino Tutorial: RoundedBox

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.

The original Cocoa RoundedBox

The Cocoa Version

RoundedBox Xcode Projects View

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.

The Plan

Here’s what we need to do at the most basic level.

  1. Combine pairs of .m and .h files into single .j files.
  2. Convert preprocessor statements like #import and #define into equivalent @import statements and globals.
  3. Substitute Cocoa typenames for Cappuccino equivalents, e.g. NSObject -> CPObject, IBAction -> @action.
  4. Remove pointer operators, e.g. (NSColorWell *)a -> (CPColorWell) a.

If you want to skip right ahead to the final result it’s available on GitHub.

Converting AppController

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:

  1. Change NS to CP. In Cocoa the standard prefix for classes, hailing from NextStep days, is NS. In Cappuccino the prefix is CP.
  2. Remove * 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) {
  1. Remove the (NSMatrix *) cast. Objective-J is dynamically typed. And, as we will see next, the sender is actually not a matrix in Cappuccino.
  2. Replace 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.

RoundedBox

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;
  1. Remove #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.
  2. Change the #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:

  1. In Cappuccino the y-axis is flipped. (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.
  2. Since we use a full 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.

MainMenu.nib

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.

Links

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!

Subscribe Tweet This
Written by Alexander Ljungberg on 2012 Oct 27 .
Tagged .