Core/Source/GrowlApplicationTicket.m
author Rudy Richter
Sat Aug 01 20:50:32 2009 -0400 (2009-08-01)
changeset 4261 48b7c994f6c8
parent 4246 4f52d1d98978
child 4489 9c0b9f927d0e
permissions -rw-r--r--
PrefPane: clang warnings and setup for Sparkle
     1 //
     2 //  GrowlApplicationTicket.m
     3 //  Growl
     4 //
     5 //  Created by Karl Adam on Tue Apr 27 2004.
     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 "GrowlApplicationTicket.h"
    12 #import "GrowlNotificationTicket.h"
    13 #import "GrowlDefines.h"
    14 #import "GrowlDisplayPlugin.h"
    15 #import "NSWorkspaceAdditions.h"
    16 #import "GrowlPathUtilities.h"
    17 #include "CFGrowlAdditions.h"
    18 #include "CFURLAdditions.h"
    19 #include "CFDictionaryAdditions.h"
    20 
    21 #define UseDefaultsKey			@"useDefaults"
    22 #define TicketEnabledKey		@"ticketEnabled"
    23 #define ClickHandlersEnabledKey	@"clickHandlersEnabled"
    24 #define PositionTypeKey			@"positionType"
    25 
    26 #pragma mark -
    27 
    28 @implementation GrowlApplicationTicket
    29 
    30 //these are specifically for auto-discovery tickets, hence the requirement of GROWL_TICKET_VERSION.
    31 + (BOOL) isValidTicketDictionary:(NSDictionary *)dict {
    32 	if (!dict)
    33 		return NO;
    34 
    35 	NSNumber *versionNum = getObjectForKey(dict, GROWL_TICKET_VERSION);
    36 	if ([versionNum intValue] == 1) {
    37 		return getObjectForKey(dict, GROWL_NOTIFICATIONS_ALL)
    38 			&& getObjectForKey(dict, GROWL_APP_NAME);
    39 	} else {
    40 		return NO;
    41 	}
    42 }
    43 
    44 + (BOOL) isKnownTicketVersion:(NSDictionary *)dict {
    45 	id version = getObjectForKey(dict, GROWL_TICKET_VERSION);
    46 	return version && ([version intValue] == 1);
    47 }
    48 
    49 #pragma mark -
    50 
    51 + (id) ticketWithDictionary:(NSDictionary *)ticketDict {
    52 	return [[[GrowlApplicationTicket alloc] initWithDictionary:ticketDict] autorelease];
    53 }
    54 
    55 - (id) initWithDictionary:(NSDictionary *)ticketDict {
    56 	if (!ticketDict) {
    57 		[self release];
    58 		NSParameterAssert(ticketDict != nil);
    59 		return nil;
    60 	}
    61 	if ((self = [self init])) {
    62 		synchronizeOnChanges = NO;
    63 
    64 		appName = [getObjectForKey(ticketDict, GROWL_APP_NAME) retain];
    65 		appId = [getObjectForKey(ticketDict, GROWL_APP_ID) retain];
    66 
    67 		humanReadableNames = [[ticketDict objectForKey:GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES] retain];
    68 		notificationDescriptions = [[ticketDict objectForKey:GROWL_NOTIFICATIONS_DESCRIPTIONS] retain];
    69 
    70 		//Get all the notification names and the data about them
    71 		allNotificationNames = [ticketDict objectForKey:GROWL_NOTIFICATIONS_ALL];
    72 		NSAssert1(allNotificationNames, @"Ticket dictionaries must contain a list of all their notifications (application name: %@)", appName);
    73 
    74 		NSArray *inDefaults = [ticketDict objectForKey:GROWL_NOTIFICATIONS_DEFAULT];
    75 		if (!inDefaults) inDefaults = allNotificationNames;
    76 
    77 		NSEnumerator *notificationsEnum = [allNotificationNames objectEnumerator];
    78 		NSMutableDictionary *allNotificationsTemp = [[NSMutableDictionary alloc] initWithCapacity:[allNotificationNames count]];
    79 		NSMutableArray *allNamesTemp = [[NSMutableArray alloc] initWithCapacity:[allNotificationNames count]];
    80 		id obj;
    81 		while ((obj = [notificationsEnum nextObject])) {
    82 			NSString *name;
    83 			GrowlNotificationTicket *notification;
    84 			if ([obj isKindOfClass:[NSString class]]) {
    85 				name = obj;
    86 				notification = [[GrowlNotificationTicket alloc] initWithName:obj];
    87 			} else {
    88 				name = [obj objectForKey:@"Name"];
    89 				notification = [[GrowlNotificationTicket alloc] initWithDictionary:obj];
    90 			}
    91 			[allNamesTemp addObject:name];
    92 			[notification setTicket:self];
    93 
    94 			//Set the human readable name if we were supplied one
    95 			[notification setHumanReadableName:[humanReadableNames objectForKey:name]];
    96 			[notification setNotificationDescription:[notificationDescriptions objectForKey:name]];
    97 
    98 			[allNotificationsTemp setObject:notification forKey:name];
    99 			[notification release];
   100 		}
   101 		allNotifications = allNotificationsTemp;
   102 		allNotificationNames = allNamesTemp;
   103 
   104 		BOOL doLookup = YES;
   105 		NSString *fullPath = nil;
   106 		id location = getObjectForKey(ticketDict, GROWL_APP_LOCATION);
   107 		if (location) {
   108 			if ([location isKindOfClass:[NSDictionary class]]) {
   109 				NSDictionary *file_data = getObjectForKey((NSDictionary *)location, @"file-data");
   110 				CFURLRef url = (CFURLRef)createFileURLWithDockDescription(file_data);
   111 				if (url) {
   112 					fullPath = [(NSString *)CFURLCopyPath(url) autorelease];
   113 					CFRelease(url);
   114 				}
   115 			} else if ([location isKindOfClass:[NSString class]]) {
   116 				fullPath = location;
   117 				if (![[NSFileManager defaultManager] fileExistsAtPath:fullPath])
   118 					fullPath = nil;
   119 			} else if ([location isKindOfClass:[NSNumber class]]) {
   120 				doLookup = [location boolValue];
   121 			}
   122 		}
   123 		if (!fullPath && doLookup) {
   124 			if (appId) {
   125 				CFURLRef appURL = NULL;
   126 				OSStatus err = LSFindApplicationForInfo(kLSUnknownCreator,
   127 														(CFStringRef)appId,
   128 														/*inName*/ NULL,
   129 														/*outAppRef*/ NULL,
   130 														&appURL);
   131 				if (err == noErr) {
   132 					fullPath = [(NSString *)CFURLCopyPath(appURL) autorelease];
   133 					CFRelease(appURL);
   134 				}
   135 			}
   136 			if (!fullPath)
   137 				fullPath = [[NSWorkspace sharedWorkspace] fullPathForApplication:appName];
   138 		}
   139 		appPath = [fullPath retain];
   140 //		NSLog(@"got appPath: %@", appPath);
   141 
   142 		[self setIcon:getObjectForKey(ticketDict, GROWL_APP_ICON)];
   143 
   144 		id value = getObjectForKey(ticketDict, UseDefaultsKey);
   145 		if (value)
   146 			useDefaults = [value boolValue];
   147 		else
   148 			useDefaults = YES;
   149 
   150 		value = getObjectForKey(ticketDict, TicketEnabledKey);
   151 		if (value)
   152 			ticketEnabled = [value boolValue];
   153 		else
   154 			ticketEnabled = YES;
   155 
   156 		displayPluginName = [[ticketDict objectForKey:GrowlDisplayPluginKey] copy];
   157 
   158 		value = getObjectForKey(ticketDict, ClickHandlersEnabledKey);
   159 		if (value)
   160 			clickHandlersEnabled = [value boolValue];
   161 		else
   162 			clickHandlersEnabled = YES;
   163 		
   164 		value = getObjectForKey(ticketDict, PositionTypeKey);
   165 		if (value)
   166 			positionType = [value intValue];
   167 		else
   168 			positionType = 0;	
   169 		
   170 		value = getObjectForKey(ticketDict, GROWL_POSITION_PREFERENCE_KEY);
   171 		if (value)
   172 			selectedCustomPosition = [value intValue];
   173 		else
   174 			selectedCustomPosition = 0;				
   175 
   176 		[self setDefaultNotifications:inDefaults];
   177 
   178 		changed = YES;
   179 		synchronizeOnChanges = YES;
   180 	}
   181 	return self;
   182 }
   183 
   184 - (void) dealloc {
   185 	[appName                  release];
   186 	[appId                    release];
   187 	[appPath                  release];
   188 	[icon                     release];
   189 	[iconData                 release];
   190 	[allNotifications         release];
   191 	[defaultNotifications     release];
   192 	[humanReadableNames       release];
   193 	[notificationDescriptions release];
   194 	[allNotificationNames     release];
   195 	[displayPluginName        release];
   196 
   197 	[super dealloc];
   198 }
   199 
   200 #pragma mark -
   201 
   202 - (id) initTicketFromPath:(NSString *) ticketPath {
   203 	CFURLRef ticketURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef)ticketPath, kCFURLPOSIXPathStyle, false);
   204 	NSDictionary *ticketDict = (NSDictionary *)createPropertyListFromURL((NSURL *)ticketURL, kCFPropertyListImmutable, NULL, NULL);
   205 	CFRelease(ticketURL);
   206 	if (!ticketDict) {
   207 		NSLog(@"Tried to init a ticket from this file, but it isn't a ticket file: %@", ticketPath);
   208 		[self release];
   209 		return nil;
   210 	}
   211 
   212 	self = [self initWithDictionary:ticketDict];
   213 	[ticketDict release];
   214 	return self;
   215 }
   216 
   217 - (id) initTicketForApplication: (NSString *) inApp {
   218 	return [self initTicketFromPath:[[[[GrowlPathUtilities growlSupportDirectory]
   219 										stringByAppendingPathComponent:@"Tickets"]
   220 										stringByAppendingPathComponent:inApp]
   221 										stringByAppendingPathExtension:@"growlTicket"]];
   222 }
   223 
   224 - (NSString *) path {
   225 	NSString *destDir = [GrowlPathUtilities growlSupportDirectory];
   226 	destDir = [destDir stringByAppendingPathComponent:@"Tickets"];
   227 	destDir = [destDir stringByAppendingPathComponent:[appName stringByAppendingPathExtension:@"growlTicket"]];
   228 	return destDir;
   229 }
   230 
   231 - (void) saveTicket {
   232 	NSString *destDir = [GrowlPathUtilities growlSupportDirectory];
   233 	destDir = [destDir stringByAppendingPathComponent:@"Tickets"];
   234 
   235 	[self saveTicketToPath:destDir];
   236 }
   237 
   238 - (void) saveTicketToPath:(NSString *)destDir {
   239 	// Save a Plist file of this object to configure the prefs of apps that aren't running
   240 	// construct a dictionary of our state data then save that dictionary to a file.
   241 	NSString *savePath = [destDir stringByAppendingPathComponent:[appName stringByAppendingPathExtension:@"growlTicket"]];
   242 	NSMutableArray *saveNotifications = [[NSMutableArray alloc] initWithCapacity:[allNotifications count]];
   243 	NSEnumerator *notificationEnum = [allNotifications objectEnumerator];
   244 	GrowlNotificationTicket *obj;
   245 	while ((obj = [notificationEnum nextObject]))
   246 		[saveNotifications addObject:[obj dictionaryRepresentation]];
   247 
   248 	NSDictionary *file_data = nil;
   249 	if (appPath) {
   250 		NSURL *url = [[NSURL alloc] initFileURLWithPath:appPath];
   251 		file_data = createDockDescriptionWithURL(url);
   252 		[url release];
   253 	}
   254 
   255 	id location = file_data ? [NSDictionary dictionaryWithObject:file_data forKey:@"file-data"] : appPath;
   256 	if (!location)
   257 		location = [NSNumber numberWithBool:NO];
   258 	[file_data release];
   259 
   260 	NSNumber *useDefaultsValue = [[NSNumber alloc] initWithBool:useDefaults];
   261 	NSNumber *ticketEnabledValue = [[NSNumber alloc] initWithBool:ticketEnabled];
   262 	NSNumber *clickHandlersEnabledValue = [[NSNumber alloc] initWithBool:clickHandlersEnabled];
   263 	NSNumber *positionTypeValue = [[NSNumber alloc] initWithInt:positionType];
   264 	NSNumber *selectedCustomPositionValue = [[NSNumber alloc] initWithInt:selectedCustomPosition];
   265 	NSData *theIconData = iconData;
   266 	if (!theIconData) {
   267 		NSImage *theIcon = [self icon];
   268 		theIconData = theIcon ? [theIcon TIFFRepresentation] : [NSData data];
   269 	}
   270 	NSMutableDictionary *saveDict = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
   271 		appName,						GROWL_APP_NAME,
   272 		saveNotifications,				GROWL_NOTIFICATIONS_ALL,
   273 		defaultNotifications,			GROWL_NOTIFICATIONS_DEFAULT,
   274 		theIconData,					GROWL_APP_ICON,
   275 		useDefaultsValue,				UseDefaultsKey,
   276 		ticketEnabledValue,				TicketEnabledKey,
   277 		clickHandlersEnabledValue,		ClickHandlersEnabledKey,
   278 		positionTypeValue,				PositionTypeKey,
   279 		selectedCustomPositionValue,	GROWL_POSITION_PREFERENCE_KEY,
   280 		location,						GROWL_APP_LOCATION,
   281 		nil];
   282 	[useDefaultsValue					release];
   283 	[ticketEnabledValue					release];
   284 	[clickHandlersEnabledValue			release];
   285 	[positionTypeValue					release];
   286 	[selectedCustomPositionValue		release];
   287 	[saveNotifications					release];
   288 	
   289 	if (displayPluginName)
   290 		[saveDict setObject:displayPluginName forKey:GrowlDisplayPluginKey];
   291 
   292 	if (humanReadableNames)
   293 		[saveDict setObject:humanReadableNames forKey:GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES];
   294 
   295 	if (notificationDescriptions)
   296 		[saveDict setObject:notificationDescriptions forKey:GROWL_NOTIFICATIONS_DESCRIPTIONS];
   297 
   298 	if (appId)
   299 		[saveDict setObject:appId forKey:GROWL_APP_ID];
   300 
   301 	NSData *plistData;
   302 	NSString *error;
   303 	plistData = [NSPropertyListSerialization dataFromPropertyList:saveDict
   304 														   format:NSPropertyListBinaryFormat_v1_0
   305 												 errorDescription:&error];
   306 	if (plistData)
   307 		[plistData writeToFile:savePath atomically:YES];
   308 	else
   309 		NSLog(@"Error writing ticket for application %@: %@", appName, error);
   310 	[saveDict release];
   311 
   312 	changed = NO;
   313 }
   314 
   315 - (void) doSynchronize {
   316 	[self saveTicket];
   317 
   318 	NSNumber *pid = [[NSNumber alloc] initWithInt:[[NSProcessInfo processInfo] processIdentifier]];
   319 	NSDictionary *userInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
   320 		appName, @"TicketName",
   321 		pid,     @"pid",
   322 		nil];
   323 	[pid release];
   324 	[[NSDistributedNotificationCenter defaultCenter] postNotificationName:GrowlPreferencesChanged
   325 																   object:@"GrowlTicketChanged"
   326 																 userInfo:userInfo];
   327 	[userInfo release];	
   328 }
   329 
   330 - (void) synchronize {
   331 	if (synchronizeOnChanges) {
   332 		//Coalesce a series of changes into a single message; this makes mass changes (such as registration) much faster.
   333 		[NSObject cancelPreviousPerformRequestsWithTarget:self
   334 												 selector:@selector(doSynchronize)
   335 												   object:nil];
   336 		[self performSelector:@selector(doSynchronize)
   337 				   withObject:nil
   338 				   afterDelay:0.5];
   339 	}
   340 }
   341 
   342 #pragma mark -
   343 
   344 - (NSImage *) icon {
   345 	if (icon)
   346 		return icon;
   347 	if (iconData) {
   348 		icon = [[NSImage alloc] initWithData:iconData];
   349 		[iconData release];
   350 		iconData = nil;
   351 	}
   352 	if (!icon && appPath)
   353 		icon = [[[NSWorkspace sharedWorkspace] iconForFile:appPath] retain];
   354 	if (!icon) {
   355 		icon = [[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kGenericApplicationIcon)] retain];
   356 		[icon setSize:NSMakeSize(128.0, 128.0)];
   357 	}
   358 	return icon;
   359 }
   360 
   361 - (void) setIcon:(NSImage *)inIcon {
   362 	if (icon != inIcon) {
   363 		if ([inIcon isEqual:icon] || [inIcon isEqual:iconData])
   364 			return;
   365 		changed = YES;
   366 		[icon     release];
   367 		[iconData release];
   368 		if (inIcon) {
   369 			if ([inIcon isKindOfClass:[NSImage class]]) {
   370 				icon = [inIcon copy];
   371 				iconData = nil;
   372 			} else {
   373 				icon = nil;
   374 				iconData = (NSData *)[inIcon retain];
   375 			}
   376 		} else {
   377 			icon = nil;
   378 			iconData = nil;
   379 		}
   380 	}
   381 }
   382 
   383 - (NSString *) applicationName {
   384 	return appName;
   385 }
   386 
   387 - (BOOL) ticketEnabled {
   388 	return ticketEnabled;
   389 }
   390 
   391 - (void) setTicketEnabled:(BOOL)inEnabled {
   392 	if (ticketEnabled != inEnabled) {
   393 		ticketEnabled = inEnabled;
   394 		[self synchronize];
   395 	}
   396 }
   397 
   398 - (BOOL) clickHandlersEnabled {
   399 	return clickHandlersEnabled;
   400 }
   401 
   402 - (void) setClickHandlersEnabled:(BOOL)inEnabled {
   403 	if (clickHandlersEnabled != inEnabled) {
   404 		clickHandlersEnabled = inEnabled;
   405 		
   406 		[self synchronize];
   407 	}
   408 }
   409 
   410 - (int) positionType {
   411 	return positionType;
   412 }
   413 
   414 - (void) setPositionType:(int)inPositionType {
   415 	positionType = inPositionType;
   416 	[self synchronize];
   417 }
   418 
   419 - (int) selectedPosition {
   420 	return selectedCustomPosition;
   421 }
   422 
   423 - (void) setSelectedPosition:(int)inPosition {
   424 	selectedCustomPosition = inPosition;
   425 	[self synchronize];
   426 }
   427 
   428 - (BOOL) useDefaults {
   429 	return useDefaults;
   430 }
   431 
   432 - (void) setUseDefaults:(BOOL)flag {
   433 	useDefaults = flag;
   434 }
   435 
   436 - (BOOL) hasChanged {
   437 	return changed;
   438 }
   439 
   440 - (void) setHasChanged:(BOOL)flag {
   441 	changed = flag;
   442 }
   443 
   444 - (NSString *) displayPluginName {
   445 	return displayPluginName;
   446 }
   447 
   448 - (GrowlDisplayPlugin *) displayPlugin {
   449 	if (!displayPlugin && displayPluginName)
   450 		displayPlugin = (GrowlDisplayPlugin *)[[[GrowlPluginController sharedController] displayPluginDictionaryWithName:displayPluginName author:nil version:nil type:nil] pluginInstance];
   451 	return displayPlugin;
   452 }
   453 
   454 - (void) setDisplayPluginName: (NSString *)name {
   455 	if (![displayPluginName isEqualToString:name]) {
   456 		[displayPluginName release];
   457 		displayPluginName = [name copy];
   458 		displayPlugin = nil;
   459 		
   460 		[self synchronize];
   461 	}
   462 }
   463 
   464 #pragma mark -
   465 
   466 - (NSString *) description {
   467 	return [NSString stringWithFormat:@"<GrowlApplicationTicket: %p>{\n\tApplicationName: \"%@\"\n\ticon: %@\n\tAll Notifications: %@\n\tDefault Notifications: %@\n\tAllowed Notifications: %@\n\tUse Defaults: %@\n}",
   468 		self, appName, icon, allNotifications, defaultNotifications, [self allowedNotifications], ( useDefaults ? @"YES" : @"NO" )];
   469 }
   470 
   471 #pragma mark -
   472 
   473 - (void) reregisterWithAllNotifications:(NSArray *)inAllNotes defaults:(id)inDefaults icon:(NSImage *)inIcon {
   474 	if (!useDefaults) {
   475 		/*We want to respect the user's preferences, but if the application has
   476 		 *	added new notifications since it last registered, we want to enable those
   477 		 *	if the application says to.
   478 		 */
   479 		NSEnumerator		*enumerator;
   480 		NSMutableDictionary *allNotesCopy = [allNotifications mutableCopy];
   481 
   482 		if ([inDefaults respondsToSelector:@selector(objectEnumerator)] ) {
   483 			enumerator = [inDefaults objectEnumerator];
   484 			Class NSNumberClass = [NSNumber class];
   485 			NSUInteger numAllNotifications = [inAllNotes count];
   486 			id obj;
   487 			while ((obj = [enumerator nextObject])) {
   488 				NSString *note;
   489 				if ([obj isKindOfClass:NSNumberClass]) {
   490 					//it's an index into the all-notifications list
   491 					unsigned notificationIndex = [obj unsignedIntValue];
   492 					if (notificationIndex >= numAllNotifications) {
   493 						NSLog(@"WARNING: application %@ tried to allow notification at index %u by default, but there is no such notification in its list of %u", appName, notificationIndex, numAllNotifications);
   494 						note = nil;
   495 					} else {
   496 						note = [inAllNotes objectAtIndex:notificationIndex];
   497 					}
   498 				} else {
   499 					//it's probably a notification name
   500 					note = obj;
   501 				}
   502 
   503 				if (note && ![allNotesCopy objectForKey:note]) {
   504 					GrowlNotificationTicket *ticket = [GrowlNotificationTicket notificationWithName:note];
   505 					[ticket setHumanReadableName:[humanReadableNames objectForKey:note]];
   506 					[ticket setNotificationDescription:[notificationDescriptions objectForKey:note]];
   507 					[allNotesCopy setObject:ticket forKey:note];
   508 				}
   509 			}
   510 
   511 		} else if ([inDefaults isKindOfClass:[NSIndexSet class]]) {
   512 			NSUInteger notificationIndex;
   513 			NSUInteger numAllNotifications = [inAllNotes count];
   514 			NSIndexSet *iset = (NSIndexSet *)inDefaults;
   515 			for (notificationIndex = [iset firstIndex]; notificationIndex != NSNotFound; notificationIndex = [iset indexGreaterThanIndex:notificationIndex]) {
   516 				if (notificationIndex >= numAllNotifications) {
   517 					NSLog(@"WARNING: application %@ tried to allow notification at index %u by default, but there is no such notification in its list of %u", appName, notificationIndex, numAllNotifications);
   518 					// index sets are sorted, so we can stop here
   519 					break;
   520 				} else {
   521 					NSString *note = [inAllNotes objectAtIndex:notificationIndex];
   522 					if (![allNotesCopy objectForKey:note]) {
   523 						GrowlNotificationTicket *ticket = [GrowlNotificationTicket notificationWithName:note];
   524 						[ticket setHumanReadableName:[humanReadableNames objectForKey:note]];
   525 						[ticket setNotificationDescription:[notificationDescriptions objectForKey:note]];
   526 						[allNotesCopy setObject:ticket forKey:note];
   527 					}
   528 				}
   529 			}
   530 
   531 		} else {
   532 			if (inDefaults)
   533 				NSLog(@"WARNING: application %@ passed an invalid object for the default notifications: %@.", appName, inDefaults);
   534 		}
   535 
   536 		if (![allNotifications isEqual:allNotesCopy]) {
   537 			[allNotifications release];
   538 			allNotifications = allNotesCopy;
   539 			changed = YES;
   540 		} else {
   541 			[allNotesCopy release];
   542 		}
   543 	}
   544 
   545 	//ALWAYS set all notifications list first, to enable handling of numeric indices in the default notifications list!
   546 	[self setAllNotifications:inAllNotes];
   547 	[self setDefaultNotifications:inDefaults];
   548 
   549 	[self setIcon:inIcon];
   550 }
   551 
   552 - (void) reregisterWithDictionary:(NSDictionary *)dict {
   553 	NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
   554 
   555 	NSImage *appIcon = [dict objectForKey:GROWL_APP_ICON];
   556 	NSString *bundleId = [dict objectForKey:GROWL_APP_ID];
   557 
   558 	if (bundleId != appId && ![bundleId isEqualToString:appId]) {
   559 		[appId release];
   560 		appId = [bundleId retain];
   561 		changed = YES;
   562 	}
   563 
   564 	//XXX - should assimilate reregisterWithAllNotifications:defaults:icon: here
   565 	NSArray	*all      = [dict objectForKey:GROWL_NOTIFICATIONS_ALL];
   566 	NSArray	*defaults = [dict objectForKey:GROWL_NOTIFICATIONS_DEFAULT];
   567 
   568 	NSDictionary *newNames = [dict objectForKey:GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES];
   569 	if (newNames != humanReadableNames && ![newNames isEqual:humanReadableNames]) {
   570 		[humanReadableNames release];
   571 		humanReadableNames = [newNames retain];
   572 		changed = YES;
   573 	}
   574 
   575 	NSDictionary *newDescriptions = [dict objectForKey:GROWL_NOTIFICATIONS_DESCRIPTIONS];
   576 	if (newDescriptions != notificationDescriptions && ![newDescriptions isEqual:notificationDescriptions]) {
   577 		[notificationDescriptions release];
   578 		notificationDescriptions = [newDescriptions retain];
   579 		changed = YES;
   580 	}
   581 
   582 	if (!defaults) defaults = all;
   583 	[self reregisterWithAllNotifications:all
   584 								defaults:defaults
   585 									icon:appIcon];
   586 
   587 	NSString *fullPath = nil;
   588 	id location = [dict objectForKey:GROWL_APP_LOCATION];
   589 	if (location) {
   590 		if ([location isKindOfClass:[NSDictionary class]]) {
   591 			NSDictionary *file_data = [location objectForKey:@"file-data"];
   592 			CFURLRef url = (CFURLRef)createFileURLWithDockDescription(file_data);
   593 			if (url) {
   594 				fullPath = [(NSString *)CFURLCopyPath(url) autorelease];
   595 				if(fullPath)
   596 					CFMakeCollectable(fullPath);		
   597 				CFRelease(url);
   598 			}
   599 		} else if ([location isKindOfClass:[NSString class]]) {
   600 			fullPath = location;
   601 			if (![[NSFileManager defaultManager] fileExistsAtPath:fullPath])
   602 				fullPath = nil;
   603 		}
   604 		/* Don't handle the NSNumber case here, the app might have moved and we
   605 		 * use the re-registration to update our stored appPath.
   606 		*/
   607 	}
   608 	if (!fullPath) {
   609 		if (appId) {
   610 			CFURLRef appURL = NULL;
   611 			OSStatus err = LSFindApplicationForInfo(kLSUnknownCreator,
   612 													(CFStringRef)appId,
   613 													/*inName*/ NULL,
   614 													/*outAppRef*/ NULL,
   615 													&appURL);
   616 			if (err == noErr) {
   617 				fullPath = [(NSString *)CFURLCopyPath(appURL) autorelease];
   618 				if(fullPath)
   619 					CFMakeCollectable(fullPath);		
   620 				CFRelease(appURL);
   621 			}
   622 		}
   623 		if (!fullPath)
   624 			fullPath = [workspace fullPathForApplication:appName];
   625 	}
   626 	if (fullPath != appPath && ![fullPath isEqualToString:appPath]) {
   627 		[appPath release];
   628 		appPath = [fullPath retain];
   629 		changed = YES;
   630 	}
   631 }
   632 
   633 - (NSArray *) allNotifications {
   634 	return [[[allNotifications allKeys] retain] autorelease];
   635 }
   636 
   637 - (void) setAllNotifications:(NSArray *)inArray {
   638 	if (allNotificationNames != inArray) {
   639 		if ([inArray isEqualToArray:allNotificationNames])
   640 			return;
   641 		changed = YES;
   642 		[allNotificationNames release];
   643 		allNotificationNames = [inArray retain];
   644 
   645 		//We want to keep all of the old notification settings and create entries for the new ones
   646 		NSEnumerator *newEnum = [inArray objectEnumerator];
   647 		NSMutableDictionary *tmp = [[NSMutableDictionary alloc] initWithCapacity:[inArray count]];
   648 		id key, obj;
   649 		while ((key = [newEnum nextObject])) {
   650 			obj = [allNotifications objectForKey:key];
   651 			if (obj) {
   652 				[tmp setObject:obj forKey:key];
   653 			} else {
   654 				GrowlNotificationTicket *notification = [[GrowlNotificationTicket alloc] initWithName:key];
   655 				[notification setHumanReadableName:[humanReadableNames objectForKey:key]];
   656 				[notification setNotificationDescription:[notificationDescriptions objectForKey:key]];
   657 				[tmp setObject:notification forKey:key];
   658 				[notification release];
   659 			}
   660 		}
   661 		[allNotifications release];
   662 		allNotifications = tmp;
   663 
   664 		// And then make sure the list of default notifications also doesn't have any straglers...
   665 		NSMutableSet *cur = [[NSMutableSet alloc] initWithArray:defaultNotifications];
   666 		NSSet *new = [[NSSet alloc] initWithArray:allNotificationNames];
   667 		[cur intersectSet:new];
   668 		[defaultNotifications release];
   669 		defaultNotifications = [[cur allObjects] retain];
   670 		[cur release];
   671 		[new release];
   672 	}
   673 }
   674 
   675 - (NSArray *) defaultNotifications {
   676 	return [[defaultNotifications retain] autorelease];
   677 }
   678 
   679 - (void) setDefaultNotifications:(id)inObject {
   680 	if (!allNotifications) {
   681 		/*WARNING: if you try to pass an array containing numeric indices, and
   682 		 *	the all-notifications list has not been supplied yet, the indices
   683 		 *	WILL NOT be dereferenced. ALWAYS set the all-notifications list FIRST.
   684 		 */
   685 		if (![defaultNotifications isEqual:inObject]) {
   686 			[defaultNotifications release];
   687 			defaultNotifications = [inObject retain];
   688 			changed = YES;
   689 		}
   690 	} else if ([inObject respondsToSelector:@selector(objectEnumerator)] ) {
   691 		NSEnumerator *mightBeIndicesEnum = [inObject objectEnumerator];
   692 		NSNumber *num;
   693 		NSUInteger numDefaultNotifications;
   694 		NSUInteger numAllNotifications = [allNotificationNames count];
   695 		if ([inObject respondsToSelector:@selector(count)])
   696 			numDefaultNotifications = [inObject count];
   697 		else
   698 			numDefaultNotifications = numAllNotifications;
   699 		NSMutableArray *mDefaultNotifications = [[NSMutableArray alloc] initWithCapacity:numDefaultNotifications];
   700 		Class NSNumberClass = [NSNumber class];
   701 		while ((num = [mightBeIndicesEnum nextObject])) {
   702 			if ([num isKindOfClass:NSNumberClass]) {
   703 				//it's an index into the all-notifications list
   704 				unsigned notificationIndex = [num unsignedIntValue];
   705 				if (notificationIndex >= numAllNotifications)
   706 					NSLog(@"WARNING: application %@ tried to allow notification at index %u by default, but there is no such notification in its list of %u", appName, notificationIndex, numAllNotifications);
   707 				else
   708 					[mDefaultNotifications addObject:[allNotificationNames objectAtIndex:notificationIndex]];
   709 			} else {
   710 				//it's probably a notification name
   711 				[mDefaultNotifications addObject:num];
   712 			}
   713 		}
   714 		if (![defaultNotifications isEqualToArray:mDefaultNotifications]) {
   715 			[defaultNotifications release];
   716 			defaultNotifications = mDefaultNotifications;
   717 			changed = YES;
   718 		} else {
   719 			[mDefaultNotifications release];
   720 		}
   721 	} else if ([inObject isKindOfClass:[NSIndexSet class]]) {
   722 		NSUInteger notificationIndex;
   723 		NSUInteger numAllNotifications = [allNotificationNames count];
   724 		NSIndexSet *iset = (NSIndexSet *)inObject;
   725 		NSMutableArray *mDefaultNotifications = [[NSMutableArray alloc] initWithCapacity:[iset count]];
   726 		for (notificationIndex = [iset firstIndex]; notificationIndex != NSNotFound; notificationIndex = [iset indexGreaterThanIndex:notificationIndex]) {
   727 			if (notificationIndex >= numAllNotifications) {
   728 				NSLog(@"WARNING: application %@ tried to allow notification at index %u by default, but there is no such notification in its list of %u", appName, notificationIndex, numAllNotifications);
   729 				// index sets are sorted, so we can stop here
   730 				break;
   731 			} else {
   732 				[mDefaultNotifications addObject:[allNotificationNames objectAtIndex:notificationIndex]];
   733 			}
   734 		}
   735 		if (![defaultNotifications isEqualToArray:mDefaultNotifications]) {
   736 			[defaultNotifications release];
   737 			defaultNotifications = mDefaultNotifications;
   738 			changed = YES;
   739 		} else {
   740 			[mDefaultNotifications release];
   741 		}
   742 	} else {
   743 		if (inObject)
   744 			NSLog(@"WARNING: application %@ passed an invalid object for the default notifications: %@.", appName, inObject);
   745 		if (![defaultNotifications isEqualToArray:allNotificationNames]) {
   746 			[defaultNotifications release];
   747 			defaultNotifications = [allNotificationNames retain];
   748 			changed = YES;
   749 		}
   750 	}
   751 
   752 	if (useDefaults)
   753 		[self setAllowedNotificationsToDefault];
   754 }
   755 
   756 - (NSArray *) allowedNotifications {
   757 	NSMutableArray* allowed = [NSMutableArray array];
   758 	NSEnumerator *notificationEnum = [allNotifications objectEnumerator];
   759 	GrowlNotificationTicket *obj;
   760 	while ((obj = [notificationEnum nextObject]))
   761 		if ([obj enabled])
   762 			[allowed addObject:[obj name]];
   763 	return allowed;
   764 }
   765 
   766 - (void) setAllowedNotifications:(NSArray *) inArray {
   767 	NSSet *allowed = [[NSSet alloc] initWithArray:inArray];
   768 	NSEnumerator *notificationEnum = [allNotifications objectEnumerator];
   769 	GrowlNotificationTicket *obj;
   770 	while ((obj = [notificationEnum nextObject]))
   771 		[obj setEnabled:[allowed containsObject:[obj name]]];
   772 	[allowed release];
   773 
   774 	useDefaults = NO;
   775 }
   776 
   777 - (void) setAllowedNotificationsToDefault {
   778 	[self setAllowedNotifications:defaultNotifications];
   779 	useDefaults = YES;
   780 }
   781 
   782 - (BOOL) isNotificationAllowed:(NSString *) name {
   783 	return ticketEnabled && [[allNotifications objectForKey:name] enabled];
   784 }
   785 
   786 - (NSComparisonResult) caseInsensitiveCompare:(GrowlApplicationTicket *)aTicket {
   787 	return [appName caseInsensitiveCompare:[aTicket applicationName]];
   788 }
   789 
   790 #pragma mark Notification Accessors
   791 - (NSArray *) notifications {
   792 	return [allNotifications allValues];
   793 }
   794 
   795 - (GrowlNotificationTicket *) notificationTicketForName:(NSString *)name {
   796 	return [allNotifications objectForKey:name];
   797 }
   798 @end