Plugins/Displays/GrowlDisplayWindowController.m
author Peter Hosey
Fri Jun 05 21:49:20 2009 -0700 (2009-06-05)
changeset 4208 10d1c49d4df1
parent 4142 5deddffb750a
child 4222 c6e8a12dd818
permissions -rw-r--r--
Fix MusicVideo not showing non-first notifications on displays that don't have the menu bar on them.

The problem was that GrowlDisplayWindowController and GrowlPositionController were both trying to get the screen of a window that wasn't on a screen (yet|anymore), both before and after displaying the notification. The solution is to get the screen from the display window controller, not from the window.
     1 //
     2 //  GrowlDisplayWindowController.m
     3 //  Display Plugins
     4 //
     5 //  Created by Mac-arena the Bored Zo on 2005-06-03.
     6 //  Copyright 2004-2006 The Growl Project. All rights reserved.
     7 //
     8 
     9 #import "GrowlDisplayWindowController.h"
    10 #import "GrowlPathUtilities.h"
    11 #import "GrowlDefines.h"
    12 #import "GrowlWindowTransition.h"
    13 #import "GrowlPositionController.h"
    14 #import "NSViewAdditions.h"
    15 #import "GrowlNotificationDisplayBridge.h"
    16 #import "GrowlApplicationNotification.h"
    17 #import "GrowlNotificationView.h"
    18 
    19 #include "GrowlLog.h"
    20 
    21 #define DEFAULT_TRANSITION_DURATION	0.75
    22 
    23 static NSMutableDictionary *existingInstances;
    24 
    25 @interface GrowlDisplayWindowController (PRIVATE)
    26 - (void)cancelDisplayDelayedPerforms;
    27 - (BOOL)supportsStickyNotifications;
    28 @end
    29 
    30 @interface NSWindow (LeopardMethods)
    31 - (void)setCollectionBehavior:(int)collectionBehavior;
    32 @end
    33 
    34 @implementation GrowlDisplayWindowController
    35 
    36 #pragma mark -
    37 #pragma mark Caching
    38 
    39 + (void) registerInstance:(id)instance withIdentifier:(NSString *)ident {
    40 	if (!existingInstances)
    41 		existingInstances = [[NSMutableDictionary alloc] init];
    42 
    43 	NSDictionary *classInstances = [existingInstances objectForKey:self];
    44 	if (!classInstances) {
    45 		classInstances = [[NSMutableDictionary alloc] init];
    46 		[existingInstances setObject:classInstances forKey:self];
    47 		[classInstances release];
    48 	}
    49 	[classInstances setValue:instance forKey:ident];
    50 }
    51 
    52 + (id) instanceWithIdentifier:(NSString *)ident {
    53 	NSMutableDictionary *classInstances = [existingInstances objectForKey:self];
    54 	if (classInstances)
    55 		return [classInstances objectForKey:ident];
    56 	else
    57 		return nil;
    58 }
    59 
    60 + (void) unregisterInstanceWithIdentifier:(NSString *)ident {
    61 	NSMutableDictionary *classInstances = [existingInstances objectForKey:self];
    62 	if (classInstances)
    63 		[classInstances removeObjectForKey:ident];
    64 }
    65 
    66 #pragma mark -
    67 
    68 - (id) initWithWindowNibName:(NSString *)windowNibName bridge:(GrowlNotificationDisplayBridge *)displayBridge {
    69 	// NOTE: for completeness we ought to offer the other nib related init methods with the plugin as a param
    70 	if ((self = [self initWithWindowNibName:windowNibName owner:displayBridge])) {
    71 		[self setBridge:displayBridge]; // weak reference
    72 	}
    73 	return self;
    74 }
    75 
    76 - (id) initWithBridge:(GrowlNotificationDisplayBridge *)displayBridge {
    77 	/* Subclasses using this method should call initWithWindowNibName: from init */
    78 	if ((self = [self init])) {
    79 		[self setBridge:displayBridge]; // weak reference
    80 	}
    81 	return self;
    82 }
    83 
    84 - (id) initWithWindow:(NSWindow *)window {
    85 	if ((self = [super initWithWindow:window])) {
    86 		windowTransitions = [[NSMutableDictionary alloc] init];
    87 		ignoresOtherNotifications = NO;
    88 		bridge = nil;
    89 		startTimes = NSCreateMapTable(NSObjectMapKeyCallBacks, NSIntMapValueCallBacks, 0U);
    90 		endTimes = NSCreateMapTable(NSObjectMapKeyCallBacks, NSIntMapValueCallBacks, 0U);
    91 		transitionDuration = DEFAULT_TRANSITION_DURATION;
    92 
    93 		//Show notifications on all Spaces
    94 		if ([window respondsToSelector:@selector(setCollectionBehavior:)]) {
    95 #define NSWindowCollectionBehaviorCanJoinAllSpaces 1 << 0
    96 			[window setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces];
    97 		}
    98 
    99 		//Respond to 'close all notifications' by closing
   100 		[[NSNotificationCenter defaultCenter] addObserver:self
   101 												 selector:@selector(stopDisplay)
   102 													 name:GROWL_CLOSE_ALL_NOTIFICATIONS
   103 												   object:nil];
   104 	}
   105 
   106 	return self;
   107 }
   108 
   109 - (void) dealloc {
   110 	[self setDelegate:nil];
   111 	[[self bridge] removeObserver:self forKeyPath:@"notification"];
   112 	[[NSNotificationCenter defaultCenter] removeObserver:self];
   113 	[self stopAllTransitions];
   114 
   115 	NSFreeMapTable(startTimes);
   116 	NSFreeMapTable(endTimes);
   117 
   118 	[bridge				 release];
   119 	[target              release];
   120 	[clickContext        release];
   121 	[clickHandlerEnabled release];
   122 	[appName             release];
   123 	[appPid              release];
   124 	[windowTransitions   release];
   125 	[notification        release];
   126 
   127 	[super dealloc];
   128 }
   129 
   130 #pragma mark -
   131 #pragma mark Screenshot mode
   132 
   133 - (void) takeScreenshot {
   134 	NSView *view = [[self window] contentView];
   135 	NSString *path = [[[GrowlPathUtilities screenshotsDirectory] stringByAppendingPathComponent:[GrowlPathUtilities nextScreenshotName]] stringByAppendingPathExtension:@"png"];
   136 	[[view dataWithPNGInsideRect:[view frame]] writeToFile:path atomically:NO];
   137 }
   138 
   139 #pragma mark -
   140 #pragma mark Display control
   141 
   142 - (BOOL)reposition_startingDisplay:(BOOL)shouldStartDisplay
   143 {
   144 	NSWindow *window = [self window];
   145 
   146 	//Make sure we don't cover any other notification (or not)
   147 	BOOL foundSpace = NO;
   148 	GrowlPositionController *pc = [GrowlPositionController sharedInstance];
   149 	if ([self respondsToSelector:@selector(idealOriginInRect:)])
   150 		foundSpace = [pc positionDisplay:self];
   151 	else
   152 		foundSpace = (ignoresOtherNotifications || [pc reserveRect:[window frame] forDisplayController:self]);
   153 
   154 	if (foundSpace) {
   155 		if (shouldStartDisplay) {
   156 			[self cancelDisplayDelayedPerforms];
   157 
   158 			[self willDisplayNotification];
   159 
   160 			[window orderFront:nil];
   161 			
   162 			if ([self startAllTransitions]) {
   163 				[self performSelector:@selector(didFinishTransitionsBeforeDisplay)
   164 						   withObject:nil
   165 						   afterDelay:transitionDuration];
   166 			} else {
   167 				[self didFinishTransitionsBeforeDisplay];
   168 			}
   169 			
   170 			[self didDisplayNotification];
   171 		}
   172 		
   173 	} else {
   174 		[[NSNotificationCenter defaultCenter] postNotificationName:GrowlDisplayWindowControllerNotificationBlockedNotification
   175 															object:self];
   176 		
   177 		//Try again in 10 seconds
   178 		if (!shouldStartDisplay) {
   179 			//If we're restarting, get this display off-screen while we wait
   180 			//XXX This should be more fluid
   181 			[window orderOut:nil];
   182 
   183 			[[GrowlPositionController sharedInstance] clearReservedRectForDisplayController:self];
   184 			
   185 		}
   186 		[self performSelector:@selector(startDisplay) withObject:nil afterDelay:5];
   187 	}
   188 	
   189 	return foundSpace;		
   190 }
   191 
   192 - (BOOL) startDisplay {
   193 	return [self reposition_startingDisplay:YES];
   194 }
   195 
   196 - (void) stopDisplay {	
   197 	id contentView = [[self window] contentView];
   198 	if ([contentView respondsToSelector:@selector(mouseOver)] &&
   199 		[contentView mouseOver] &&
   200 		!userRequestedClose) {
   201 		//The mouse is currently within the view; close when it exits
   202 		[contentView setCloseOnMouseExit:YES];
   203 
   204 	} else {
   205 		//If we're already transitioning out, just keep doing our thing
   206 		if (displayStatus != GrowlDisplayTransitioningOutStatus) {
   207 			[self cancelDisplayDelayedPerforms];
   208 
   209 			[self willTakeDownNotification];
   210 			if ([self startAllTransitions]) {
   211 				[self performSelector:@selector(didFinishTransitionsAfterDisplay) 
   212 						   withObject:nil
   213 						   afterDelay:transitionDuration];
   214 			} else {
   215 				[self didFinishTransitionsAfterDisplay];
   216 			}
   217 		}
   218 	}
   219 }
   220 
   221 - (void) clickedClose {
   222 	userRequestedClose = YES;
   223 	[self stopDisplay];
   224 }
   225 
   226 #pragma mark -
   227 #pragma mark Display stages
   228 
   229 - (void)cancelDisplayDelayedPerforms
   230 {
   231 	[[self class] cancelPreviousPerformRequestsWithTarget:self
   232 												 selector:@selector(didFinishTransitionsBeforeDisplay) 
   233 												   object:nil];
   234 	
   235 	[[self class] cancelPreviousPerformRequestsWithTarget:self
   236 												 selector:@selector(didFinishTransitionsAfterDisplay) 
   237 												   object:nil];
   238 
   239 	[[self class] cancelPreviousPerformRequestsWithTarget:self
   240 												 selector:@selector(stopDisplay) 
   241 												   object:nil];	
   242 }
   243 
   244 - (void) willDisplayNotification {
   245 	displayStatus = GrowlDisplayTransitioningInStatus;
   246 
   247 	[[NSNotificationCenter defaultCenter] postNotificationName:GrowlDisplayWindowControllerWillDisplayWindowNotification
   248 														object:self];
   249 }
   250 
   251 - (void) didFinishTransitionsBeforeDisplay {
   252 	[self cancelDisplayDelayedPerforms];
   253 
   254 	if (![[[notification auxiliaryDictionary] objectForKey:GROWL_NOTIFICATION_STICKY] boolValue] ||
   255 		![self supportsStickyNotifications]) {
   256 		[self performSelector:@selector(stopDisplay)
   257 				   withObject:nil
   258 				   afterDelay:(displayDuration+transitionDuration)];		
   259 	}
   260 	
   261 	displayStatus = GrowlDisplayOnScreenStatus;
   262 }
   263 
   264 - (void) didFinishTransitionsAfterDisplay {
   265 	[self cancelDisplayDelayedPerforms];
   266 
   267 	//Clear the rect we reserved...
   268 	NSWindow *window = [self window];
   269 	[window orderOut:nil];
   270 
   271 	//Release all window transitions immediately; they may have retained our window.
   272 	[self stopAllTransitions];
   273 	[windowTransitions release]; windowTransitions = nil;
   274 
   275 	[[GrowlPositionController sharedInstance] clearReservedRectForDisplayController:self];
   276 
   277 	[self didTakeDownNotification];
   278 
   279 	if ((bridge) && ([bridge respondsToSelector:@selector(display)]))
   280 		[[bridge display] displayWindowControllerDidTakeDownWindow:self];
   281 	else {
   282 		NSLog(@"%@ bridge does not respond to display",bridge);
   283 	}
   284 }
   285 
   286 - (void) didDisplayNotification {
   287 	if (screenshotMode)
   288 		[self takeScreenshot];
   289 
   290 	[[NSNotificationCenter defaultCenter] postNotificationName:GrowlDisplayWindowControllerDidDisplayWindowNotification
   291 														object:self];
   292 }
   293 
   294 - (void) willTakeDownNotification {
   295 	[[NSNotificationCenter defaultCenter] postNotificationName:GrowlDisplayWindowControllerWillTakeWindowDownNotification
   296 														object:self];
   297 	displayStatus = GrowlDisplayTransitioningOutStatus;
   298 }
   299 
   300 - (void) didTakeDownNotification {
   301 	NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
   302 	if (clickContext) {
   303 		NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] initWithCapacity:2U];
   304 		[userInfo setValue:clickContext forKey:GROWL_KEY_CLICKED_CONTEXT];
   305 		if (appPid)
   306 			[userInfo setValue:appPid forKey:GROWL_APP_PID];
   307 		[nc postNotificationName:GROWL_NOTIFICATION_TIMED_OUT object:appName userInfo:userInfo];
   308 		[userInfo release];
   309 
   310 		//Avoid duplicate click messages by immediately clearing the clickContext
   311 		clickContext = nil;
   312 	}
   313 	[nc postNotificationName:GrowlDisplayWindowControllerDidTakeWindowDownNotification object:self];
   314 }
   315 #pragma mark -
   316 #pragma mark Click feedback
   317 
   318 - (void) notificationClicked:(id)sender {
   319 #pragma unused(sender)
   320 	if (clickContext) {
   321 		NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] initWithCapacity:3U];
   322 		[userInfo setValue:clickHandlerEnabled forKey:@"ClickHandlerEnabled"];
   323 		[userInfo setValue:clickContext forKey:GROWL_KEY_CLICKED_CONTEXT];
   324 		if (appPid)
   325 			[userInfo setValue:appPid forKey:GROWL_APP_PID];
   326 		[[NSNotificationCenter defaultCenter] postNotificationName:GROWL_NOTIFICATION_CLICKED
   327 															object:appName
   328 														  userInfo:userInfo];
   329 		[userInfo release];
   330 
   331 		//Avoid duplicate click messages by immediately clearing the clickContext
   332 		clickContext = nil;
   333 	}
   334 
   335 	if (target && action && [target respondsToSelector:action])
   336 		[target performSelector:action withObject:self];
   337 
   338 	//Now that we've notified the clickContext and target, it's as if the user just clicked the close button
   339 	[self clickedClose];
   340 }
   341 
   342 #pragma mark -
   343 #pragma mark Window Transitions
   344 
   345 - (BOOL) addTransition:(GrowlWindowTransition *)transition {
   346 	[transition setWindow:[self window]];
   347 	[transition setDelegate:self];
   348 	if (![windowTransitions objectForKey:[transition class]]) {
   349 		[windowTransitions setObject:transition forKey:[transition class]];
   350 		return TRUE;
   351 	}
   352 	return FALSE;
   353 }
   354 
   355 - (void) removeTransition:(GrowlWindowTransition *)transition {
   356 	[transition setDelegate:nil];
   357 	[transition setWindow:nil];
   358 	
   359 	[windowTransitions removeObjectForKey:[transition class]];
   360 }
   361 
   362 - (void) setStartPercentage:(unsigned)start endPercentage:(unsigned)end forTransition:(GrowlWindowTransition *)transition {
   363 	NSAssert1((start <= 100U || start < end),
   364 			  @"The start parameter was invalid for the transition: %@",
   365 			  transition);
   366 	NSAssert1((end <= 100U || start < end),
   367 			  @"The end parameter was invalid for the transition: %@",
   368 			  transition);
   369 
   370 	NSMapInsert(startTimes, transition, (void *)start);
   371 	NSMapInsert(endTimes, transition, (void *)end);
   372 }
   373 
   374 #pragma mark-
   375 
   376 - (NSArray *) allTransitions {
   377 	return [windowTransitions allValues];
   378 }
   379 
   380 - (NSArray *) activeTransitions {
   381 	int count = [windowTransitions count];
   382 	NSMutableArray *result = [NSMutableArray arrayWithCapacity:count];
   383 	NSArray *transitionArray = [windowTransitions allValues];
   384 
   385 	int i;
   386 	for (i=0; i<count; ++i) {
   387 		GrowlWindowTransition *transition = [transitionArray objectAtIndex:i];
   388 		if ([transition isAnimating])
   389 			[result addObject:transition];
   390 	}
   391 
   392 	return result;
   393 }
   394 
   395 - (NSArray *) inactiveTransitions {
   396 	int count = [windowTransitions count];
   397 	NSMutableArray *result = [NSMutableArray arrayWithCapacity:count];
   398 	NSArray *transitionArray = [windowTransitions allValues];
   399 
   400 	int i;
   401 	for (i=0; i<count; ++i) {
   402 		GrowlWindowTransition *transition = [transitionArray objectAtIndex:i];
   403 		if (![transition isAnimating])
   404 			[result addObject:transition];
   405 	}
   406 
   407 	return result;
   408 }
   409 
   410 - (BOOL) startAllTransitions {
   411 	BOOL result = NO;
   412 	GrowlWindowTransition *transition;
   413 	NSEnumerator *transitionEnum = [[windowTransitions allValues] objectEnumerator];
   414 
   415 	while ((transition = [transitionEnum nextObject]))
   416 		if ([self startTransition:transition])
   417 			result = YES;
   418 	return result;
   419 }
   420 
   421 - (BOOL) startTransition:(GrowlWindowTransition *)transition {
   422 	int startPercentage = (int) NSMapGet(startTimes, transition);
   423 	int endPercentage   = (int) NSMapGet(endTimes, transition);
   424 
   425 	// If there were no times set up then the end time would be NULL (0)...
   426 	if (endPercentage == 0)
   427 		return NO;
   428 
   429 	// Work out the start and the end times...
   430 	CFTimeInterval startTime = (float)startPercentage * ((float)transitionDuration * 0.01);
   431 	CFTimeInterval endTime = (float)endPercentage * ((float)transitionDuration * 0.01);
   432 
   433 	// Set up this transition...
   434 	[transition setDuration: (endTime - startTime)];
   435 	[transition performSelector:@selector(startAnimation) 
   436 					 withObject:nil
   437 					 afterDelay:startTime];
   438 
   439 	return YES;
   440 }
   441 
   442 - (BOOL) startTransitionOfKind:(Class)transitionClass {
   443 	GrowlWindowTransition *transition = [windowTransitions objectForKey:transitionClass];
   444 	if (transition)
   445 		return [self startTransition:transition];
   446 	return NO;
   447 }
   448 
   449 - (void) stopAllTransitions {
   450 	GrowlWindowTransition *transition;
   451 	NSEnumerator *transitionEnum = [[windowTransitions allValues] objectEnumerator];
   452 	while (( transition = [transitionEnum nextObject] ))
   453 		[self stopTransition:transition];
   454 }
   455 
   456 - (void) stopTransition:(GrowlWindowTransition *)transition {
   457 	[transition stopAnimation];
   458 	[self removeTransition:transition];
   459 
   460 	[[self class] cancelPreviousPerformRequestsWithTarget:transition
   461 												 selector:@selector(startAnimation)
   462 												   object:nil];
   463 }
   464 
   465 - (void) stopTransitionOfKind:(Class)transitionClass {
   466 	GrowlWindowTransition *transition = [windowTransitions objectForKey:transitionClass];
   467 	if (transition)
   468 		[self stopTransition:transition];
   469 }
   470 
   471 - (void) animationDidEnd:(NSAnimation *)animation
   472 {
   473 	if ([animation isKindOfClass:[GrowlWindowTransition class]] &&
   474 		([(GrowlWindowTransition *)animation window] == [self window]) &&
   475 		([(GrowlWindowTransition *)animation direction] == GrowlReverseTransition)) {
   476 		//A fade out nonrepeating animation finished. We don't need to wait on our timeout; we know we finished displaying a notification.
   477 		[self didFinishTransitionsAfterDisplay];
   478 	}
   479 }
   480 
   481 - (void) reverseAllTransitions
   482 {
   483 	[[windowTransitions allValues] makeObjectsPerformSelector:@selector(reverse)];
   484 }
   485 
   486 #pragma mark -
   487 - (void) mouseEnteredNotificationView:(GrowlNotificationView *)notificationView
   488 {
   489 #pragma unused (notificationView)
   490 	if (!userRequestedClose &&
   491 		(displayStatus == GrowlDisplayTransitioningOutStatus)) {
   492 		// We're transitioning out; we need to go back to transitioning in...
   493 		[self willDisplayNotification];
   494 		[self reverseAllTransitions];
   495 		[self didFinishTransitionsBeforeDisplay];
   496 
   497 		// ...but when the mouse leaves, transition out again
   498 		[self stopDisplay];
   499 	}
   500 }
   501 
   502 - (void) mouseExitedNotificationView:(GrowlNotificationView *)notificationView
   503 {
   504 #pragma unused (notificationView)
   505 	// Notifies us that the mouse left the notification view.
   506 }
   507 
   508 #pragma mark -
   509 #pragma mark Notifications
   510 
   511 - (GrowlApplicationNotification *) notification {
   512 	// Only here for binding conformance
   513     return notification;
   514 }
   515 
   516 - (void) setNotification:(GrowlApplicationNotification *)theNotification {
   517     if (notification != theNotification) {
   518 		[notification release];
   519 		notification = [theNotification retain];
   520 	}
   521 }
   522 
   523 - (void) updateToNotification:(GrowlApplicationNotification *)theNotification {
   524 	[self setNotification:theNotification];
   525 
   526 	switch (displayStatus) {
   527 		case GrowlDisplayUnknownStatus:
   528 		case GrowlDisplayTransitioningInStatus:
   529 			//Do nothing; we're still transitioning in
   530 			break;
   531 			
   532 		case GrowlDisplayOnScreenStatus:
   533 			//We're on screen; reset our timer for transitioning out
   534 			[self didFinishTransitionsBeforeDisplay];
   535 			break;
   536 			
   537 		case GrowlDisplayTransitioningOutStatus:
   538 			//Reset userRequestedClose in case we were transitioning out via the user's request; we have new information!
   539 			userRequestedClose = NO;
   540 
   541 			[self willDisplayNotification];
   542 			[self reverseAllTransitions];
   543 			[self didFinishTransitionsBeforeDisplay];
   544 
   545 			break;
   546 	}
   547 
   548 	[self reposition_startingDisplay:NO];
   549 }
   550 
   551 #pragma mark -
   552 
   553 - (GrowlNotificationDisplayBridge *) bridge {
   554     return bridge;
   555 }
   556 
   557 - (void) setBridge:(GrowlNotificationDisplayBridge *)theBridge {
   558 	if (bridge != theBridge) {
   559 		if (bridge) {
   560 			NSLog(@"*** This may be an error. %@ had its bridge reset", self);
   561 			[bridge removeObserver:self forKeyPath:@"notification"];
   562 		}
   563 		
   564 		bridge = [theBridge retain];
   565 		
   566 		[bridge addObserver:self forKeyPath:@"notification" options:NSKeyValueObservingOptionNew context:NULL];
   567 		[self observeValueForKeyPath:@"notification" ofObject:bridge change:nil context:NULL];
   568 	}
   569 }
   570 
   571 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
   572 {
   573 #pragma unused(change)
   574 #pragma unused(context)
   575 	if ((object == bridge) &&
   576 		[keyPath isEqualToString:@"notification"]) {
   577 		[self setNotification:[bridge notification]];
   578 	}
   579 }
   580 
   581 #pragma mark -
   582 
   583 - (CFTimeInterval) transitionDuration {
   584     return transitionDuration;
   585 }
   586 
   587 - (void) setTransitionDuration:(CFTimeInterval)theTransitionDuration{
   588     transitionDuration = theTransitionDuration;
   589 }
   590 
   591 #pragma mark -
   592 
   593 - (CFTimeInterval) displayDuration {
   594 	return displayDuration;
   595 }
   596 
   597 - (void) setDisplayDuration:(CFTimeInterval)newDuration {
   598 	displayDuration = newDuration;
   599 }
   600 
   601 #pragma mark -
   602 
   603 - (BOOL) screenshotModeEnabled {
   604 	return screenshotMode;
   605 }
   606 
   607 - (void) setScreenshotModeEnabled:(BOOL)newScreenshotMode {
   608 	screenshotMode = newScreenshotMode;
   609 }
   610 
   611 #pragma mark -
   612 
   613 - (NSScreen *) screen {
   614 	NSArray *screens = [NSScreen screens];
   615 	if (screenNumber < [screens count])
   616 		return [screens objectAtIndex:screenNumber];
   617 	else
   618 		return [NSScreen mainScreen];
   619 }
   620 
   621 - (void) setScreen:(NSScreen *)newScreen {
   622 	unsigned newScreenNumber = [[NSScreen screens] indexOfObjectIdenticalTo:newScreen];
   623 	if (newScreenNumber == NSNotFound)
   624 		[NSException raise:NSInternalInconsistencyException format:@"Tried to set %@ %p to a screen %p that isn't in the screen list", [self class], self, newScreen];
   625 	[self setScreenNumber:newScreenNumber];
   626 }
   627 
   628 - (void) setScreenNumber:(unsigned)newScreenNumber {
   629 	[self willChangeValueForKey:@"screenNumber"];
   630 	screenNumber = newScreenNumber;
   631 	[self  didChangeValueForKey:@"screenNumber"];
   632 }
   633 
   634 - (BOOL)supportsStickyNotifications
   635 {
   636 	return ![[self window] ignoresMouseEvents];
   637 }
   638 
   639 #pragma mark -
   640 
   641 - (id) target {
   642 	return target;
   643 }
   644 
   645 - (void) setTarget:(id)object {
   646 	if (object != target) {
   647 		[target release];
   648 		target = [object retain];
   649 	}
   650 }
   651 
   652 #pragma mark -
   653 
   654 - (SEL) action {
   655 	return action;
   656 }
   657 
   658 - (void) setAction:(SEL) selector {
   659 	action = selector;
   660 }
   661 
   662 #pragma mark -
   663 
   664 - (NSString *) notifyingApplicationName {
   665 	return appName;
   666 }
   667 
   668 - (void) setNotifyingApplicationName:(NSString *)inAppName {
   669 	if (inAppName != appName) {
   670 		[appName release];
   671 		appName = [inAppName copy];
   672 	}
   673 }
   674 
   675 #pragma mark -
   676 
   677 - (NSNumber *) notifyingApplicationProcessIdentifier {
   678 	return appPid;
   679 }
   680 
   681 - (void) setNotifyingApplicationProcessIdentifier:(NSNumber *)inAppPid {
   682 	if (inAppPid != appPid) {
   683 		[appPid release];
   684 		appPid = [inAppPid retain];
   685 	}
   686 }
   687 
   688 #pragma mark -
   689 
   690 - (id) clickContext {
   691 	return clickContext;
   692 }
   693 
   694 - (void) setClickContext:(id)inClickContext {
   695 	if (clickContext != inClickContext) {
   696 		[clickContext release];
   697 		clickContext = [inClickContext retain];
   698 	}
   699 }
   700 
   701 #pragma mark -
   702 
   703 - (BOOL) ignoresOtherNotifications {
   704 	return ignoresOtherNotifications;
   705 }
   706 
   707 - (void) setIgnoresOtherNotifications:(BOOL)flag {
   708 	ignoresOtherNotifications = flag;
   709 }
   710 
   711 #pragma mark -
   712 
   713 - (id) delegate {
   714 	return delegate;
   715 }
   716 
   717 - (void) setDelegate:(id)newDelegate {
   718 	if (delegate)
   719 		[self removeNotificationObserver:delegate];
   720 
   721 	if (newDelegate)
   722 		[self addNotificationObserver:newDelegate];
   723 
   724 	delegate = newDelegate;
   725 }
   726 
   727 #pragma mark -
   728 
   729 - (NSNumber *) clickHandlerEnabled {
   730 	return clickHandlerEnabled;
   731 }
   732 
   733 - (void) setClickHandlerEnabled:(NSNumber *)flag {
   734 	if (flag != clickHandlerEnabled) {
   735 		[clickHandlerEnabled release];
   736 		clickHandlerEnabled = [flag retain];
   737 	}
   738 }
   739 
   740 #pragma mark -
   741 
   742 - (void) addNotificationObserver:(id)observer {
   743 	NSParameterAssert(observer != nil);
   744 
   745 	NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
   746 
   747 	if (observer) {
   748 		//register the new delegate.
   749 		if ([observer respondsToSelector:@selector(displayWindowControllerWillDisplayWindow:)])
   750 			[nc addObserver:observer
   751 				   selector:@selector(displayWindowControllerWillDisplayWindow:)
   752 					   name:GrowlDisplayWindowControllerWillDisplayWindowNotification
   753 					 object:self];
   754 		if ([observer respondsToSelector:@selector(displayWindowControllerDidDisplayWindow:)])
   755 			[nc addObserver:observer
   756 				   selector:@selector(displayWindowControllerDidDisplayWindow:)
   757 					   name:GrowlDisplayWindowControllerDidDisplayWindowNotification
   758 					 object:self];
   759 
   760 		if ([observer respondsToSelector:@selector(displayWindowControllerWillTakeDownWindow:)])
   761 			[nc addObserver:observer
   762 				   selector:@selector(displayWindowControllerWillTakeWindowDown:)
   763 					   name:GrowlDisplayWindowControllerWillTakeWindowDownNotification
   764 					 object:self];
   765 		if ([observer respondsToSelector:@selector(displayWindowControllerDidTakeWindowDown:)])
   766 			[nc addObserver:observer
   767 				   selector:@selector(displayWindowControllerDidTakeWindowDown:)
   768 					   name:GrowlDisplayWindowControllerDidTakeWindowDownNotification
   769 					 object:self];
   770 		if ([observer respondsToSelector:@selector(displayWindowControllerNotificationBlocked:)])
   771 			[nc addObserver:observer
   772 				   selector:@selector(displayWindowControllerNotificationBlocked:)
   773 					   name:GrowlDisplayWindowControllerNotificationBlockedNotification
   774 					 object:self];
   775 	}
   776 }
   777 - (void) removeNotificationObserver:(id)observer {
   778 	[[NSNotificationCenter defaultCenter] removeObserver:observer];
   779 }
   780 
   781 @end