Rhapsody In Purple
Volume Number: 14
Issue Number: 3
Column Tag: Rhapsody
Rhapsody in Purple
by Karl Kraft
An overview of the several ways to patch applications in
Rhapsody, with various levels of bravery
Rhapsody may have an OS 8 look and feel, but under the hood it is running a completely
different engine. While this new engine may sweep away some of our most beloved Mac
items, like loadable "system" code in the form of INITs and CDEVs, their Rhapsody
equivalents are by no means non-existent, or even difficult to implement. In fact
modifications to the system and to applications under Rhapsody can be done in a clean
and safe manner using high level constructs.
This article seeks to explain how a Rhapsody application comes to have a particular
look and feel, and ultimately how to modify the look and feel of not only your
applications, but all applications on the system. The overall effect of the source in the
article is to change the background color of windows to an awful shade of purple.
How Programs Launch in Rhapsody
When a user thinks of an application, he usually thinks of a single software package,
such as "ClarisWorks", which is actually a collection of templates, fonts, applications,
help files, and other miscellaneous items.
When most developers thinks of an application, the target is more specific, usually a
single binary image that contains executable code. Under Mac OS, this executable would
also have a resource fork with various resources bound to the application.
Under Rhapsody, an application consists of many more parts, each of which is either
executed or interpreted by the system as a whole. The most basic parts are:
1. The executable file, which includes loading instructions for individual
processor families, and basic information such as the application's icon. Under
Rhapsody, this comes in a format called "mach-o".
2. Resources in the form of images, property-lists, string files, and others.
3. NeXT Interface Builder or "nib" files, that define the layout and
connection of objects in the interface.
4. Various frameworks and dynamic libraries supplied by Apple Computer,
such as AppKit, Foundation and System.
5. The Window Server, a separate program that handles all the drawing and
PostScript imaging.
6. The many utility and server programs that run on the machine such as
AKServer (short for AppKit Server), a program that helps handle the
pasteboard.
7. The mach kernel and underlying operating system.
Together these pieces offer you, the Macintosh hacker, dozens of places where a patch
can be applied both for your projects, and applications delivered in a compiled form.
Simple Non-Code Patches
Even if you aren't a programmer, you shouldn't have much trouble figuring out how to
use interface builder to modify an application. This can be useful when a sysadmin type
desires to remove functionality from an application, or perform interface butchery.
Using Viewer, the Rhapsody equivalent to Finder, select an application such as
Terminal.app. For this example, pretend that you have have been ordered to prevent
users from changing their preferences in this application. Note: Defaults can be
changed in ways besides the preferences panel, but the type of people who would try
this stunt would seldom think out the total impact of their interface butchery.
Start by making a copy of Terminal.app, and storing it in a safe place. You might need
it to recover from your actions.
Terminal.app is actually a folder that contains the various resources and executable
pieces of the Terminal application. Through the magic of Viewer, folders with the .app
extension appear as single files. You can invoke stronger magic by selecting
Terminal.app, and then picking "Open As Folder" from Viewer's File menu. This should
reveal a folder with three items, a file called Terminal, which is the actual executable
image, a folder with a header file for communicating with Terminal, and a folder called
Resources. The Resources folder contains all the resources for the application, such as
images, nib files, string files, and property lists.
Descending into the Resources folder, you should find a folder titled English.lproj. The
lproj extension indicates a language project, which contains all the localizable items
for this application. This is often just about every resource in the application. Even
images are localizable, because the concept they represent can be conveyed differently
in each language. An image of a dollar sign might be localized to a Yen symbol for the
Japanese language project.
Inside English.lproj you strike paydirt, a nib file that is cleverly named Terminal.nib.
You can open this nib file in interface builder by double clicking on the nib file icon.
Note: Terminal.app and all the resources grouped with it are not modifiable by a
regular user. You need to be either logged in as root, or working on a copy of
Terminal.app that belongs to you.
Using Interface Builder, you can navigate through the Terminal menu structure until
you find the Preferences menu item. Since nib files contain not only interface objects
but also their connections, you can discover what method is called when this menu item
is clicked. Using the Inspector, and selecting connections, you should discover that it is
connected to the File's Owner object, and calls the method preferences:.
Simply picking disconnect from the connections inspector will render this item
impotent, leaving a boring menu item that does nothing. Perhaps a dash of purple will
help spice up things . Using Interface Builder, you can add a panel to the application,
with some nice purple text, that says "This item is disabled". You can see a sample of
my panel in Figure 1.
Figure 1.
Hold down the control key, and drag a line from the Preferences menu item to the title
bar of the newly created panel, and the connection inspector will appear. Set the
connection to makeKeyAndOrderFront:.
Now save all your mischief and give it a try. If you did it right, picking Preferences
should bring up your new preferences panel, and frighten the user from every picking
an un-authorized menu item again.
You can edit just about any resource in this manner. Images, property lists, and other
resources can be modified by simply using the appropriate editor. You can also use this
technique to localize an application that has not been localized to a desired language by
the developer. Simply copy English.lproj to Esperanto.lproj, and edit away. The
changes you make in Esperanto.lproj will be seen by any user whose default language
choice is set to Esperanto before English.
Color Me Purple All Over
What if you desire a more drastic change? What if you want all the windows in your
application to have a purple background? One option would be to carefully check every
time a window is created, and set its background color to purple.
In a small application that might work. A few windows means a few lines of code.
However, I know of several OPENSTEP applications that have on the order of several
hundred nib files, and hundreds of windows that can be created on the fly. Such a
manual technique would be troublesome, because you would eventually miss a window,
and its gray background would just look atrocious compared to all the glorious purple
windows spread across the screen.
There is a better way, and in this example, you will convert TextEdit to produce
purple panels and windows. While you read this next section, grab a copy of the source
to TextEdit from /NextDeveloper/Examples/AppKit/ from the Rhapsody Developer
Release, and start the project building.
One of the great advantages of Objective-C is the ability to completely override a class
by a technique known as posing. In posing, a class (ie: PurpleWindow) assumes the
identity of its superclass, such as NSWindow. In Figure 2 a simple class tree shows
where NSPanel and PurpleWindow are both subclasses of NSWindow. After
PurpleWindow begins posing as NSWindow, the class tree is changed to that of Figure
3.
Figure 2.
Figure 3.
By using this technique, you can not only subclass an existing class, but make the
implementation of your class the default implementation for the super class and all it's
subclasses. This saves you from the trouble and difficulty of having to write a separate
subclass for NSWindow, NSPanel, and other windows that you may not even know exist.
To see this in action, use ProjectBuilder to create a new class called PurpleWindow.
ProjectBuilder will create two files for you, an .m file (methods) and an .h file
(headers). A quick look at the header shows that ProjectBuilder thinks PurpleWindow
is a subclass of NSObject rather than NSWindow. Edit the header file to read:
implementation PurpleWindow:NSWindow
@end
Now the question is, how do you get PurpleWindows to have a purple background? If
you think that the compiler should just figure it out from the prefix of "Purple", you
are going to be sorely disappointed. You need to pick some method of NSWindow,
override it, and then call a method to set the purple color. For starters, write the
method that sets the background color to purple.
-(void)becomePurple;
[self setBackgroundColor:[NSColor purpleColor]];
}
Now all that is left is to call becomePurple sometime early in the creation of a window,
before it is displayed. A particularly tempting target is makeKeyAndOrderFront:. This
method is called to put a window on screen, and make it the key window (the window
which accepts keyboard events). So you should dutifully write:
-(void) makeKeyAndOrderFront:sender;
[self becomePurple];
[super makeKeyAndOrderFront:sender];
}
Now whenever a PurpleWindow receives the makeKeyAndOrderFront: message it
should change its background color to purple, and then have NSWindow actually put the
window on screen.
Note: makeKeyAndOrderFront: probably isn't the best place to actually make this patch.
I picked it as an example because it is well known, quick and easy for developers to
understand.
If you typed everything correctly, you should be able to compile your new modified
TextEdit, and give it a test. The change should have absolutely no effect at this point.
Simply creating a subclass of NSWindow does not make it automatically pose as
NSWindow, nor does it cause all windows to become PurpleWindows. At this point, the
only way you will get a PurpleWindow, is if you create one in InterfaceBuilder, or
programmitcally.
To start the actual posing takes a single line:
[PurpleWindow poseAsClass:[NSWindow class]];
Your decision at this point is where to insert this line in your project. After you start
the posing, all calls to NSWindow methods will instead be sent to PurpleWindow.
Because of this, the ideal circumstance is to perform the posing before any NSWindow
objects are created. This will guarantee that all NSWindow objects in your application
are actually PurpleWindow objects.
The earliest point in the execution of your application would be the function main(),
found in the file Edit_main.m. By making this line the first line in main(), it will take
place before any objects are created. Inserting this early in main() produces:
#import "PurpleWindow.h
int main(int argc, const char *argv[]) {
[PurpleWindow poseAsClass:[NSWindow class]];
return NSApplicationMain(argc, argv);
}
Build the project again, run and test. You should find that document windows have no
noticeable change. At first this may disappoint you, but the reason is that the white
background of the document window comes from the NSText object that completely
covers the background. You will need to look at windows with some background visible,
such as the Info panel, the Preferences panel, and the Find panel. These should look
like Figure 4.
Figure 4.
Posing can be a valuable tool, and a dangerous weapon. Careless and unnecessary use
can make programs difficult to debug and create strange effects. If all you need is some
purple windows for your application, you would probably be better off with just using
a subclass. Reserve the power of posing for patching class that you lack source for,
such as the Appkit, and always keep the posing down to a minimum.
Posing in a Friendly Executable
If you can pose in your application, can you pose in an application where the source is
not available? To pose, you need:
1. Your class to be linked into the target application.
2. A call relatively early on, where you can engage the actual posing action.
Some applications will make this task easy for you. These "friendly" applications
support the loading and linking of dynamic code in the form of bundles. Examples
include ProjectBuilder, InterfaceBuilder, Viewer, and Mail. All of these applications
load bundles in one form or another.
For the next example, the goal is to make windows in the ProjectBuilder application
use the same PurpleWindow as we used in TextEdit, but this time I'll assume you don't
have the complete source to ProjectBuilder or Yellow Box.
The application ProjectBuilder, supports the ability to load Bundles of code and
resources than can receive a notification when a project is opened, closed, an other
various useful things.
A bundle is much like an INIT in that it offers a section of executable code and
resources like images, all packaged as a single unit. The biggest difference between
these two is that an INIT contains code to patch the Macintosh System on startup, and a
bundle contains loadable code for a single application. Another difference is that the
loading of INITs happens automatically, provided that they are in the proper place.
Bundles are usually only loaded by applications that explicitly look for them.
Bundles are created using the same tools, and a process very similar to building
applications. You start by creating a new Project in Project Builder, but pick the type
Bundle instead of Application. You can then add and edit classes to your bundle, build,
and test.
For the building of ProjectBuilder bundles, the task is made a little easier by a
preference template that can be found at
http://www.ensuing.com/~karl/RhapHack/RhapBundle.html. This template is a
ProjectBuilder bundle ready for use in ProjectBuilder, with a default interface and
class that only needs a small portion of work to be useable. To start, copy the template
project to some place convenient, like your home directory, and then open it in
ProjectBuilder. This type of project does not have nay interface, so all the work is
done in ProjectBuilder.
You now need to edit the basic class. As you can see there are several methods already
created for the template that perform the basic task of grabbing information from the
ProjectBuilder application and setting up the notification center to call methods on
certain events. One of these methods is called setup:. According to the documentation,
this method is called when the PBBundleHost first loads the bundle. This looks like a
good place to start your PurpleWindow posing as NSWindow. Insert this simple code
snippet in the method builderStarted:
+(void)builderStarted;