2005-05-14

Mac OS 10.4 slideshows from Java

You may have seen the slideshows provided by iPhoto, and now the Finder, and Mail. They're pretty nice. But how do they work, and how can you start one from a Java program?

To find out, I selected some images in the Finder, started a profile, and then started a slideshow. This line stood out:

203 -[Slideshow runSlideshowWithDataSource:options:]

Admittedly, I could probably have guessed that name, but it's always good to be sure. locate(1) came up with a private framework called Slideshow, and nm(1) on the shared library within showed that we were looking in the right place.

Once you know that, class-dump is broadly equivalent to Java's javap(1), and will make you a .h file for the private framework (part of being private is that you don't get the header files).

Slideshow follows the usual sharedSlideshow pattern, so there's no need to worry about how to create a suitable instance. There are two 'run' methods, runSlideshowWithDataSource:options: and runSlideshowWithPDF:options:. The latter is trivially easy to use. Just give it an NSURL pointing to a PDF file, and you're away. (You can usually recognize when something's expecting an NSURL if you give it an NSString and it complains that there's no scheme method.)

But I wanted a slideshow of pictures, not the pages in a PDF file.

If you're a Java programmer, you probably think of an interface as a static thing. So you'd expect to be able to find statically the methods your "data source" is supposed to implement. In Objective-C, this isn't necessarily the case. A common pattern is for the caller to invoke respondsToSelector: to see what you're capable of, and only invoke other methods if it knows you have them. So if you pass a new NSObject instance as your data source, for example, you won't get any errors, but you won't get a slideshow either.

What you want is a method like this, that prints the name of everything it's asked for:

- (BOOL)respondsToSelector:(SEL)aSelector {
NSLog(@"respondsToSelector: %@", NSStringFromSelector(aSelector));
return [super respondsToSelector: aSelector];
}

If you provide such an object to Slideshow, you see you're asked about the following selectors:

2005-05-14 11:46:49.533 slideshow[15369] respondsToSelector: numberOfObjectsInSlideshow
2005-05-14 11:46:49.533 slideshow[15369] respondsToSelector: slideshowObjectAtIndex:
2005-05-14 11:46:49.533 slideshow[15369] respondsToSelector: canExportObjectAtIndexToiPhoto:
2005-05-14 11:46:49.533 slideshow[15369] respondsToSelector: exportObjectsToiPhoto:

So numberOfObjectsInSlideshow and slideshowObjectAtIndex: looks like the minimum we have to implement.

If you're wondering about the "options", by the way, they're an NSDictionary. The keys (determined using the same respondsToSelector: trick as above only with objectForKey: instead) appear to be:

  • startIndex
  • debug
  • MAGIC_NUMBER
  • autoPlayDelay


To get things started, I gave NSApplication my own delegate, and made applicationDidFinishLaunching: start the slideshow. The slideshow is asynchronous, so I also had to implement applicationShouldTerminateAfterLastWindowClosed: so we can quit when it's finished.

As usual, the latest version will be in the salma-hayek library, linked to from my home page. But here's a snapshot in time:

/*
slideshow - a command-line interface to Mac OS' slideshow functionality.
Copyright (C) 2005 Elliott Hughes

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

*/
#include <Cocoa/Cocoa.h>

// Generated by class-dump 3.0.
// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004 by Steve Nygard.
@interface Slideshow : NSResponder {
id mPrivateData;
}

+ (id)sharedSlideshow;
+ (void)addImageToiPhoto:(id)fp8;
- (id)init;
- (void)dealloc;
- (void)setDataSource:(id)fp8;
- (void)loadConfigData;
- (void)runSlideshowWithDataSource:(id)fp8 options:(id)fp12;
- (void)startSlideshow:(id)fp8;
- (void)runSlideshowWithPDF:(id)fp8 options:(id)fp12;
- (void)stopSlideshow:(id)fp8;
- (void)noteNumberOfItemsChanged;
- (void)reloadData;
- (int)indexOfCurrentObject;
- (void)setAutoPlayDelay:(float)fp8;
- (void)mouseMoved:(id)fp8;
@end

@interface MyDelegate : NSObject {
NSMutableArray* mFilenames;
}
- (void) addFilename:(const char*)utf8;
@end

@implementation MyDelegate
- (id)init {
self = [super init];
if (self != nil) {
mFilenames = [[NSMutableArray alloc] init];
}
return self;
}

- (void)dealloc {
[mFilenames dealloc];
[super dealloc];
}

// Useful if you need to find out what interface you have to implement...
- (BOOL)respondsToSelector:(SEL)aSelector {
//NSLog(@"respondsToSelector: %@", NSStringFromSelector(aSelector));
return [super respondsToSelector: aSelector];
}

- (void)addFilename:(const char*)utf8 {
[mFilenames addObject:[NSString stringWithUTF8String: utf8]];
}

- (int)numberOfObjectsInSlideshow {
return [mFilenames count];
}

- (id)slideshowObjectAtIndex:(int)index {
return [mFilenames objectAtIndex: index];
}

// Start the slideshow as soon as we're running.
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
(void) aNotification;

Slideshow* slideshow = [Slideshow sharedSlideshow];
[slideshow runSlideshowWithDataSource:self options:nil];
}

// Tell Cocoa that when the slideshow ends, our work here is done.
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)theApplication {
(void)theApplication;
return YES;
}

@end

int main(int argCount, char* argValues[]) {
// These are required if we want Cocoa to work.
static NSAutoreleasePool* global_pool;
global_pool = [[NSAutoreleasePool alloc] init];
static NSApplication* application;
application = [NSApplication sharedApplication];

MyDelegate* delegate = [[MyDelegate alloc] init];
(void) argCount;
while (*++argValues) {
[delegate addFilename: *argValues];
}
[application setDelegate: delegate];
[application run];

return 0;
}

Build with: gcc -o slideshow slideshow.m -framework Cocoa -F/System/Library/PrivateFrameworks -framework Slideshow

Run with: ./slideshow ~/Pictures/*

So not only can you use Mac OS' slideshow functionality from your Java programs, you can use it from your scripts, too. How cool is that? If the world isn't awash with fancy-looking pr0n viewers by Monday, I'll want to know why...