Wandering Polygon Walkthrough

This post is for those developers who want to have a better understanding of how to write screen savers that smoothly flow between two or more monitors.


To make this short enough to be readable, I’m going to assume you have already read through the documentation from Apple on how to write a normal screen saver.

Other than the generic project cruft, WanderingPolygon is 3 source files.

The first, WanderingPolygonViewPart.[h,m], is the original 1991 version that was written for NeXTSTEP. This isn’t even compiled, it is just there for completeness sake. An interesting note is that when I originally wrote this my NeXT workstation was black and white (well gray scal actually). I shipped the color ramp on faith, and didn’t actually see it in color until months later, when I was visiting NeXT and saw it running by chance on someone’s workstation.

When the screen saver kicks in, a full screen window is placed on top of all the other windows, and the primary view of that window is the screen saver view. This is repeated for each monitor. So if you have 3 monitors you will have 3 windows, and 3 separate views. This is why screensavers don’t span multiple monitors without some extra coding.

The most fundamental part of that coding is to divide the state or model from the view, or screen real-estate. This is why there are 2 remaining source files. WanderingPolygonView.[h,m] is the “view class”, a subclass of ScreenSaverView that every screen saver needs to inherit from. However instead of having the drawing code or drawing state in this class like in a normal screen saver, the drawing and state code is in WanderingPolygon.[h,m], the “state” class.

I’ll start with View subclass

//
//  WanderingPolygonView.h
//  WanderingPolygon
//
//  Created by Karl Kraft on 4/22/08.
//  Copyright 1991-2008 Karl Kraft. All rights reserved.
//
...

typedef enum _ViewType {
	VT_Unknown=0,
	VT_Preview,
	VT_MainScreen,
	VT_AuxScreen
} ViewType;

Because this same code needs to work in both the preview pane, on the main screen and the auxiliary screens we have a define so that each instance can discover which of the 3 cases it represents.

  • VT_Preview – an instance of the view acting as a small preview inside the preference pane
  • VT_MainScreen – an instance of the view when running as a true screen saver. This instance is on the main screen
  • VT_AuxScreen – any other instance in screen saver mode that isn’t on the main screen.

//
//  WanderingPolygonView.m
//  WanderingPolygon
//
//  Created by Karl Kraft on 4/22/08.
//  Copyright 1991-2008 Karl Kraft. All rights reserved.
//

....
WanderingPolygon *sharedPolygon = nil;

- (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview
{
...
	localPolygon = [[WanderingPolygon polygonInRects:&r count:1] retain];
...
}


When running in screen saver mode, each monitor will have a separate instance of WanderingPolygonView. However these views need to share the same underlying state, a common polygon. It might be tempting to try to stick this singleton somewhere, but the screensaver needs to deal properly with changes in monitor arrangement or presence, something the state object can’t really detect.

When running as a preview we don’t want to use that shared polygon, because we want the previous version to be limited to the small frame of the preview area. Hence we create a “localPolygon” for the preview case.


- (void)computeViewType;


This method is called only once per instance, and determines what the ViewType is for this particular instance so that drawing can be adjusted accordingly.

	if (self.window.screen == [[NSScreen screens] objectAtIndex:0]) {
		cachedViewType = VT_MainScreen;
....
		[sharedPolygon release]; sharedPolygon = nil;
		sharedPolygon = [[WanderingPolygon polygonInScreens] retain];

Since there is no easy hook for a screen saver to determine when the “screensaving” is ending, we need a hook to reset the polygon. Since we can’t do this when the saver ends, it is done the next time a view is created for the main screen. The main screen view can be detected by seeing if the frame of the saver view is The purpose of this code is to recreate the polygon whenever the screen saver terminates.

You might be tempted to use [NSScreen mainScreen] to get the “main screen”, but the “mainScreen” is not the one with the menu bar.


- (void)drawVersionInfo;
...
- (void)drawDebugInfo;


Both of these draw a small string in the bottom left of the window. drawDebugInfo can be useful if you want to understand the relationship between the view frames, and the frames of the window containing each view.


- (void)drawRect:(NSRect)rect
{
	if (cachedViewType == VT_Unknown) [self computeViewType];


If we haven’t figure out what kind of view we are, now is the time to do so.

	if (!backCleared) {
		[[NSColor blackColor] set];
		NSRectFill([self bounds]);
		backCleared = YES;
		[self drawVersionInfo];
	}


The screensaver needs a black background, but we can only draw it once when the view is starting to draw. After that the view will (by deisng) just slap more and more color on the screen over the existing drawing.


	if (cachedViewType == VT_Preview) {
		[localPolygon draw];


If a preview instance, just draw the local polygon


	} else if (cachedViewType == VT_MainScreen) {
		[sharedPolygon draw];


If the main screen, draw the polygon as normal

	} else {
		NSGraphicsContext *savedContext= [NSGraphicsContext currentContext];
		CGContextRef ctx = [savedContext graphicsPort];
		CGContextSaveGState(ctx);
		CGContextTranslateCTM(ctx, 0-self.window.frame.origin.x, 0-self.window.frame.origin.y);
//		NSLog(@"translating %@",NSStringFromRect(self.window.frame));
		[sharedPolygon draw];
		CGContextRestoreGState(ctx);


If an auxiliary screen perform a translation and then perform the same drawing as the main screen. The resulting translation will make the image appear seamless between the two (or more) monitors.


- (void)animateOneFrame


Some special code is needed here. If you have 7 screens, this method will be called 7 times, once for each instance. Therefore we test to make sure we are the main screen view or the preview instance and only then do we actually increment the state.

- (void)viewDidMoveToWindow;
...
- (void)viewDidMoveToSuperview;
...
- (void)setFrame:(NSRect)frameRect;


Because of the way the preview instance is created and added to the window, some trapping is added here to detect when the view is re-added or re-selected, and when that happens the view is cleared to black.

Coming Soon…. the state class….

This entry was posted in TheCodeBook - Fundamentals. Bookmark the permalink. Follow any comments here with the RSS feed for this post. Both comments and trackbacks are currently closed.
  • iChat Status