This tutorial is the first in a multi-part series in which we will be building a full featured Scrapbooking application, complete with backend storage. The goal of these tutorials is to start off simple and incrementally add features, mimicking the development of a real world application and allowing us to focus on different aspects of Cappuccino along the way. For this first installment, we will be getting familiar with Cappuccino’s view hierarchy and graphics subsystem.
Before we jump into any serious programming, let’s begin by getting a good idea of what we’re building. The goal of the scrapbook application is to provide users with a place to arrange their photos into albums. These albums will have different themes and allow users to insert, scale, rotate, and crop their images. When done, they will be able to save and share their albums.
As I mentioned earlier, we will be focusing on some of the core graphical aspects of this application right now, and saving most of the overall design decisions of this application for later. So the very first thing we’re going to tackle is building a solid UI for scaling and cropping images. The plan is to present the user with a few controls in a heads up display which will allow them to manipulate selected images, and then go away when the user is done editing. You can see a working example of this here. You should take a few minutes to play around with it and get an idea of what we will be building. You can also view or download the entire source of the application here if you prefer to follow along with the code.
Since we’re not doing anything too complex yet, we’re going to build off
of the Starter Template. Open up AppController.j
and
delete all the code relating to the HelloWorld textfield, such that all
that is left in applicationDidFinishLaunching:
is the following:
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
var theWindow = [[CPWindow alloc]
initWithContentRect:CGRectMakeZero()
styleMask:CPBorderlessBridgeWindowMask],
contentView = [theWindow contentView];
[theWindow orderFront:self];
}
If you now rerun this application by hitting refresh, you will see that nothing shows up on the screen. So let’s take a moment to understand what is going on in these very few lines of code.
In Cappuccino, all view hierarchies start from within a CPWindow
. So
to get anything to display on the screen, we first require the creation
of a window. CPWindow
s should not be confused with the browser window
however. CPWindow
s exist within the main browser window, and there can
be multiple of them. The relationship between CPWindow
and the browser
window is similar to that between Desktop windows and the screen. In
Cappuccino, we often refer to the browser window as the bridge
,
since the browser window bridges the web to the desktop.
When you create a CPWindow
, you generally specify the content
rectangle and style mask. The content rectangle describes the position
and size of the inner content ignoring the chrome around it. The style
mask describes the chrome, or look, of the window. In this case, we are
creating a window with the “borderless bridge” look. The borderless
bridge style describes a CPWindow
with no chrome at all, and that
takes up the entire size of the browser window, which is why we don’t
bother sending a meaningful value in for the content rectangle – it
gets set automatically for us. The CPBorderlessWindowMask
is similar
to this, but does not follow the size of the browser window.
Now, the contentView
of a CPWindow
is the root view class of a
window. This is where you will be putting all of your custom UI, such as
buttons, text fields, etc. Let’s go ahead and set the color of our
content view to be black by adding this code right after the
declaration:
[contentView setBackgroundColor:[CPColor blackColor]];
If you refresh now, you’ll see that the entire browser window has turned black. This is because, as we described earlier, borderless bridge windows “fit” their parent browser windows.
Since we’re on the topic of windows, let’s go ahead and make our
heads-up-display (HUD) controller that we mentioned earlier. First,
let’s just make a HUD panel. A panel is a type of window that floats on
top of other windows. Create a panel in the applicationDidFinishLaunching:
method:
var HUDPanel = [[CPPanel alloc]
initWithContentRect:CGRectMake(0, 0, 225, 125)
styleMask:CPHUDBackgroundWindowMask | CPClosableWindowMask];
[HUDPanel setFloatingPanel:YES];
[HUDPanel orderFront:self];
Now if you refresh, you’ll see a little HUD panel show up at the top
left corner of your screen. You can drag on it to move it around, and
click on the close box to close it. As you can see, in this case we
specified an actual content rectangle since we aren’t relying on the
panel simply stretching to the entire size of the bridge. We chose two
style masks in this case: CPHUDBackgroundWindowMask
and
CPClosableWindowMask
. CPHUDBackgroundWindowMask
notifies our
panel that we want the HUD look, and CPClosableWindowMask
lets the
panel know that it should display its close box. If you remove the
CPClosableWindowMask
mask, you will no longer be able to close the
panel.
Let’s start giving the panel some contents. The easiest thing to add is a title:
[HUDPanel setTitle:"Inspector"];
We will now add our two sliders to control the scale and rotation of our
photos. To do this we will use the CPSlider
class. Just as we did with
the main window, we will grab the contentView of the HUD panel to place
these sliders in:
var panelContentView = [HUDPanel contentView],
centerX = (CGRectGetWidth([panelContentView bounds]) - 135.0) / 2.0;
var scaleSlider = [[CPSlider alloc] initWithFrame:CGRectMake(centerX, 13.0, 135.0, 24.0)];
[scaleSlider setMinValue:50];
[scaleSlider setMaxValue:150];
[scaleSlider setValue:100];
[panelContentView addSubview:scaleSlider];
var scaleStartLabel = [self labelWithTitle:"50%"],
scaleEndLabel = [self labelWithTitle:"150%"];
[scaleStartLabel setFrameOrigin:CGPointMake(
centerX - CGRectGetWidth([scaleStartLabel frame]), 10)];
[scaleEndLabel setFrameOrigin:
CGPointMake(CGRectGetMaxX([scaleSlider frame]), 10)];
[panelContentView addSubview:scaleStartLabel];
[panelContentView addSubview:scaleEndLabel];
rotationSlider = [[CPSlider alloc] initWithFrame:
CGRectMake(centerX, 43, 135, 24)];
[rotationSlider setMinValue:0];
[rotationSlider setMaxValue:360];
[rotationSlider setValue:0];
[panelContentView addSubview:rotationSlider];
var rotationStartLabel = [self labelWithTitle:"0\u00B0"],
rotationEndLabel = [self labelWithTitle:"360\u00B0"];
[rotationStartLabel setFrameOrigin:CGPointMake(centerX - CGRectGetWidth([rotationStartLabel frame]), 40)];
[rotationEndLabel setFrameOrigin:CGPointMake(CGRectGetMaxX([rotationSlider frame]), 40)];
[panelContentView addSubview:rotationStartLabel];
[panelContentView addSubview:rotationEndLabel];
You’ll notice this makes use of the labelWithTitle:
method that we
have not yet defined. Go ahead and define it in AppController like so:
- (CPTextField)labelWithTitle:(CPString)aTitle
{
var label = [[CPTextField alloc] initWithFrame:CGRectMakeZero()];
[label setStringValue:aTitle];
[label setTextColor:[CPColor whiteColor]];
[label sizeToFit];
return label;
}
This may seem daunting, but it’s really not that complex. Let’s start by
analyzing the labelWithTitle:
method. It should look familiar, since
it’s almost identical to the code that was in the original
implementation of applicationDidFinishLaunching:
. All we are doing is
creating a text field, setting its text contents to be aTitle
,
setting its text color to white, and finally sizing it appropriately and
returning it.
In the slider creation code we are creating two sliders: one to modify
scale and another for rotation, so we only really need to go over half
the code. We are making our sliders 135 pixels wide, so begin by
calculating the center position in our panel’s content view. After we
have this, We create a new slider at this position. We then set three
important values for sliders: the minimum value, the maximum value, and
the current value. The minimum value is what the left hand side of the
slider represents, the maximum is what the right hand side represents,
and the current value is simply where the knob of the slider currently
resides. In this case of the scaleSlider
, we want to be able to
scale our photos from 50% to 150%, and obviously we want them to begin
at 100%. With the rotationSlider, we want to go from 0 degrees to 360
degrees, and start at 0 degrees. To highlight these values, we create 2
labels for each slider to sit to the left and to the right of the
sliders. So, your code should now look like this:
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
var theWindow = [[CPWindow alloc]
initWithContentRect:CGRectMakeZero()
styleMask:CPBorderlessBridgeWindowMask],
contentView = [theWindow contentView];
[theWindow orderFront:self];
var HUDPanel = [[CPPanel alloc]
initWithContentRect:CGRectMake(0, 0, 225, 125)
styleMask:CPHUDBackgroundWindowMask | CPClosableWindowMask];
[HUDPanel setFloatingPanel:YES];
[HUDPanel orderFront:self];
[HUDPanel setTitle:"Inspector"];
var panelContentView = [HUDPanel contentView],
centerX = (CGRectGetWidth([panelContentView bounds])
- 135) / 2;
var scaleSlider = [[CPSlider alloc]
initWithFrame:CGRectMake(centerX, 13, 135, 24)];
[scaleSlider setMinValue:50];
[scaleSlider setMaxValue:150];
[scaleSlider setValue:100];
[panelContentView addSubview:scaleSlider];
var scaleStartLabel = [self labelWithTitle:"50%"],
scaleEndLabel = [self labelWithTitle:"150%"];
[scaleStartLabel setFrameOrigin:CGPointMake(centerX -
CGRectGetWidth([scaleStartLabel frame]), 10)];
[scaleEndLabel setFrameOrigin:
CGPointMake(CGRectGetMaxX([scaleSlider frame]), 10)];
[panelContentView addSubview:scaleStartLabel];
[panelContentView addSubview:scaleEndLabel];
rotationSlider = [[CPSlider alloc]
initWithFrame:CGRectMake(centerX, 43, 135, 24)];
[rotationSlider setMinValue:0];
[rotationSlider setMaxValue:360];
[rotationSlider setValue:0];
[panelContentView addSubview:rotationSlider];
var rotationStartLabel = [self labelWithTitle:"0\u00B0"],
rotationEndLabel = [self labelWithTitle:"360\u00B0"];
[rotationStartLabel setFrameOrigin:CGPointMake(centerX -
CGRectGetWidth([rotationStartLabel frame]), 40)];
[rotationEndLabel setFrameOrigin:
CGPointMake(CGRectGetMaxX([rotationSlider frame]), 40)];
[panelContentView addSubview:rotationStartLabel];
[panelContentView addSubview:rotationEndLabel];
}
- (CPTextField)labelWithTitle:(CPString)aTitle
{
var label = [[CPTextField alloc] initWithFrame:CGRectMakeZero()];
[label setStringValue:aTitle];
[label setTextColor:[CPColor whiteColor]];
[label sizeToFit];
return label;
}
@end
We now have a functioning panel, but it’s not much good to us here
within the applicationDidFinishLaunching:
method. So let’s create a
new file called PhotoInspector.j
to contain it.
In PhotoInspector.j
, we will be creating a subclass of
CPWindowController
to manage this panel. CPWindowController
s are
designed to manage CPWindow
s and their interactions with other
objects. Once we’ve create this class, we’ll move all the relevant code
to it, namely the labelWithTitle
method and view creation code:
@import <AppKit/CPPanel.j>
@import <AppKit/CPWindowController.j>
@implementation PhotoInspector : CPWindowController
{
}
- (id)init
{
var theWindow = [[CPPanel alloc]
initWithContentRect:CGRectMake(0, 0, 225, 125)
styleMask:CPHUDBackgroundWindowMask | CPClosableWindowMask];
self = [super initWithWindow:theWindow];
if (self)
{
[theWindow setTitle:@"Inspector"];
[theWindow setFloatingPanel:YES];
var contentView = [theWindow contentView],
centerX = (CGRectGetWidth([contentView bounds]) - 135) / 2;
scaleSlider = [[CPSlider alloc]
initWithFrame:CGRectMake(centerX, 13, 135, 24)];
[scaleSlider setMinValue:50];
[scaleSlider setMaxValue:150];
[scaleSlider setValue:100];
[contentView addSubview:scaleSlider];
var scaleStartLabel = [self labelWithTitle:"50%"],
scaleEndLabel = [self labelWithTitle:"150%"];
[scaleStartLabel setFrameOrigin:CGPointMake(centerX -
CGRectGetWidth([scaleStartLabel frame]), 10)];
[scaleEndLabel setFrameOrigin:
CGPointMake(CGRectGetMaxX([scaleSlider frame]), 10)];
[contentView addSubview:scaleStartLabel];
[contentView addSubview:scaleEndLabel];
rotationSlider = [[CPSlider alloc]
initWithFrame:CGRectMake(centerX, 43, 135, 24)];
[rotationSlider setMinValue:0];
[rotationSlider setMaxValue:360];
[rotationSlider setValue:0];
[contentView addSubview:rotationSlider];
var rotationStartLabel = [self labelWithTitle:"0\u00B0"],
rotationEndLabel = [self labelWithTitle:"360\u00B0"];
[rotationStartLabel setFrameOrigin:CGPointMake(centerX -
CGRectGetWidth([rotationStartLabel frame]), 40)];
[rotationEndLabel setFrameOrigin:
CGPointMake(CGRectGetMaxX([rotationSlider frame]), 40)];
[contentView addSubview:rotationStartLabel];
[contentView addSubview:rotationEndLabel];
}
return self;
}
- (CPTextField)labelWithTitle:(CPString)aTitle
{
var label = [[CPTextField alloc] initWithFrame:CGRectMakeZero()];
[label setStringValue:aTitle];
[label setTextColor:[CPColor whiteColor]];
[label sizeToFit];
return label;
}
@end
As you can see very little of the code has changed. The only real
addition is calling initWithWindow:
in the new init
method. This is
the standard way to create window controllers. We can now modify our
AppController code to the following:
@import <Foundation/CPObject.j>
@import "PhotoInspector.j"
@implementation AppController : CPObject
{
}
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
var theWindow = [[CPWindow alloc]
initWithContentRect:CGRectMakeZero()
styleMask:CPBorderlessBridgeWindowMask],
contentView = [theWindow contentView];
[contentView setBackgroundColor:[CPColor blackColor]];
[theWindow orderFront:self];
var theInspector = [[PhotoInspector alloc] init];
[theInspector showWindow:self];
}
@end
This successfully encapsulates the inspector panel away. To bring it up
all we need to do now is call showWindow:
on our PhotoInspector
class, which we inherit for free from CPWindowController
. You’ll
notice we also had to include the PhotoInspector
class’ file
PhotoInspector.j
. We use quotes to include this file because it is
local, as opposed to the AppKit files which are in our default search
paths.
Now that we have a photo inspector, we need some photos to inspect. In
our product, Scrapbook will be represented as a series of pages. Each
page will contain a number of panes that each contain an image. These
pages will be represented by PageView
s, so let’s start fleshing out
our PageView
class in a file called PageView.j
.
@import <AppKit/CPView.j>
@implementation PageView : CPView
{
}
@end
As you can see, PageView
is a subclass of CPView
, the core UI class
in Cappuccino. However, we will actually be doing the brunt of our
graphics work not in a view, but in a layer
. In Cappuccino,
CALayer
s are designed and optimized for drawing and performing complex
graphics routines, whereas CPView
s are better at handling UI widgets.
We can add layers to views by making them layer-backed:
@implementation PageView : CPView
{
CALayer _rootLayer;
}
- (id)initWithFrame:(CGRect)aFrame
{
self = [super initWithFrame:aFrame];
if (self)
{
_rootLayer = [CALayer layer];
[self setWantsLayer:YES];
[self setLayer:_rootLayer];
[_rootLayer setBackgroundColor:[CPColor whiteColor]];
[_rootLayer setNeedsDisplay];
}
return self;
}
@end
Here we create the view as usual, but let the view know that it will be
hosting a layer with the setWantsLayer:
method, and adding the actual
layer with setLayer:
. We give the layer a background color, but need
to call setNeedsDisplay
for the layer to actually show itself. In
Cappuccino, you never explicitly tell views or layers to draw, instead
you inform them that they need to do so. This is because Cappuccino
coalesces and optimizes drawing.
As we said earlier, we now need a pane in this page. Pane’s hold images as well as storing the scale and rotation properties of the image it displays. We will be implementing panes as layers:
@implementation PaneLayer : CALayer
{
float _rotationRadians;
float _scale;
CPImage _image;
CALayer _imageLayer;
PageView _pageView;
}
- (id)initWithPageView:(PageView)aPageView
{
self = [super init];
if (self)
{
_pageView = aPageView;
_rotationRadians = 0.0;
_scale = 1.0;
_imageLayer = [CALayer layer];
[_imageLayer setDelegate:self];
[self addSublayer:_imageLayer];
}
return self;
}
- (PageView)pageView
{
return _pageView;
}
- (void)setBounds:(CGRect)aRect
{
[super setBounds:aRect];
[_imageLayer setPosition:
CGPointMake(CGRectGetMidX(aRect),
CGRectGetMidY(aRect))];
}
- (void)setImage:(CPImage)anImage
{
if (_image == anImage)
return;
_image = anImage;
if (_image)
[_imageLayer setBounds:CGRectMake(0.0, 0.0,
[_image size].width, [_image size].height)];
[_imageLayer setNeedsDisplay];
}
- (void)setRotationRadians:(float)radians
{
if (_rotationRadians == radians)
return;
_rotationRadians = radians;
[_imageLayer setAffineTransform:CGAffineTransformScale(
CGAffineTransformMakeRotation(_rotationRadians),
_scale, _scale)];
}
- (void)setScale:(float)aScale
{
if (_scale == aScale)
return;
_scale = aScale;
[_imageLayer setAffineTransform:CGAffineTransformScale(
CGAffineTransformMakeRotation(_rotationRadians),
_scale, _scale)];
}
- (void)drawInContext:(CGContext)aContext
{
CGContextSetFillColor(aContext, [CPColor grayColor]);
CGContextFillRect(aContext, [self bounds]);
}
- (void)imageDidLoad:(CPImage)anImage
{
[_imageLayer setNeedsDisplay];
}
- (void)drawLayer:(CALayer)aLayer inContext:(CGContext)aContext
{
var bounds = [aLayer bounds];
if ([_image loadStatus] !=
CPImageLoadStatusCompleted)
[_image setDelegate:self];
else
CGContextDrawImage(aContext, bounds, _image);
}
@end
That’s the entire PaneLayer
class and it’s pretty straight forward. I
just put it in PageView.j
for now right before the implementation of
PageView
. You can choose put it in a separate file, but don’t forget
to import it in PageView.j
if you do.
Let’s start by looking at the insance variables. As I said, the pane
layer keeps an image, rotation, and scale, represented here by
_image
, _rotationRadians
, and _scale
. We have two
additional members though: _imageLayer
, which we will use to draw
the image, and _pageView
, which just gives us a reference to our
owning page.
In initWithPageView:
we set up our default values. Clearly we’d like
the image to start off unrotated and unscaled. We also create our
internal _imageLayer
and set ourselves to be the delegate
with
setDelegate:
. Delegates are an important concept in Cappuccino,
because they allow you to modify the behavior of classes without having
to subclass them. Many classes in Cappuccino allow you to supply a
delegate, and then call several methods on that delegate. In this case,
we set ourselves as the delegate of the image layer class because we
want to implement drawLayer:inContext:
- (void)drawLayer:(CALayer)aLayer inContext:(CGContext)aContext
{
var bounds = [aLayer bounds];
if ([_image loadStatus] != CPImageLoadStatusCompleted)
[_image setDelegate:self];
else
CGContextDrawImage(aContext, bounds, _image);
}
This delegate method allows another class to draw within the
_imageLayer
. In this case all we’re doing is drawing our image into
the layer. aContext
is the drawing context of the layer in question,
and we use CGContextDrawImage
to render _image
to the rectangle
bounds
. This just means that we always want the image to fit the
entire width and height of the layer. Notice that we use delegates once
more though: If the image hasn’t completed loading, we set ourselves as
the delegate of the image. Once the image has completed loading, it will
send its delegate the imageDidLoad:
message:
- (void)imageDidLoad:(CPImage)anImage
{
[_imageLayer setNeedsDisplay];
}
So once the image has completed loading, we just tell our
_imageLayer
that it needs to redisplay itself because it has new
information to show. The other drawing function we have is the layer’s
own drawInContext:
, where all we do is draw gray since this will be
the background color of our pane.
The rest of the methods are now prety straight forward since they are
just setters and getters for our existing properties. We supply
the pageView
method to allow one to get the owning page view. We
override setBounds:
to keep the internal _imageLayer
centered,
and we use setImage:
to set the image of the layer:
- (void)setImage:(CPImage)anImage
{
if (_image == anImage)
return;
_image = anImage;
if (_image)
[_imageLayer setBounds:CGRectMake(0.0, 0.0,
[_image size].width, [_image size].height)];
[_imageLayer setNeedsDisplay];
}
We use the information from the image to set the size of the
_imageLayer
, since we want them to match. We also once again inform
our _imageLayer
that it needs to redraw because it now has a new
image.
The last two methods are the most interesting and where the magic
happens. Both setScale:
and setRotationRadians:
use the built in
transformation methods to adjust the _imageLayer
. In both cases we
simply take our two values and create a transform
with them. This is
all that is necessary in Cappuccino to rotate and scale layers.
- (void)setRotationRadians:(float)radians
{
if (_rotationRadians == radians)
return;
_rotationRadians = radians;
[_imageLayer setAffineTransform:CGAffineTransformScale(
CGAffineTransformMakeRotation(_rotationRadians),
_scale, _scale)];
}
- (void)setScale:(float)aScale
{
if (_scale == aScale)
return;
_scale = aScale;
[_imageLayer setAffineTransform:CGAffineTransformScale(
CGAffineTransformMakeRotation(_rotationRadians),
_scale, _scale)];
}
Let’s now go ahead and create a simple pane within our page view:
@implementation PageView : CPView
{
CALayer _rootLayer;
PaneLayer _paneLayer;
}
- (id)initWithFrame:(CGRect)aFrame
{
self = [super initWithFrame:aFrame];
if (self)
{
_rootLayer = [CALayer layer];
[self setWantsLayer:YES];
[self setLayer:_rootLayer];
[_rootLayer setBackgroundColor:
[CPColor whiteColor]];
_paneLayer = [[PaneLayer alloc]
initWithPageView:self];
[_paneLayer setBounds:CGRectMake(0, 0,
400 - 2 * 40, 400. - 2 * 40)];
[_paneLayer setAnchorPoint:CGPointMakeZero()];
[_paneLayer setPosition:CGPointMake(40, 40)];
[_paneLayer setImage:[[CPImage alloc]
initWithContentsOfFile:
@"Resources/sample.jpg"
size:CGSizeMake(500, 430)]];
[_rootLayer addSublayer:_paneLayer];
[_paneLayer setNeedsDisplay];
[_rootLayer setNeedsDisplay];
}
return self;
}
@end
For now we are just going to use a default image for our pane, which you
can download here and put into a
folder called “Resources”. We set the size of the _paneLayer
with
setBounds:
, and the position of the layer with a combination of
setAnchorPoint:
and setPosition:
. This is because unlike CPView
s,
by default the position
of a CALayer
refers to the position of its
center point. To make the position
refer to the top left corner, we
have to specify an achor point of (0.0, 0.0).
We are now almost done with our application. All that is left is to
bring together the photo inspector and the page view. The photo
inspector will essentially “edit” the page views, so let’s add the
following code after [_rootLayer addSublayer:_paneLayer]
in
initWithFrame:
_borderLayer = [CALayer layer];
[_borderLayer setAnchorPoint:CGPointMakeZero()];
[_borderLayer setBounds:[self bounds]];
[_borderLayer setDelegate:self];
[_rootLayer addSublayer:_borderLayer];
Also add _borderLayer
to PageView’s declaration:
@implementation PageView : CPView
{
CALayer _borderLayer;
CALayer _rootLayer;
PaneLayer _paneLayer;
}
The border layer is meant to draw the borders of the panes. Once again
we are making ourselves the delegate of _borderLayer
, so add
drawLayer:inContext:
to PageView
- (void)drawLayer:(CALayer)aLayer inContext:(CGContext)aContext
{
CGContextSetFillColor(aContext, [CPColor whiteColor]);
var bounds = [aLayer bounds],
width = CGRectGetWidth(bounds),
height = CGRectGetHeight(bounds);
CGContextFillRect(aContext, CGRectMake(0, 0, width, 40));
CGContextFillRect(aContext, CGRectMake(0, 40,
40, height - 2 * 40));
CGContextFillRect(aContext, CGRectMake(width - 40, 40,
40, height - 2 * 40));
CGContextFillRect(aContext, CGRectMake(0, height - 40,
width, 40));
}
Here we are just drawing a few white-filled rectangles to be the borders
of our internal pane. We can now add a new method called setEditing:
to our page view:
- (void)setEditing:(BOOL)isEditing
{
[_borderLayer setOpacity:isEditing ? 0.5 : 1.0];
}
What this method does is make the border of our pane semi-transparent when we are editing. That way as you scale and rotate you can still see much of the full image despite it being “inside” the pane.
We will now return to our PhotoInspector
class to make it aware of
these two new classes we’ve created. Start by adding a PaneLayer
to
the PhotoInspector
declaration:
@import <AppKit/CPWindowController.j>
@import "PageView.j"
@implementation PhotoInspector : CPWindowController
{
CPSlider _scaleSlider;
CPSlider _rotationSlider;
PaneLayer _paneLayer;
}
Now let’s add a few methods to make the PhotoInspector a singleton
,
since there’s only ever one used in the application:
var PhotoInspectorSharedInstance = nil;
@implementation PhotoInspector : CPWindowController
{
CPSlider _scaleSlider;
CPSlider _rotationSlider;
PaneLayer _paneLayer;
}
+ (PhotoInspector)sharedPhotoInspector
{
if (!PhotoInspectorSharedInstance)
PhotoInspectorSharedInstance = [[PhotoInspector alloc] init];
return PhotoInspectorSharedInstance;
}
You can now get at the shared PhotoInspector
instance by calling
[PhotoInspector sharedPhotoInspector]
. Notice that this method begins
with a plus instead of a minus. That’s because this is a class
method. Also, we declared a file local variable to store the
shared instance at the top with this line:
var PhotoInspectorSharedInstance = nil;
Unlike JavaScript, varing variables in a file does trap them in that
file’s scope, so this variable will only be accessible through the
sharedPhotoInspector
method.
We will now add the setter for the _paneLayer
member, which
represents the pane and image that the photo inspector is currently
inspecting:
- (void)setPaneLayer:(PaneLayer)anPaneLayer
{
if (_paneLayer == anPaneLayer)
return;
[[_paneLayer pageView] setEditing:NO];
_paneLayer = anPaneLayer;
var page = [_paneLayer pageView];
[page setEditing:YES];
if (_paneLayer)
{
var frame = [page convertRect:[page bounds] toView:nil],
windowSize = [[self window] frame].size;
[[self window] setFrameOrigin:
CGPointMake(CGRectGetMidX(frame) -
windowSize.width / 2.0, CGRectGetMidY(frame))];
}
}
As you can see, when we pass in a new pane layer, we first do a little bit of cleanup on our old pane layer if we have one. We tell its corresponding page view that we are no longer editing, and then tell the new pane layer’s page view that we are editing (thus dimming the border described above). We then position our window at the center of the pane.
Let’s actually make our two sliders useful now by giving them
targets
and actions
in our init
method after they are created:
[rotationSlider setTarget:self];
[rotationSlider setAction:@selector(rotate:)];
[scaleSlider setTarget:self];
[scaleSlider setAction:@selector(scale:)];
Targets and actions are kind of like callbacks, but object-oriented and
much more powerful. Instead of supplying a callback function
, you
supply both a callback object, or target, and a callback method to call
on that object. In this case, when we move the rotationSlider
, we
will have rotate:
called on self
(the PhotoInspector
instance),
and when we move the scaleSlider
, scale:
will be called.
We of course must implement scale:
and rotate:
in PhotoInspector
for this to be useful at all:
- (void)scale:(id)aSender
{
[_paneLayer setScale:[aSender value] / 100.0];
}
- (void)rotate:(id)aSender
{
[_paneLayer setRotationRadians:PI / 180 * [aSender value]];
}
All we do in these two methods is make use of the setters we implemented
in the PaneLayer
class. You might have noticed that our code is
curiously devoid of the null-checks you so often find in JavaScript,
Java, C++, etc. That’s because Objective-J just ignores messages sent to
nil, so instead of having to do:
if (myObject)
[myObject method];
You can safely do:
[myObject method];
We are going to add two more methods to PhotoInspector to complete it:
+ (void)inspectPaneLayer:(PaneLayer)anPaneLayer
{
var inspector = [self sharedPhotoInspector];
[inspector setPaneLayer:anPaneLayer];
[inspector showWindow:self];
}
- (void)windowWillClose:(id)aSender
{
[self setPaneLayer:nil];
}
The first just provides a shorthand for bringing up the inspector and
telling it to inspect a certain PaneLayer
. The second handles the case
when the inspector window is closed: we certainly don’t want to leave
the currently editing pane in editing mode, so we set our editing pane
to be nil
. However, to receive the windowDidCLose:
method, we must
make ourselves the delegate of our window, which we can do in the init
method:
//...
if (self)
{
[theWindow setTitle:@"Inspector"];
[theWindow setFloatingPanel:YES];
[theWindow setDelegate:self];
//...
Our PhotoInspector
is now complete, we just need to bring it up when
we double click on our page view. To do this, let’s return to the
PageView class and add a mouseDown:
method:
- (void)mouseDown:(CPEvent)anEvent
{
if ([anEvent clickCount] == 2)
[PhotoInspector inspectPaneLayer:_paneLayer];
}
There is no need to register for events in Cappuccino as you might do in
JavaScript, simply implement on of the event handling methods such as
mouseDown:
and you will receive the event. Here, we simply check the
click count of the mouse down event and inspect our single pange if it
is a double click.
To finish this application, put the following in your
applicationDidFinishLaunching:
to make a sample PageView
:
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
var theWindow = [[CPWindow alloc]
initWithContentRect:CGRectMakeZero()
styleMask:CPBorderlessBridgeWindowMask],
contentView = [theWindow contentView];
[contentView setBackgroundColor:[CPColor blackColor]];
[theWindow orderFront:self];
var bounds = [contentView bounds],
pageView = [[PageView alloc] initWithFrame:
CGRectMake(CGRectGetWidth(bounds) / 2
- 200, CGRectGetHeight(bounds) / 2 - 200,
400, 400)];
[pageView setAutoresizingMask: CPViewMinXMargin |
CPViewMaxXMargin |
CPViewMinYMargin |
CPViewMaxYMargin];
[contentView addSubview:pageView];
var label = [[CPTextField alloc] initWithFrame:CGRectMakeZero()];
[label setTextColor:[CPColor whiteColor]];
[label setStringValue:@"Double Click to Edit Photo"];
[label sizeToFit];
[label setFrameOrigin:CGPointMake(CGRectGetWidth(bounds) / 2 -
CGRectGetWidth([label frame]) / 2,
CGRectGetMinY([pageView frame]) -
CGRectGetHeight([label frame]))];
[label setAutoresizingMask: CPViewMinXMargin |
CPViewMaxXMargin |
CPViewMinYMargin |
CPViewMaxYMargin];
[contentView addSubview:label];
}
Your AppController.j
should now look something like this:
@import <Foundation/CPObject.j>
@import "PageView.j"
@import "PhotoInspector.j"
@implementation AppController : CPObject
{
}
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
var theWindow = [[CPWindow alloc]
initWithContentRect:CGRectMakeZero()
styleMask:CPBorderlessBridgeWindowMask],
contentView = [theWindow contentView];
[contentView setBackgroundColor:[CPColor blackColor]];
[theWindow orderFront:self];
var bounds = [contentView bounds],
pageView = [[PageView alloc] initWithFrame:
CGRectMake(CGRectGetWidth(bounds) / 2
- 200, CGRectGetHeight(bounds) / 2 - 200,
400, 400)];
[pageView setAutoresizingMask: CPViewMinXMargin |
CPViewMaxXMargin |
CPViewMinYMargin |
CPViewMaxYMargin];
[contentView addSubview:pageView];
var label = [[CPTextField alloc] initWithFrame:CGRectMakeZero()];
[label setTextColor:[CPColor whiteColor]];
[label setStringValue:@"Double Click to Edit Photo"];
[label sizeToFit];
[label setFrameOrigin:CGPointMake(CGRectGetWidth(bounds) / 2 -
CGRectGetWidth([label frame]) / 2,
CGRectGetMinY([pageView frame]) -
CGRectGetHeight([label frame]))];
[label setAutoresizingMask: CPViewMinXMargin |
CPViewMaxXMargin |
CPViewMinYMargin |
CPViewMaxYMargin];
[contentView addSubview:label];
}
@end
This concludes our first tutorial. We’ve gotten familiar with some of the drawing and event constructs of Cappuccino and have put together a pretty simple prototype of our Scrapbook application. Next time, we’ll be going deeper into events and drag and drop to start making our application more useful.
Something you may have noticed is that we never once spoke about browser inconsistencies, nor do we have any conditional code based on what browser the user is on. Since all the constructs we use are from Cappuccino, we shift all the heavy lifting of these sorts of tasks to the framework, and can instead focus on the key features of our application.