Stone Design Stone Design
News Download Buy Software About Stone
software

OS X & Cocoa Writings by Andrew C. Stone     ©1995-2003 Andrew C. Stone

Getting The Job Done - an agnostic approach to Mac OS X programming
©2002 Andrew C. Stone All Rights Reserved.

Do you ever wonder how I come up with OS X programming topics month after month? The answer is simple - Fate! By that, I mean that constantly working on software will always bring up interesting problems that require creative solutions, and if they merit it, I write an article on the subject. After Mac OS X 10.2.4 was released, one of our users noted that PStill could be added as an option to the PDF workflow options of every Mac OS X application's print panel. This lets our users print directly to PStill to either shrink the PDF or repurpose it, for example, by downsampling images for web deployment. What a cool feature!

Apple describes the feature at: http://developer.apple.com/techpubs/macosx/CoreTechnologies/graphics/Printing/PDF_Workflow/pdfwf_concepts/index.html - but all the user needs to do is make a folder named "PDF Services" in their home library folder, and then make an alias to PStill. They may also want to rename the alias, because that's the name it appears under in the Print Panel:

So, a user could make the folder, create a PStill alias, drag it into the folder, and rename it, but wouldn't it be much cooler if PStill noticed that they hadn't done this, and offer to do this for them?

I thought, great, a few lines of Cocoa and I'm done:

NSFileManager *fm = [NSFileManager defaultManager];
NSString *path = [[NSHomeDirectory() stringByAppendingPathComponent:
@"Library"] stringByAppendingPathComponent:@"PDF Services"];
if (![fm fileExistsAtPath:path]) {
int returnValue = NSRunAlertPanel(PDF_WORKFLOW_TITLE, PDF_WORKFLOW_MSG,OK,CANCEL,NULL,NULL);
if (returnValue == NSAlertDefaultReturn) {
if (![fm fileExistsAtPath:path]) [fm createDirectoryAtPath:path attributes:nil];
[fm createSymbolicLinkAtPath:[path stringByAppendingPathComponent:@"Repurpose with PStill"] pathContent:[[[[NSBundle mainBundle] resourcePath] stringByDeletingLastPathComponent] stringByDeletingLastPathComponent]];
}
}

This is when we enter the dark night of the soul! It turns out that a symbolic link so familiar to unix weenies as "ln -s" is in no way equivalent to the old carbon notion of an "alias":

ln -s PStill.app "PStill Symbolic Link"
In Finder, select PStill and choose "Make Alias" and then do an ls -l:

lrwxrwxrwx 1 andrew admin 10 Feb 21 10:51 PStill Symbolic Link@ -> PStill.app
-rw-r--r-- 1 andrew admin 0 Feb 21 10:51 PStill alias

Finder says the "PStill alias" is 68K! And since the terminal says "0", that means the alias magic is inside a resource fork. So, reviewing Apple's docs, we see:

The type of item placed in one of these directories (/Library/PDF Services , ~/Library/PDF Services or /Network/Library/PDF) determines what the item does when the user selects it from the PDF workflow pop-up menu. For example, if the item is a location (such as a folder), then the PDF is copied to that location. The name of the item appears as an option in the PDF workflow pop-up menu, so it is important that the item is named appropriately.

Users can place the following items in any of the three directories mentioned previously:
a folder or an alias to a folder

an application or an alias to an application

a UNIX tool or an alias to a UNIX tool

an AppleScript file or an alias to an AppleScript file


There is currently no way using Cocoa to create one of these alias beasts - and, alas, although I have 15 years programming in Cocoa, I'm a relative neophyte to Carbon and frankly, FSSpecs and what not give me a headache! Searching through the carbon headers, I found in Aliases.h:

extern OSErr
NewAlias(
const FSSpec * fromFile,
const FSSpec * target,
AliasHandle * alias)

And very little documentation on how to use it. An hour of trying to get it to fly never did. So, I asked a friend who threw up his digital hands and said, "I have no idea. Why not use AppleScript?". The light bulb turned on brightly at that moment (maybe I should have said the the dark hour of the soul). Of course, we should use the new NSAppleScript object to ask Finder to do it! NSAppleScript can be initialized with a source script, compile and execute it in 3 lines of code.What's more, we can debug the script in Script Editor before trying to roll in the implementation. A script like this works great:

tell application "Finder"
    
set f to "Applications:Stone Studio:PStill.app"
    
set al to make new alias file at "Users:andrew:Library:PDF Services" to f
    
set the name of al to "Repurpose with PStill"
end tell


So, our only programming challenge left is to write a simple method to convert Posix-style paths (what we use in Cocoa) to the old-fashioned colon delimited style used by Finder. And we find CoreFoundation's CFURL has just what we need to wrap this up in an easy to use Objective C method:

- (NSString *)hfsStylePathFromPosixPath:(NSString *)s isDirectory:(BOOL)isDirectory {
    OSErr myErr;
CFStringRef hfsStyle;
    CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
s,
kCFURLPOSIXPathStyle,
                     isDirectory?
TRUE : FALSE /*isDirectory*/);

if (url == NULL) {
printf(
"Can't get URL.\n");
return(nil);
}

if (hfsStyle = CFURLCopyFileSystemPath(url, kCFURLHFSPathStyle)) {
CFRelease(url);
return [hfsStyle autorelease];
}
CFRelease(url);
return nil;
}

Our final implementation should provide a bit of extra functionality and error checking! First, one should always include a way for the user not to be bothered in case they are not interested in this feature - we'll simply include a button on the alert panel that writes an NSUserDefault, and check this before calling the method again the next time they launch the program.

Second, we remember that NSAppleScript is not part of Mac OS X 10.1, so in order for our application to be compiled in Jaguar but run on 10.1, we must not make any reference to non-existing symbols on the older platform. So we use NSClassFromString() to prevent the runtime system from gagging on the symbol "NSAppleScript":

#define ALIAS_NAME    NSLocalizedStringFromTable(@"Repurpose with PStill",@"PStill",@"name of an alias to PStill for pdf workflow")
#define FINDER_STRING    @"tell application \"Finder\"\nset f to \"%@\"\nset al to make new alias file at \"%@\" to f\nset the name of al to \"%@\"\nend tell"

- (void)createAliasFrom:(NSString *)source to:(NSString *)targetPath {
// NSAppleScript is NOT part of MAC OS X 10.1.x! Watch this trick!

if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_1) {
/* On a 10.1 - 10.1.x system where there is no PDF workflow yet... */
}
else {

NSString *command = [NSString stringWithFormat:FINDER_STRING,[
self hfsStylePathFromPosixPath:source isDirectory:YES],[self hfsStylePathFromPosixPath:targetPath isDirectory:YES],ALIAS_NAME];
NSDictionary *errorInfo;
NSAppleScript *as = [[[NSClassFromString(
@"NSAppleScript") alloc] initWithSource:command] autorelease];
NSAppleEventDescriptor *aedesc = [as executeAndReturnError:&errorInfo];

if (aedesc == nil) NSLog([errorInfo description]);
}
}

#define DONT_WARN NSLocalizedStringFromTable(@"Don't show me this again", @"PStill", "title of cancel and don't show warning again button.")
#define PDF_WORKFLOW_TITLE NSLocalizedStringFromTable(@"PDF Workflow",@"PStill",@"title of PDF workflow alert")
#define PDF_WORKFLOW_MSG NSLocalizedStringFromTable(@"PStill can appear as a PDF workflow option in every OS X app's Print Panel - would you like that? A new directory in your Library folder 'PDF Services' will be created with an alias to PStill. You can drag Watched Folders into 'PDF Services' for additional PDF workflow targets.",@"PStill",@"message for print panel")


- (void)checkForWorkflow {
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
if (![ud boolForKey:@"DontAskAboutWorkFlow"]) {
NSFileManager *fm = [NSFileManager defaultManager];
NSString *path = [[NSHomeDirectory() stringByAppendingPathComponent:
@"Library"] stringByAppendingPathComponent:@"PDF Services"];
if (![fm fileExistsAtPath:path] || ![fm fileExistsAtPath:[path stringByAppendingPathComponent:ALIAS_NAME]]) {
int returnValue = NSRunAlertPanel(PDF_WORKFLOW_TITLE, PDF_WORKFLOW_MSG,OK,CANCEL,DONT_WARN,NULL);
if (returnValue == NSAlertDefaultReturn) {
if (![fm fileExistsAtPath:path]) [fm createWritableDirectory:path];
// make a symlink or aliaas:
[
self createAliasFrom:[[[[NSBundle mainBundle] resourcePath] stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] to:path];
}
else if (returnValue == NSAlertOtherReturn)
[[NSUserDefaults standardUserDefaults] setObject:
@"YES" forKey:@"DontAskAboutWorkFlow"];
}
}
}

Conclusion

Sure I love Cocoa and I try to have 100% Cocoa applications. However if there's a feature that only has a Carbon API, I'm happy to forego my religious beliefs in order to get the job done. With the new NSAppleScript object, one can avoid Carbon altogether and let the tight AppleScript integration do the job for us.

Andrew Stone, CEO of Stone Design, http://stone.com, has virtual bumper stickers which read "I'd Rather Be Coding" and "My Other Car is a Computer".

PreviousTopIndexNext
©1997-2005 Stone Design top