The Perl Journal June 2003
With millions of Mac OS X (and they do insist you pronounce the X as "ten") installations around the world, it finally is possible to have both the stability, performance, and configurability of UNIX and the ease of use, good looks, and multimedia capability of the Mac.
One of OS X's most intriguing features is its elaborate developer framework. One can write OS X applications using the Cocoa, Carbon, Java, or Classic frameworks. The Classic framework allows older pre-OS X programs (i.e., Mac OS 9) to continue to run under OS X.
Carbon provides more traditional methods to access Mac OS X features. The Carbon APIs can be used to write Mac OS X applications that also run on previous versions of the Mac OS (8.1 or later).
The Cocoa application environment is designed specifically for Mac OS X-only native applications. It is comprised of a set of object-oriented frameworks that support rapid development and are designed to promote high productivity. The Cocoa frameworks include a full-featured set of classes designed to create robust and powerful Mac OS X applications. The object-oriented design simplifies application development and debugging.
Cocoa provides developers starting new Mac OS X-only projects the fastest way to full-featured implementations. Applications from UNIX and other OS platforms can also be brought to Mac OS X quickly by using Cocoa to build Aqua user interfaces while retaining most existing core code.
One can use Apple's standard Project Builder integrated development environment (IDE) to create, edit, compile, and debug Cocoa applications. One of the main attractions of Cocoa is its tight integration with the Aqua user interface. Aqua is a class framework/API that allows you to write programs inheriting the full power of the OS X GUI, including its expressive icons, vibrant color, and fluid motion. Aqua includes a number of innovative time- and work-saving features that help you navigate and organize your system. Therefore, it is possible to write a simple program and let it inherit the looks and ease of use of OS X.
In Aqua, title bars of windows are no longer the old and boring Mac OS platinum gray, but a subtle blending of translucent stripes. Instead of a border around a window, a drop shadow adds depth to your perception. This shadow grows larger for the window in the foreground, adding a visual cue to how many windows you have open. As you move windows around, the contents move also, instead of just the outline. Aqua also provides basic functionality such as file opening and saving from within your application. When one of these save dialogs is open, a popup menu offers access to your favorite and most recently used folders. To save the document somewhere else, simply disclose the full filesystem browser. It also offers spring-loaded folders that snap open when you drag things into them in the Finder view.
The sample Objective C program in Listing 1 shows how easy it is to use OS X features. I have been using OS X for half a year now and one of the first questions I had was how to integrate Cocoa with the ease of use and power of Perl.
A fast search on the Internet revealed that this integration had already been achieved with an open-source integration tool called "CamelBones" (see http://camelbones.sourceforge.net/). CamelBones is a framework that allows many types of Cocoa programs to be written entirely in Perl. It also provides a high-level object-oriented wrapper around an embedded Perl interpreter, so that Cocoa programs written in Objective-C can easily make use of code and libraries written in Perl.
An example of a currency converter using the Aqua interface and the Cocoa framework shows how easy it is to tightly integrate Perl in OS X. In Figure 1, you can see the seamless integration of Perl code and Cocoa in the IDE tool.
Listing 2 shows how to call Cocoa methods from within Perl using the CamelBones framework. Notice how Perl is used to interact with the user on the screen:
# Outlets
$self->{'RateField'} = undef;
$self->{'DollarField'} = undef;
$self->{'TotalField'} = undef;
$self->{'Window'} = undef;
bless ($self, $class);
$self->{'NSWindowController'} =
NSWindowController->alloc>initWithWindowNibName_owner(
"ConverterWindow", $self);
$self->{'NSWindowController'}->window;
The Window, TotalField, DollarField, and RateField widgets need to be created in the OS X Interface Builder (which is included with the Developer Tools package along with Project Builder) and are an instance of the NSTextField class. NSTextField, like most GUI widget classes, is a subclass of NSControl, and inherits the setStringValue method from that class. Instance methods are called for GUI widgets by treating the outlets connected to them as object references. So, the sample sayHello method should look like this:
sub sayHello {
my ($self, $sender) = @_;
$self->{'TextLabel'}->setStringValue("Hello");
}
If you don't know what $self is, this would be a great time to read Tom Christiansen's excellent OO tutorial, found in the perltoot POD document.
Any interactive application (like the currency converter used here) needs to handle events, such as buttons pressed. With CamelBones, you can (and should in fact) register event handlers, too.
There are some rough edges in CamelBones. The integration with the GUI still needs to be done through Objective-C. That's because the GUI resource is "owned" by an Objective-C class, rather than one that's defined in Perl. Handling menu selections is a bit different than handling actions sent by other GUI widgets.
To handle a menu selection action, you connect the action to the "First Responder" in Project Builder, instead of the "File's Owner." This will allow you to register your own Perl event handler.
In Project Builder, the Perl method to handle the menu action should be added to the MyApp class, instead of the MyWindowController class.
Listing 3 shows the sample event controller for the currency converter.
Though it still has a few rough edges, the CamelBones framework is a good start. It allows a relatively easy integration of Perl and Cocoa, which should get you started in connecting your Perl code to the OS X Aqua interface.
TPJ
#include <Carbon/Carbon.h>
#include "CocoaBundle.h"
enum
{
kOpenCocoaWindow = 'COCO'
};
CFBundleRef bundleRef = NULL;
static OSStatus
handleBundleCommand(int commandID) {
OSStatus osStatus = noErr;
if (commandID == kEventButtonPressed) {
OSStatus (*funcPtr)(CFStringRef message);
funcPtr = CFBundleGetFunctionPointerForName(bundleRef,
CFSTR("changeText"));
require(funcPtr, CantFindFunction);
osStatus = (*funcPtr)(CFSTR("button pressed!"));
require_noerr(osStatus, CantCallFunction);
}
CantFindFunction:
CantCallFunction:
return osStatus;
}
static void
myLoadPrivateFrameworkBundle(CFStringRef framework, CFBundleRef *bundlePtr)
{
CFBundleRef appBundle = NULL;
CFURLRef baseURL = NULL;
CFURLRef bundleURL = NULL;
appBundle = CFBundleGetMainBundle();
require(appBundle, CantFindMainBundle);
baseURL = CFBundleCopyPrivateFrameworksURL(appBundle);
require(baseURL, CantCopyURL);
bundleURL = CFURLCreateCopyAppendingPathComponent(kCFAllocatorSystemDefault, baseURL,
CFSTR("CocoaBundle.bundle"), false);
require(bundleURL, CantCreateBundleURL);
bundleRef = CFBundleCreate(NULL, bundleURL);
CFRelease(bundleURL);
CantCreateBundleURL:
CFRelease(baseURL);
CantCopyURL:
CantFindMainBundle:
return;
}
static OSStatus
appCommandHandler(EventHandlerCallRef inCallRef, EventRef inEvent, void* userData) {
HICommand command;
OSStatus (*funcPtr)(void *);
OSStatus (*showPtr)(void);
OSStatus err = eventNotHandledErr;
if (GetEventKind(inEvent) == kEventCommandProcess) {
GetEventParameter( inEvent, kEventParamDirectObject, typeHICommand,
NULL, sizeof(HICommand), NULL, &command );
switch ( command.commandID ) {
case kOpenCocoaWindow:
myLoadPrivateFrameworkBundle(CFSTR("CocoaBundle.bundle"),
&bundleRef);
require(bundleRef, CantCreateBundle);
// call function to initialize bundle
funcPtr = CFBundleGetFunctionPointerForName(bundleRef,
CFSTR("initializeBundle"));
require(funcPtr, CantFindFunction);
err = (*funcPtr)(handleBundleCommand);
require_noerr(err, CantInitializeBundle);
// call function to show window
showPtr = CFBundleGetFunctionPointerForName(bundleRef,
CFSTR("orderWindowFront"));
require(showPtr, CantFindFunction);
err = (*showPtr)();
require_noerr(err, CantCallFunction);
CantCreateBundle:
CantCallFunction:
CantInitializeBundle:
CantFindFunction:
break;
default:
break;
}
}
return err;
}
int main(int argc, char* argv[])
{
IBNibRef nibRef;
WindowRef window;
OSStatus err;
EventTypeSpec cmdEvent = {kEventClassCommand, kEventCommandProcess};
// Create a Nib reference passing the name of the nib file
// (without the .nib extension)
// CreateNibReference only searches into the application bundle.
err = CreateNibReference(CFSTR("main"), &nibRef);
require_noerr( err, CantGetNibRef );
// Once the nib reference is created, set the menu bar.
// "MainMenu" is the name of the menu bar
// object. This name is set in InterfaceBuilder when the nib is
// created.
err = SetMenuBarFromNib(nibRef, CFSTR("MenuBar"));
require_noerr( err, CantSetMenuBar );
// Then create a window. "MainWindow" is the name of the window
// object. This name is set in
// InterfaceBuilder when the nib is created.
err = CreateWindowFromNib(nibRef, CFSTR("MainWindow"), &window);
require_noerr( err, CantCreateWindow );
// We don't need the nib reference anymore.
DisposeNibReference(nibRef);
// The window was created hidden so show it.
ShowWindow( window );
InstallApplicationEventHandler(NewEventHandlerUPP(appCommandHandler), 1,
&cmdEvent, 0, NULL);
// Call the event loop
RunApplicationEventLoop();
CantCreateWindow:
CantSetMenuBar:
CantGetNibRef:
return err;
}
package WindowController;
use Foundation;
use Foundation::Functions;
use AppKit;
use AppKit::Functions;
@
ISA = qw(Exporter);
sub new {
# Typical Perl constructor
# See 'perltoot' for details
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = {};
# Outlets
$self->{'RateField'} = undef;
$self->{'DollarField'} = undef;
$self->{'TotalField'} = undef;
$self->{'Window'} = undef;
bless ($self, $class);
$self->{'NSWindowController'} =
NSWindowController->alloc->initWithWindowNibName_owner(
"ConverterWindow", $self);
$self->{'NSWindowController'}->window;
return $self;
}
sub convertButtonClicked {
my $self = shift;
my $rate = $self->{'RateField'}->floatValue;
my $amount = $self->{'DollarField'}->floatValue;
my $total = $rate * $amount;
$self->{'TotalField'}->setFloatValue($total);
return 1;
}
1;
package MyController;
use Foundation;
use Foundation::Functions;
use AppKit;
use AppKit::Functions;
use WindowController;
@
ISA = qw(Exporter);
our $wc = undef;
sub new {
# Typical Perl constructor
# See 'perltoot' for details
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = {
};
bless ($self, $class);
return $self;
}
sub applicationWillFinishLaunching {
my ($self, $notification) = @_;
# Create the new controller object
$wc = new WindowController;
return 1;
}
1;