Core/Source/GrowlPositionController.m
author Peter Hosey
Fri Jun 05 21:49:20 2009 -0700 (2009-06-05)
changeset 4208 10d1c49d4df1
parent 3583 66400f11c7c2
child 4227 1d6cde4c4fc3
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 //  GrowlPositionController.m
     3 //  Growl
     4 //
     5 //  Created by Ofri Wolfus on 31/08/05.
     6 //  Copyright 2004-2006 The Growl Project. All rights reserved.
     7 //
     8 // This file is under the BSD License, refer to License.txt for details
     9 //
    10 
    11 #import "GrowlPositionController.h"
    12 #import "GrowlDisplayWindowController.h"
    13 #import "GrowlPreferencesController.h"
    14 #import "NSMutableStringAdditions.h"
    15 #import "GrowlDefines.h"
    16 #import "GrowlTicketController.h"
    17 
    18 #import "GrowlLog.h"
    19 
    20 @interface GrowlPositionController (PRIVATE)
    21 - (NSMutableSet *)reservedRectsForScreen:(NSScreen *)inScreen;
    22 - (NSRectArray)copyRectsInSet:(NSSet *)rectSet count:(int *)outCount padding:(float)padding excludingDisplayController:(GrowlDisplayWindowController *)displayController;
    23 @end
    24 
    25 @implementation GrowlPositionController
    26 
    27 //Initialize
    28 - (id) initSingleton {
    29 	if ((self = [super initSingleton])) {
    30 		reservedRects = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    31 		reservedRectsByController = [[NSMutableDictionary alloc] init];
    32 	}
    33 
    34 	return self;
    35 }
    36 
    37 //Deallocate
    38 - (void) destroy {
    39 	CFRelease(reservedRects);
    40 }
    41 
    42 //Read in the stored selection from picker and translate to a properly returned GrowlPosition.
    43 + (enum GrowlPosition)selectedOriginPosition
    44 {
    45 	enum GrowlPositionOrigin globalSelectedPosition = (enum GrowlPositionOrigin)[[GrowlPreferencesController sharedController] integerForKey:GROWL_POSITION_PREFERENCE_KEY];
    46 	enum GrowlPosition translatedPosition;
    47 		
    48 	switch(globalSelectedPosition){
    49 		default:
    50 		case GrowlNoOrigin:
    51 			//Default to middle of the screen if no origin is set, though this case shouldn't be hit.
    52 			translatedPosition = GrowlMiddleColumnPosition;
    53 			break;
    54 		case GrowlTopLeftCorner:
    55 			translatedPosition = GrowlTopLeftPosition;
    56 			break;
    57 		case GrowlBottomRightCorner:
    58 			translatedPosition = GrowlBottomRightPosition;
    59 			break;
    60 		case GrowlTopRightCorner:
    61 			translatedPosition = GrowlTopRightPosition;
    62 			break;
    63 		case GrowlBottomLeftCorner:
    64 			translatedPosition = GrowlBottomLeftPosition;
    65 			break;
    66 	}
    67 	
    68 	return translatedPosition;
    69 }
    70 
    71 //Return a rect suitable for the position and screen.
    72 + (NSRect) rectForPosition:(enum GrowlPosition)position inScreen:(NSScreen *)screen {
    73 	NSRect screenFrame;
    74 	NSSize areaSize;
    75 	NSRect result = NSZeroRect;
    76 
    77 	//Treat nil as the main screen
    78 	if (!screen)
    79 		screen = [NSScreen mainScreen];
    80 
    81 	screenFrame = [screen visibleFrame];
    82 	areaSize = NSMakeSize(screenFrame.size.width / 3.0f, screenFrame.size.height / 3.0f);	//We have 9 identical areas on each screen
    83 
    84 	switch (position) {
    85 			//Top left
    86 		case GrowlTopLeftPosition:
    87 			result = NSMakeRect(screenFrame.origin.x,
    88 								screenFrame.origin.y + areaSize.height + areaSize.height,
    89 								areaSize.width,
    90 								areaSize.height);
    91 			break;
    92 
    93 			//Top middle
    94 		case GrowlTopMiddlePosition:
    95 			result = NSMakeRect(screenFrame.origin.x + areaSize.width,
    96 								screenFrame.origin.y + areaSize.height + areaSize.height,
    97 								areaSize.width,
    98 								areaSize.height);
    99 			break;
   100 
   101 			//Top right
   102 		case GrowlTopRightPosition:
   103 			result = NSMakeRect(screenFrame.origin.x + areaSize.width + areaSize.width,
   104 								screenFrame.origin.y + areaSize.height + areaSize.height,
   105 								areaSize.width,
   106 								areaSize.height);
   107 			break;
   108 
   109 			//Center left
   110 		case GrowlCenterLeftPosition:
   111 			result = NSMakeRect(screenFrame.origin.x,
   112 								screenFrame.origin.y + areaSize.height,
   113 								areaSize.width,
   114 								areaSize.height);
   115 			break;
   116 
   117 			//Center middle
   118 		case GrowlCenterMiddlePosition:
   119 			result = NSMakeRect(screenFrame.origin.x + areaSize.width,
   120 								screenFrame.origin.y + areaSize.height,
   121 								areaSize.width,
   122 								areaSize.height);
   123 			break;
   124 
   125 			//Center right
   126 		case GrowlCenterRightPosition:
   127 			result = NSMakeRect(screenFrame.origin.x + areaSize.width + areaSize.width,
   128 								screenFrame.origin.y + areaSize.height,
   129 								areaSize.width,
   130 								areaSize.height);
   131 			break;
   132 
   133 			//Bottom left
   134 		case GrowlBottomLeftPosition:
   135 			result = NSMakeRect(screenFrame.origin.x,
   136 								screenFrame.origin.y,
   137 								areaSize.width,
   138 								areaSize.height);
   139 			break;
   140 
   141 			//Bottom middle
   142 		case GrowlBottomMiddlePosition:
   143 			result = NSMakeRect(screenFrame.origin.x + areaSize.width,
   144 								screenFrame.origin.y,
   145 								areaSize.width,
   146 								areaSize.height);
   147 			break;
   148 
   149 			//Bottom right
   150 		case GrowlBottomRightPosition:
   151 			result = NSMakeRect(screenFrame.origin.x + areaSize.width + areaSize.width,
   152 								screenFrame.origin.y,
   153 								areaSize.width,
   154 								areaSize.height);
   155 			break;
   156 
   157 			//Top row
   158 		case GrowlTopRowPosition:
   159 			result = NSMakeRect(screenFrame.origin.x,
   160 								screenFrame.origin.y + areaSize.height + areaSize.height,
   161 								screenFrame.size.width,
   162 								areaSize.height);
   163 			break;
   164 
   165 			//Center row
   166 		case GrowlCenterRowPosition:
   167 			result = NSMakeRect(screenFrame.origin.x,
   168 								screenFrame.origin.y + areaSize.height,
   169 								screenFrame.size.width,
   170 								areaSize.height);
   171 			break;
   172 
   173 			//Bottom row
   174 		case GrowlBottomRowPosition:
   175 			result = NSMakeRect(screenFrame.origin.x,
   176 								screenFrame.origin.y,
   177 								screenFrame.size.width,
   178 								areaSize.height);
   179 			break;
   180 
   181 			//Left column
   182 		case GrowlLeftColumnPosition:
   183 			result = NSMakeRect(screenFrame.origin.x,
   184 								screenFrame.origin.y,
   185 								areaSize.width,
   186 								screenFrame.size.height);
   187 			break;
   188 
   189 			//Middle column
   190 		case GrowlMiddleColumnPosition:
   191 			result = NSMakeRect(screenFrame.origin.x + areaSize.width,
   192 								screenFrame.origin.y,
   193 								areaSize.width,
   194 								screenFrame.size.height);
   195 			break;
   196 
   197 			//Right column
   198 		case GrowlRightColumnPosition:
   199 			result = NSMakeRect(screenFrame.origin.x + areaSize.width + areaSize.width,
   200 								screenFrame.origin.y,
   201 								areaSize.width,
   202 								screenFrame.size.height);
   203 			break;
   204 	}
   205 
   206 	return result;
   207 }
   208 
   209 - (BOOL) positionDisplay:(GrowlDisplayWindowController *)displayController {
   210 	GrowlLog *growlLog = [GrowlLog sharedController];
   211 	
   212 	GrowlApplicationTicket *displayTicket = [[GrowlTicketController sharedController] ticketForApplicationName:[[displayController notification] applicationName]];
   213 	selectedPositionType = [displayTicket positionType];
   214 	selectedCustomPosition = (enum GrowlPositionOrigin)[displayTicket selectedPosition];
   215 
   216 	NSScreen *preferredScreen = [displayController screen];
   217 	NSRect screenFrame = [preferredScreen visibleFrame];
   218 	NSSize displaySize = [[displayController window] frame].size;
   219 	float padding = [displayController requiredDistanceFromExistingDisplays];
   220 
   221 	// Ask the display where it wants to be displayed in the first instance....
   222 	NSPoint idealOrigin;
   223 	NSRect idealFrame;
   224 
   225 	enum GrowlExpansionDirection primaryDirection = [displayController primaryExpansionDirection];
   226 	enum GrowlExpansionDirection secondaryDirection = [displayController secondaryExpansionDirection];
   227 	
   228 	if ([reservedRectsByController objectForKey:[NSValue valueWithPointer:displayController]]) {
   229 		NSRect currentlyReservedRect = [[reservedRectsByController objectForKey:[NSValue valueWithPointer:displayController]] rectValue];
   230 		idealOrigin = currentlyReservedRect.origin;
   231 		
   232 		//The expansion direction determines which origins should be kept constant
   233 		switch (primaryDirection) {
   234 			case GrowlDownExpansionDirection:
   235 				idealOrigin.y += (currentlyReservedRect.size.height - displaySize.height);
   236 				break;
   237 			case GrowlUpExpansionDirection:
   238 				break;
   239 			case GrowlLeftExpansionDirection:
   240 				idealOrigin.x += (currentlyReservedRect.size.width - displaySize.width);
   241 				break;
   242 			case GrowlRightExpansionDirection:
   243 				break;
   244 			case GrowlNoExpansionDirection:
   245 				break;
   246 		}
   247 
   248 		idealFrame = NSMakeRect(idealOrigin.x, idealOrigin.y,
   249 								displaySize.width, displaySize.height);
   250 
   251 		if (!NSContainsRect(screenFrame,idealFrame)) {
   252 			idealOrigin = [displayController idealOriginInRect:screenFrame];
   253 			idealFrame = NSMakeRect(idealOrigin.x,idealOrigin.y,displaySize.width,displaySize.height);
   254 		}
   255 	} else {
   256 		idealOrigin = [displayController idealOriginInRect:screenFrame];
   257 		idealFrame = NSMakeRect(idealOrigin.x,idealOrigin.y,displaySize.width,displaySize.height);
   258 	}
   259 	
   260 	// Try and reserve the rect
   261 	NSRect displayFrame = idealFrame;
   262 	if ([self reserveRect:displayFrame inScreen:preferredScreen forDisplayController:displayController]) {
   263 		[[displayController window] setFrame:displayFrame display:YES animate:YES];		
   264 		return YES;
   265 	}
   266 
   267 	// Something was blocking the display...try to find the next position for the display.
   268 
   269 	[growlLog writeToLog:@"---"];
   270 	[growlLog writeToLog:@"positionDisplay: could not reserve initial rect; looking for another one"];
   271 	[growlLog writeToLog:@"primaryDirection: %@", NSStringFromGrowlExpansionDirection(primaryDirection)];
   272 	[growlLog writeToLog:@"secondaryDirection: %@", NSStringFromGrowlExpansionDirection(secondaryDirection)];
   273 	
   274 	int			numberOfRects;
   275 	NSRectArray usedRects = [self copyRectsInSet:[self reservedRectsForScreen:preferredScreen] count:&numberOfRects padding:padding excludingDisplayController:displayController];
   276 
   277 	/* This will loop until the display is placed or we run off the screen entirely
   278 	 * A more 'efficient' implementation might sort all of the usedRects, then look at them iteratively.  I (evands) found it to be
   279 	 * thoroughly nontrivial to do such a sort in a robust fashion. While the below code does loop more than an 'efficient' search,
   280 	 * it ends up being a whole bunch of simple float comparisons in the worst case, which any modern computer can handle with ease. Let's
   281 	 * not over-optimize unless this is an actual bottleneck. :)
   282 	 */
   283 	while (1) {
   284 		BOOL haveBestSecondaryOrigin = NO;
   285 		float bestSecondaryOrigin = 0;
   286 
   287 		while (NSContainsRect(screenFrame,displayFrame)) {
   288 			//Adjust in our primary direction
   289 			switch (primaryDirection) {
   290 				case GrowlDownExpansionDirection:
   291 					displayFrame.origin.y -= 1;
   292 					break;
   293 				case GrowlUpExpansionDirection:
   294 					displayFrame.origin.y += 1;
   295 					break;
   296 				case GrowlLeftExpansionDirection:
   297 					displayFrame.origin.x -= 1;
   298 					break;
   299 				case GrowlRightExpansionDirection:
   300 					displayFrame.origin.x += 1;
   301 					break;
   302 				case GrowlNoExpansionDirection:
   303 					NSLog(@"This should never happen");
   304 					free(usedRects);
   305 					return NO;
   306 					break;
   307 			}
   308 			
   309 			BOOL intersects = NO;
   310 			//Check to see if the proposed displayFrame intersects with any used rect
   311 			for (int i = 0; i < numberOfRects; i++) {
   312 				if (NSIntersectsRect(displayFrame, usedRects[i])) {
   313 					//We intersected. Sadness.
   314 					intersects = YES;
   315 					
   316 					/* Determine, based on this intersection, how far we should shift if we end up moving in
   317 					 * our secondary direction.
   318 					 */
   319 					switch (secondaryDirection) {
   320 						case GrowlDownExpansionDirection:
   321 						{
   322 							if (!haveBestSecondaryOrigin ||
   323 								NSMinY(usedRects[i]) > bestSecondaryOrigin) {
   324 								haveBestSecondaryOrigin = YES;
   325 								bestSecondaryOrigin = NSMinY(usedRects[i]) - NSHeight(displayFrame);
   326 							}
   327 							break;
   328 						}
   329 						case GrowlUpExpansionDirection:
   330 						{
   331 							if (!haveBestSecondaryOrigin ||
   332 								NSMaxY(usedRects[i]) < bestSecondaryOrigin) {
   333 								haveBestSecondaryOrigin = YES;
   334 								bestSecondaryOrigin = NSMaxY(usedRects[i]);
   335 							}
   336 							break;
   337 						}
   338 						case GrowlLeftExpansionDirection:
   339 						{
   340 							if (!haveBestSecondaryOrigin ||
   341 								NSMinX(usedRects[i]) < bestSecondaryOrigin) {
   342 								haveBestSecondaryOrigin = YES;
   343 								bestSecondaryOrigin = NSMinX(usedRects[i]) - NSWidth(displayFrame);
   344 							}
   345 							break;
   346 						}
   347 						case GrowlRightExpansionDirection:
   348 						{
   349 							if (!haveBestSecondaryOrigin ||
   350 								NSMaxX(usedRects[i]) < bestSecondaryOrigin) {
   351 								haveBestSecondaryOrigin = YES;
   352 								bestSecondaryOrigin = NSMaxX(usedRects[i]);
   353 							}
   354 							break;
   355 						}
   356 						case GrowlNoExpansionDirection:
   357 							NSLog(@"This should never happen");
   358 							free(usedRects);
   359 							return NO;
   360 							break;
   361 					}
   362 				}
   363 			}
   364 			
   365 			if (!intersects) break;
   366 		}
   367 
   368 		if (NSContainsRect(screenFrame,displayFrame)) {
   369 			//The rect is on the screen! Try to reserve it.
   370 			if ([self reserveRect:displayFrame inScreen:preferredScreen forDisplayController:displayController]) {
   371 				[[displayController window] setFrame:displayFrame display:YES animate:YES];		
   372 				free(usedRects);
   373 				return YES;
   374 			}
   375 		}
   376 		// If we've run offscreen or couldn't reserve that rect, use the secondary direction after resetting from our previous efforts
   377 		switch (primaryDirection) {
   378 			case GrowlDownExpansionDirection:
   379 			case GrowlUpExpansionDirection:
   380 				displayFrame.origin.y = idealFrame.origin.y;
   381 				break;
   382 			case GrowlLeftExpansionDirection:
   383 			case GrowlRightExpansionDirection:
   384 				displayFrame.origin.x = idealFrame.origin.x;
   385 				break;
   386 			case GrowlNoExpansionDirection:
   387 				NSLog(@"This should never happen");
   388 				free(usedRects);
   389 				return NO;
   390 				break;
   391 		}
   392 		
   393 		switch (secondaryDirection) {
   394 			case GrowlDownExpansionDirection:
   395 			case GrowlUpExpansionDirection:
   396 				displayFrame.origin.y = bestSecondaryOrigin;
   397 				break;
   398 			case GrowlLeftExpansionDirection:
   399 			case GrowlRightExpansionDirection:
   400 				displayFrame.origin.x = bestSecondaryOrigin;
   401 				break;
   402 			case GrowlNoExpansionDirection:
   403 				NSLog(@"This should never happen");
   404 				free(usedRects);
   405 				return NO;
   406 				break;
   407 		}
   408 		
   409 		if (!NSContainsRect(screenFrame,displayFrame)) {
   410 			NSLog(@"Could not display Growl notification; no screen space available.");
   411 			break;
   412 		}
   413 	}
   414 	
   415 	free(usedRects);
   416 
   417 	return NO;
   418 }
   419 
   420 //Reserve a rect in a specific screen.
   421 - (BOOL) reserveRect:(NSRect)inRect inScreen:(NSScreen *)inScreen forDisplayController:(GrowlDisplayWindowController *)displayController {
   422 	BOOL result = YES;
   423 	NSValue *displayControllerValue = (displayController ? [NSValue valueWithPointer:displayController] : nil);
   424 
   425 	//Treat nil as the main screen
   426 	if (!inScreen) inScreen = [NSScreen mainScreen];
   427 
   428 	NSMutableSet	*reservedRectsOfScreen = [self reservedRectsForScreen:inScreen];
   429 	NSValue			*newRectValue = [NSValue valueWithRect:inRect];
   430 	NSEnumerator	*rectValuesEnumerator;
   431 	NSValue			*value;
   432 	
   433 	//Make sure the rect is not already reserved. However, if it is reserved by displayController, that's fine (it is just rerequesting its current space).
   434 	if ([reservedRectsOfScreen member:newRectValue] &&
   435 		(!displayController || (![[reservedRectsByController objectForKey:displayControllerValue] isEqual:newRectValue]))) {
   436 		result = NO;
   437 	} else {
   438 		rectValuesEnumerator = [reservedRectsOfScreen objectEnumerator];
   439 		
   440 		// Loop through all the values in reservedRects and make sure
   441 		// that the new rect does not intersect with any of the already
   442 		// reserved rects, excepting if the displayController itself is reserving the rect.
   443 		while ((value = [rectValuesEnumerator nextObject])) {	
   444 			if ((NSIntersectsRect(inRect, [value rectValue])) && 
   445 				(!displayController || (![[reservedRectsByController objectForKey:displayControllerValue] isEqual:value]))) {
   446 				result = NO;
   447 				break;
   448 			}
   449 		}
   450 	}
   451 	
   452 	// Add the new rect if it passed the intersection test
   453 	if (result) {
   454 		[self clearReservedRectForDisplayController:displayController];
   455 		[reservedRectsByController setObject:[NSValue valueWithRect:inRect]
   456 									  forKey:displayControllerValue];
   457 		[reservedRectsOfScreen addObject:newRectValue];
   458 	}
   459 
   460 	return result;
   461 }
   462 
   463 - (BOOL) reserveRect:(NSRect)inRect forDisplayController:(GrowlDisplayWindowController *)displayController {
   464 	return [self reserveRect:inRect inScreen:[displayController screen] forDisplayController:displayController];
   465 }
   466 
   467 - (void) clearReservedRectForDisplayController:(GrowlDisplayWindowController *)displayController
   468 {
   469 	NSValue *controllerKey = [NSValue valueWithPointer:displayController];
   470 	NSMutableSet *reservedRectsOfScreen = [self reservedRectsForScreen:[displayController screen]];
   471 	NSValue *value = [reservedRectsByController objectForKey:controllerKey];
   472 
   473 	if (value) {
   474 		[reservedRectsOfScreen removeObject:value];
   475 		[reservedRectsByController removeObjectForKey:controllerKey];
   476 	}
   477 }
   478 
   479 /*!
   480  * @method copyRectsInSet:count:padding
   481  * @brief Returns a malloc'd array of NSRect structs which were contained as values in rectSet
   482  *
   483  * @param rectSet An NSSet which must contain only NSValues representing rects via -[NSValue rectValue]
   484  * @param outCount If non-NULL, on return will have the number of rects in the returned array
   485  * @param padding Padding to add to each returned rect in the rect array
   486  * @param displayController A display controller whose rect(s) should not be included. Pass nil to include all rects.
   487  * @result A malloc'd NSRectArray. This value should be freed after use.
   488  */
   489 - (NSRectArray)copyRectsInSet:(NSSet *)rectSet count:(int *)outCount padding:(float)padding excludingDisplayController:(GrowlDisplayWindowController *)displayController
   490 {
   491 	NSEnumerator *enumerator = [rectSet objectEnumerator];
   492 	NSValue		 *value;
   493 	NSValue		 *displayControllerValue = [NSValue valueWithPointer:displayController];
   494 	int			  count = [rectSet count];
   495 	
   496 	if (outCount) *outCount = count;
   497 
   498 	NSRectArray gridRects = (NSRectArray)malloc(sizeof(NSRect) * count);
   499 	int i = 0;
   500 	while ((value = [enumerator nextObject])) {
   501 		if (!displayController || (![[reservedRectsByController objectForKey:displayControllerValue] isEqual:value])) {
   502 			gridRects[i++] = NSInsetRect([value rectValue], -padding, -padding);
   503 		}
   504 	}
   505 	
   506 	return gridRects;
   507 }
   508 
   509 //Returns the set of reserved rect for a specific screen. The return value *is* the storage!
   510 - (NSMutableSet *)reservedRectsForScreen:(NSScreen *)screen {
   511 	NSMutableSet *result = nil;
   512 
   513 	//Treat nil as the main screen
   514 	if (!screen)
   515 		screen = [NSScreen mainScreen];
   516 
   517 	//Get the set of reserved rects for our screen
   518 	result = (NSMutableSet *)CFDictionaryGetValue(reservedRects, screen);
   519 
   520 	//Make sure the set exists. If not, create it.
   521 	if (!result) {
   522 		result = [[NSMutableSet alloc] init];
   523 		CFDictionarySetValue(reservedRects, screen, result);
   524 		[result release];
   525 	}
   526 
   527 	return result;
   528 }
   529 
   530 - (enum GrowlPosition) originPosition {
   531 	if (selectedPositionType == 1) {
   532 		enum GrowlPosition translatedPosition;
   533 		switch (selectedCustomPosition) {
   534 			default:
   535 			case GrowlNoOrigin:
   536 				//Default to middle of the screen if no origin is set, though this case shouldn't be hit.
   537 				translatedPosition = GrowlMiddleColumnPosition;
   538 				break;
   539 			case GrowlTopLeftCorner:
   540 				translatedPosition = GrowlTopLeftPosition;
   541 				break;
   542 			case GrowlBottomRightCorner:
   543 				translatedPosition = GrowlBottomRightPosition;
   544 				break;
   545 			case GrowlTopRightCorner:
   546 				translatedPosition = GrowlTopRightPosition;
   547 				break;
   548 			case GrowlBottomLeftCorner:
   549 				translatedPosition = GrowlBottomLeftPosition;
   550 				break;
   551 		}		
   552 		return translatedPosition;
   553 	}
   554 	return [GrowlPositionController selectedOriginPosition];
   555 }
   556 
   557 @end
   558 
   559 NSString *NSStringFromGrowlPosition(enum GrowlPosition pos) {
   560 	NSString *str = nil;
   561 
   562 	NSString *first;
   563 	switch (pos) {
   564 		case GrowlTopLeftPosition:
   565 		case GrowlTopMiddlePosition:
   566 		case GrowlTopRightPosition:
   567 		case GrowlTopRowPosition:
   568 			first = @"top";
   569 			break;
   570 
   571 		case GrowlCenterLeftPosition:
   572 		case GrowlCenterMiddlePosition:
   573 		case GrowlCenterRightPosition:
   574 		case GrowlCenterRowPosition:
   575 			first = @"center";
   576 			break;
   577 
   578 		case GrowlBottomLeftPosition:
   579 		case GrowlBottomMiddlePosition:
   580 		case GrowlBottomRightPosition:
   581 		case GrowlBottomRowPosition:
   582 			first = @"bottom";
   583 			break;
   584 
   585 		case GrowlLeftColumnPosition:
   586 			first = @"left";
   587 			break;
   588 
   589 		case GrowlMiddleColumnPosition:
   590 			first = @"middle";
   591 			break;
   592 
   593 		case GrowlRightColumnPosition:
   594 			first = @"right";
   595 			break;
   596 
   597 		default:
   598 			first = nil;
   599 	};
   600 
   601 	NSString *second;
   602 	switch (pos) {
   603 		case GrowlTopLeftPosition:
   604 		case GrowlCenterLeftPosition:
   605 		case GrowlBottomLeftPosition:
   606 			second = @"left";
   607 			break;
   608 
   609 		case GrowlTopMiddlePosition:
   610 		case GrowlBottomMiddlePosition:
   611 			second = @"center";
   612 			break;
   613 
   614 		case GrowlCenterMiddlePosition:
   615 			//just say 'center'
   616 			second = @"";
   617 			break;
   618 
   619 		case GrowlTopRightPosition:
   620 		case GrowlCenterRightPosition:
   621 		case GrowlBottomRightPosition:
   622 			second = @"right";
   623 			break;
   624 
   625 		case GrowlTopRowPosition:
   626 		case GrowlCenterRowPosition:
   627 		case GrowlBottomRowPosition:
   628 			second = @"row";
   629 			break;
   630 
   631 		case GrowlLeftColumnPosition:
   632 		case GrowlMiddleColumnPosition:
   633 		case GrowlRightColumnPosition:
   634 			second = @"column";
   635 
   636 		default:
   637 			second = nil;
   638 	};
   639 
   640 	if (first && second) {
   641 		unsigned  firstLength = [first  length];
   642 		unsigned secondLength = [second length];
   643 
   644 		if (firstLength && secondLength) {
   645 			unsigned capacity = firstLength + secondLength + 1U;
   646 			NSMutableString *mutable = [[NSMutableString alloc] initWithCapacity:capacity];
   647 
   648 			[mutable appendString:first];
   649 			[mutable appendCharacter:'-'];
   650 			[mutable appendString:second];
   651 
   652 			str = [mutable autorelease];
   653 		} else if (firstLength || secondLength) {
   654 			str = firstLength ? first : second;
   655 		}
   656 	}
   657 
   658 	return str;
   659 }	
   660 
   661 NSString *NSStringFromGrowlExpansionDirection(enum GrowlExpansionDirection dir) {
   662 	switch (dir) {
   663 		case GrowlNoExpansionDirection:
   664 			return @"nowhere";
   665 		case GrowlDownExpansionDirection:
   666 			return @"down";
   667 		case GrowlUpExpansionDirection:
   668 			return @"up";
   669 		case GrowlLeftExpansionDirection:
   670 			return @"left";
   671 		case GrowlRightExpansionDirection:
   672 			return @"right";
   673 		default:
   674 			return nil;
   675 	};
   676 }