BOLTS : Click to return to MacEdition homepage
 

Arrrr! Avast ye screen savers! (Part 2)

By Mark Dalrymple (markd@borkware.com), May 16, 2002

Feedback Farm

Have something to say about this article? Let us know below and your post might be the Post of the Month! Please read our Official Rules and Sponsor List.

Forums

Want to dig even deeper? Post to the new MacEdition Forums!

When last we left our intrepid hero, he was bundling up the sample Mac OS X screen-saver. There’s still work to do, though. Be sure to read Part 1 (or re-read as necessary) as we leap right back into the programming pit.

Preview Mode

Remember the little box in the System Preferences panel where you can simulate what the screen-saver is going to do? That’s Preview Mode (as opposed to Normal mode where you suck up all the bits on the screen you can get your hands on). The flag passed to initWithFrame: isPreview: tells you if you’re running in this mode. Why would you care? There might be features or functions that aren’t appropriate for Preview. For instance, if your screen saver bounces a colored line around the screen, you’d use a thin line in preview mode and a thick line when you have the screen to yourself. For my Drip Screen Saver (which drips little square boxes down from the top of the screen. Very retro), I reduced the block size in preview mode so that the user could get a better feel of what the thing actually does. Note that preview pane is an irregular size (233 pixels across), so if you have any assumptions about the screen (like it’s an even number of pixels tall, a multiple of 16 pixels wide), you could have drawing that’s not aesthetically pleasing. So let’s be careful out there.

The screen saver name that’s displayed in the Screen Saver control pane is taken from the name of the screen saver bundle that’s in the Library/Screen Savers directory. If you rename this bundle, you’ll rename your screen saver in the control pane. (You might need to close and re-open the System Preferences to get the name to refresh.) If you decide you want to change the name of your screen saver to something more interesting or descriptive, do this:

Configuration and Preferences

Sure, your screen-saver is perfect the way it is, but what about those silly users who want to customize it via that little “Configure” button on the Screen Saver Preview Pane? Overriding hasConfigureSheet and returning YES will enable the button. To actually make something happen, though, you will have to:

NSBundle’s loadNibNamed: works just fine for loading your window and any controller objects you need. (There is no need to do the bundleForClass: thing that you had to do when explicitly loading other kinds of resources.)

Of course, there’s that minor detail of writing the code to actually handle the user actions on the sheet. This is the usual “write some target methods on your class, hook up the connections in Interface Builder” thing. You’ll need to provide some mechanism (like OK and Cancel buttons) to dismiss your preference sheet by invoking [NSApp endSheet: configureSheet returnCode: NSOKButton]. If you don’t do this, your sheet will display but will never go back up. Your only recourse is to force the System Preferences program to quit, or kill it from the terminal.

Now that you have a window that can accept user input, you need a way to store that information so that subsequent invocations of your screen saver can react accordingly. If you’ve used the UserPreferences API, the ScreenSaverDefaults will seem like home. Set up a dictionary of default values and use registerDefaults to tell the preference system what the factory default values are. For instance, from the Drip screen saver:


     ScreenSaverDefaults *userPrefs;
     NSDictionary *defaults;

     defaults = [NSDictionary dictionaryWithObjectsAndKeys:
                 [NSNumber numberWithFloat: 25.0], @"density",
                 [NSNumber numberWithFloat: 40.0], @"rate", nil];

     userPrefs = [ScreenSaverDefaults defaultsForModuleWithName:@"BWDrip"];
     [userPrefs registerDefaults: defaults];

When the user changes a value for one of your persistent pieces of data, you set the values using the preference API, such as setFloat:forKey: and retrieve them with floatForKey: I’ve been putting such code in the “OK” button handler:


- (IBAction) handleOK: (id) sender
{
     ScreenSaverDefaults *userPrefs;

     userPrefs = [ScreenSaverDefaults defaultsForModuleWithName:@"BWDrip"];
     [userPrefs setFloat: [rateSlider floatValue]  forKey: @"rate"];
     [userPrefs setFloat: [densitySlider floatValue]   forKey:@"density"];

     [userPrefs synchronize];

     [NSApp endSheet: configureSheet  returnCode: NSOKButton];

     // and then tell your screen saver view to redraw itself

} // handleOK

You’ll need to pick a globally unique key for your screen saver data. For example, you may want to base it off of your company name or domain name. (In the code above I used BWDrip, but I probably should have really used something like com.borkware.screensavers.drip. Maybe for version 2.0.)

When your screen saver starts up, it can get the data. Wheee! Based on the code we used above to register the defaults, the following code would be used to retrieve their current values:

Resources

Also check VersionTracker and your favorite search engine for “Mac OS X screen savers” and “Mac OS X screensavers.” There are a lot of nifty screen savers out there, many of them including source.


dripRate = [userPrefs floatForKey: @"rate"]; 
density = [userPrefs floatForKey: @"density"];

There’s a file (a plist) that stores the preference information that differs from your factory defaults. It lives in the ~/Library/Preferences/ByHost directory and has a file name based on the key you gave to defaultsForModuleWithName:, along with a large number appended to it. So, for my BWDrip screen saver code above, the file name would be something like BWDrip.000502ceec62.plist.

This is good to know when you’re testing your preference storage. Nuke this file occasionally to make sure things will be working right the first time someone uses your screen saver, and that your defaults give you the effect that people should see the first time they run it.

One little gotcha (thanks to the folks at uselesssoft for this one) is that you should manually call the synchronize method on the ScreenSaverDefaults object, like in the handleOK: method above, to force the writing of the data to disk. If you don’t do this, occasionally the data won’t get saved properly. Presumably there’s some race condition happening between the setting of of values to the ScreenSaverDefaults object and when the values get flushed to disk.

Code Structure

Since screen savers are written in Cocoa-land, you can use one of the pervasive design patterns, the good old Model View Controller thingie for handling your user preferences.

If your screen saver is simple (or you’re just hacking around) and the data storage needs are simple, you can just stick all this stuff (loading of the preference sheet nib file, the IBOutletss for referencing the UI objects, and the IBActions for changing the preference values) into your View class. This also saves you the trouble of building an an API between the controller and the view. Personally, I tend to just glom everything together into the ScreenSaverView and then break out stuff once that class starts getting too big or unwieldy.

Features you can use in the screen savers

You have full access to the Cocoa libraries inside of your screen saver, so you can do pretty much whatever you want. You can use NSURL to hit a website and pull down some text or images to display. You can use NSSound to make some noise. OpenGL is there to do 3-D rendering. In short, screen savers are an excuse to exercise creativity and have some fun.

Mark Dalrymple (markd@borkware.com) has been wrangling Mac and Unix systems for entirely too many years. If you need a custom Mac OS X application developed, boing on over to Borkware and we’ll talk.

E-mail this story to a friend

Talkback on this story!