Common/Source/GrowlPathUtilities.m
author Rudy Richter
Sat Aug 01 20:51:35 2009 -0400 (2009-08-01)
changeset 4263 680abae744d7
parent 4246 4f52d1d98978
child 4459 fac59330ff0c
permissions -rw-r--r--
Common: clang warning
ingmarstein@1791
     1
//
boredzo@2467
     2
//  GrowlPathUtil.m
ingmarstein@1791
     3
//  Growl
ingmarstein@1791
     4
//
ingmarstein@1791
     5
//  Created by Ingmar Stein on 17.04.05.
ingmarstein@3040
     6
//  Copyright 2005-2006 The Growl Project. All rights reserved.
ingmarstein@1791
     7
//
ingmarstein@1791
     8
// This file is under the BSD License, refer to License.txt for details
ingmarstein@1791
     9
ingmarstein@2470
    10
#import "GrowlPathUtilities.h"
ingmarstein@2472
    11
#import "GrowlPreferencesController.h"
ingmarstein@2472
    12
#import "GrowlTicketController.h"
boredzo@2479
    13
#import "GrowlDefinesInternal.h"
ingmarstein@1791
    14
ingmarstein@1791
    15
static NSBundle *helperAppBundle;
ingmarstein@1791
    16
static NSBundle *prefPaneBundle;
ingmarstein@1791
    17
boredzo@2477
    18
#define NAME_OF_SCREENSHOTS_DIRECTORY           @"Screenshots"
boredzo@2477
    19
#define NAME_OF_TICKETS_DIRECTORY               @"Tickets"
boredzo@2477
    20
#define NAME_OF_PLUGINS_DIRECTORY               @"Plugins"
boredzo@2477
    21
boredzo@2677
    22
@implementation GrowlPathUtilities
boredzo@2677
    23
boredzo@2467
    24
#pragma mark Bundles
ingmarstein@1791
    25
boredzo@4043
    26
//Searches the process list (as yielded by GetNextProcess) for a process with the given bundle identifier.
boredzo@4043
    27
//Returns the oldest matching process.
boredzo@4043
    28
+ (NSBundle *) bundleForProcessWithBundleIdentifier:(NSString *)identifier
boredzo@4043
    29
{
boredzo@4043
    30
boredzo@4043
    31
restart:;
boredzo@4043
    32
	OSStatus err;
boredzo@4043
    33
	NSBundle *bundle = nil;
boredzo@4043
    34
	struct ProcessSerialNumber psn = { 0, 0 };
boredzo@4043
    35
	UInt32 oldestProcessLaunchDate = UINT_MAX;
boredzo@4043
    36
boredzo@4043
    37
	while ((err = GetNextProcess(&psn)) == noErr) {
Rudy@4246
    38
		struct ProcessInfoRec info = { .processInfoLength = (UInt32)sizeof(struct ProcessInfoRec) };
boredzo@4043
    39
		err = GetProcessInformation(&psn, &info);
boredzo@4043
    40
		if (err == noErr) {
boredzo@4043
    41
			//Compare the launch dates first, since it's cheaper than comparing bundle IDs.
boredzo@4043
    42
			if (info.processLaunchDate < oldestProcessLaunchDate) {
boredzo@4043
    43
				//This one is older (fewer ticks since startup), so this is our current prospect to be the result.
boredzo@4043
    44
				NSDictionary *dict = (NSDictionary *)ProcessInformationCopyDictionary(&psn, kProcessDictionaryIncludeAllInformationMask);
Rudy@4263
    45
				
boredzo@4043
    46
				if (dict) {
Rudy@4263
    47
					CFMakeCollectable(dict);
boredzo@4043
    48
					pid_t pid = 0;
boredzo@4043
    49
					GetProcessPID(&psn, &pid);
boredzo@4043
    50
					if ([[dict objectForKey:(NSString *)kCFBundleIdentifierKey] isEqualToString:identifier]) {
boredzo@4043
    51
						NSString *bundlePath = [dict objectForKey:@"BundlePath"];
boredzo@4043
    52
						if (bundlePath) {
boredzo@4043
    53
							bundle = [NSBundle bundleWithPath:bundlePath];
boredzo@4043
    54
							oldestProcessLaunchDate = info.processLaunchDate;
boredzo@4043
    55
						}
boredzo@4043
    56
					}
boredzo@4043
    57
boredzo@4043
    58
					[dict release];
boredzo@4043
    59
				} else {
boredzo@4043
    60
					//ProcessInformationCopyDictionary returning NULL probably means that the process disappeared out from under us (i.e., exited) in between GetProcessInformation and ProcessInformationCopyDictionary. Start over.
boredzo@4043
    61
					goto restart;
boredzo@4043
    62
				}
boredzo@4043
    63
			}
boredzo@4043
    64
		} else {
boredzo@4043
    65
			if (err != noErr) {
boredzo@4043
    66
				//Unexpected failure of GetProcessInformation (Process Manager got confused?). Assume severe breakage and bail.
boredzo@4043
    67
				NSLog(@"Couldn't get information about process %lu,%lu: GetProcessInformation returned %i/%s", psn.highLongOfPSN, psn.lowLongOfPSN, err, GetMacOSStatusCommentString(err));
boredzo@4043
    68
				err = noErr; //So our NSLog for GetNextProcess doesn't complain. (I wish I had Python's while..else block.)
boredzo@4043
    69
				break;
boredzo@4043
    70
			} else {
boredzo@4043
    71
				//Process disappeared out from under us (i.e., exited) in between GetNextProcess and GetProcessInformation. Start over.
boredzo@4043
    72
				goto restart;
boredzo@4043
    73
			}
boredzo@4043
    74
		}
boredzo@4043
    75
	}
boredzo@4043
    76
	if (err != procNotFound) {
boredzo@4043
    77
		NSLog(@"%s: GetNextProcess returned %i/%s", __PRETTY_FUNCTION__, err, GetMacOSStatusCommentString(err));
boredzo@4043
    78
	}
boredzo@4043
    79
boredzo@4043
    80
	return bundle;
boredzo@4043
    81
}
boredzo@4043
    82
boredzo@4043
    83
//Obtains the bundle for the active GrowlHelperApp process. Returns nil if there is no such process.
boredzo@4043
    84
+ (NSBundle *) runningHelperAppBundle {
boredzo@4049
    85
	return [self bundleForProcessWithBundleIdentifier:GROWL_HELPERAPP_BUNDLE_IDENTIFIER];
boredzo@4043
    86
}
boredzo@4043
    87
boredzo@2677
    88
+ (NSBundle *) growlPrefPaneBundle {
ingmarstein@1791
    89
	NSArray			*librarySearchPaths;
ingmarstein@1791
    90
	NSString		*path;
ingmarstein@1791
    91
	NSString		*bundleIdentifier;
ingmarstein@1791
    92
	NSEnumerator	*searchPathEnumerator;
ingmarstein@1791
    93
	NSBundle		*bundle;
ingmarstein@1791
    94
ingmarstein@2591
    95
	if (prefPaneBundle)
ingmarstein@2591
    96
		return prefPaneBundle;
ingmarstein@2327
    97
ingmarstein@2327
    98
	prefPaneBundle = [NSBundle bundleWithIdentifier:GROWL_PREFPANE_BUNDLE_IDENTIFIER];
ingmarstein@2591
    99
 	if (prefPaneBundle)
ingmarstein@2591
   100
		return prefPaneBundle;
ingmarstein@1791
   101
boredzo@4043
   102
	//If GHA is running, the prefpane bundle is the bundle that contains it.
boredzo@4043
   103
	NSBundle *runningHelperAppBundle = [self runningHelperAppBundle];
boredzo@4043
   104
	NSString *runningHelperAppBundlePath = [runningHelperAppBundle bundlePath];
boredzo@4043
   105
	//GHA in Growl.prefPane/Contents/Resources/
boredzo@4043
   106
	NSString *possiblePrefPaneBundlePath1 = [runningHelperAppBundlePath stringByDeletingLastPathComponent];
boredzo@4043
   107
	//GHA in Growl.prefPane/ (hypothetical)
boredzo@4043
   108
	NSString *possiblePrefPaneBundlePath2 = [[possiblePrefPaneBundlePath1 stringByDeletingLastPathComponent] stringByDeletingLastPathComponent];
boredzo@4043
   109
	if ([[[possiblePrefPaneBundlePath1 pathExtension] lowercaseString] isEqualToString:@"prefpane"]) {
boredzo@4043
   110
		prefPaneBundle = [NSBundle bundleWithPath:possiblePrefPaneBundlePath1];
boredzo@4043
   111
		if (prefPaneBundle)
boredzo@4043
   112
			return prefPaneBundle;
boredzo@4043
   113
	}
boredzo@4043
   114
	if ([[[possiblePrefPaneBundlePath2 pathExtension] lowercaseString] isEqualToString:@"prefpane"]) {
boredzo@4043
   115
		prefPaneBundle = [NSBundle bundleWithPath:possiblePrefPaneBundlePath2];
boredzo@4043
   116
		if (prefPaneBundle)
boredzo@4043
   117
			return prefPaneBundle;
boredzo@4043
   118
	}
boredzo@4043
   119
	
ingmarstein@1791
   120
	static const unsigned bundleIDComparisonFlags = NSCaseInsensitiveSearch | NSBackwardsSearch;
ingmarstein@1791
   121
ingmarstein@2327
   122
	NSFileManager *fileManager = [NSFileManager defaultManager];
ingmarstein@2327
   123
ingmarstein@1791
   124
	//Find Library directories in all domains except /System (as of Panther, that's ~/Library, /Library, and /Network/Library)
ingmarstein@1791
   125
	librarySearchPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask & ~NSSystemDomainMask, YES);
ingmarstein@1791
   126
boredzo@2467
   127
	/*First up, we'll look for Growl.prefPane, and if it exists, check whether
boredzo@2467
   128
	 *	it is our prefPane.
ingmarstein@1791
   129
	 *This is much faster than having to enumerate all preference panes, and
ingmarstein@1791
   130
	 *	can drop a significant amount of time off this code.
ingmarstein@1791
   131
	 */
ingmarstein@1791
   132
	searchPathEnumerator = [librarySearchPaths objectEnumerator];
ingmarstein@1791
   133
	while ((path = [searchPathEnumerator nextObject])) {
ingmarstein@1791
   134
		path = [path stringByAppendingPathComponent:PREFERENCE_PANES_SUBFOLDER_OF_LIBRARY];
ingmarstein@1791
   135
		path = [path stringByAppendingPathComponent:GROWL_PREFPANE_NAME];
ingmarstein@1791
   136
ingmarstein@2327
   137
		if ([fileManager fileExistsAtPath:path]) {
ingmarstein@1791
   138
			bundle = [NSBundle bundleWithPath:path];
ingmarstein@1791
   139
ingmarstein@1791
   140
			if (bundle) {
ingmarstein@1791
   141
				bundleIdentifier = [bundle bundleIdentifier];
ingmarstein@1791
   142
ingmarstein@1791
   143
				if (bundleIdentifier && ([bundleIdentifier compare:GROWL_PREFPANE_BUNDLE_IDENTIFIER options:bundleIDComparisonFlags] == NSOrderedSame)) {
ingmarstein@1791
   144
					prefPaneBundle = bundle;
ingmarstein@1791
   145
					return prefPaneBundle;
ingmarstein@1791
   146
				}
ingmarstein@1791
   147
			}
ingmarstein@1791
   148
		}
ingmarstein@1791
   149
	}
ingmarstein@1791
   150
ingmarstein@1791
   151
	/*Enumerate all installed preference panes, looking for the Growl prefpane
ingmarstein@1791
   152
	 *	bundle identifier and stopping when we find it.
ingmarstein@1791
   153
	 *Note that we check the bundle identifier because we should not insist
ingmarstein@1791
   154
	 *	that the user not rename his preference pane files, although most users
ingmarstein@1791
   155
	 *	of course will not.  If the user wants to mutilate the Info.plist file
ingmarstein@1791
   156
	 *	inside the bundle, he/she deserves to not have a working Growl
ingmarstein@1791
   157
	 *	installation.
ingmarstein@1791
   158
	 */
ingmarstein@1791
   159
	searchPathEnumerator = [librarySearchPaths objectEnumerator];
ingmarstein@1791
   160
	while ((path = [searchPathEnumerator nextObject])) {
ingmarstein@1791
   161
		NSString				*bundlePath;
ingmarstein@1791
   162
		NSDirectoryEnumerator   *bundleEnum;
ingmarstein@1791
   163
ingmarstein@1791
   164
		path = [path stringByAppendingPathComponent:PREFERENCE_PANES_SUBFOLDER_OF_LIBRARY];
ingmarstein@2327
   165
		bundleEnum = [fileManager enumeratorAtPath:path];
ingmarstein@1791
   166
ingmarstein@1791
   167
		while ((bundlePath = [bundleEnum nextObject])) {
ingmarstein@1791
   168
			if ([[bundlePath pathExtension] isEqualToString:PREFERENCE_PANE_EXTENSION]) {
ingmarstein@1791
   169
				bundle = [NSBundle bundleWithPath:[path stringByAppendingPathComponent:bundlePath]];
ingmarstein@1791
   170
ingmarstein@1791
   171
				if (bundle) {
ingmarstein@1791
   172
					bundleIdentifier = [bundle bundleIdentifier];
ingmarstein@1791
   173
ingmarstein@1791
   174
					if (bundleIdentifier && ([bundleIdentifier compare:GROWL_PREFPANE_BUNDLE_IDENTIFIER options:bundleIDComparisonFlags] == NSOrderedSame)) {
ingmarstein@1791
   175
						prefPaneBundle = bundle;
ingmarstein@1791
   176
						return prefPaneBundle;
ingmarstein@1791
   177
					}
ingmarstein@1791
   178
				}
ingmarstein@1791
   179
ingmarstein@1791
   180
				[bundleEnum skipDescendents];
ingmarstein@1791
   181
			}
ingmarstein@1791
   182
		}
ingmarstein@1791
   183
	}
ingmarstein@1791
   184
ingmarstein@1791
   185
	return nil;
ingmarstein@1791
   186
}
ingmarstein@1791
   187
boredzo@2677
   188
+ (NSBundle *) helperAppBundle {
ingmarstein@1791
   189
	if (!helperAppBundle) {
boredzo@4043
   190
		helperAppBundle = [self runningHelperAppBundle];
ingmarstein@2591
   191
		if (!helperAppBundle) {
ingmarstein@1791
   192
			//look in the prefpane bundle.
boredzo@2677
   193
			NSBundle *bundle = [GrowlPathUtilities growlPrefPaneBundle];
ingmarstein@1791
   194
			NSString *helperAppPath = [bundle pathForResource:@"GrowlHelperApp" ofType:@"app"];
ingmarstein@1791
   195
			helperAppBundle = [NSBundle bundleWithPath:helperAppPath];
ingmarstein@1791
   196
		}
ingmarstein@1791
   197
	}
ingmarstein@1791
   198
	return helperAppBundle;
ingmarstein@1791
   199
}
ingmarstein@1791
   200
ingmarstein@1907
   201
#pragma mark -
boredzo@2467
   202
#pragma mark Directories
boredzo@2467
   203
boredzo@2677
   204
+ (NSArray *) searchPathForDirectory:(GrowlSearchPathDirectory) directory inDomains:(GrowlSearchPathDomainMask) domainMask mustBeWritable:(BOOL)flag {
boredzo@2467
   205
	if (directory < GrowlSupportDirectory) {
boredzo@2467
   206
		NSArray *searchPath = NSSearchPathForDirectoriesInDomains(directory, domainMask, /*expandTilde*/ YES);
boredzo@2677
   207
		if (!flag)
boredzo@2467
   208
			return searchPath;
boredzo@2467
   209
		else {
boredzo@2467
   210
			//flag is not NO: exclude non-writable directories.
boredzo@2467
   211
			NSMutableArray *result = [NSMutableArray arrayWithCapacity:[searchPath count]];
boredzo@2467
   212
			NSFileManager *mgr = [NSFileManager defaultManager];
boredzo@2467
   213
boredzo@2467
   214
			NSEnumerator *searchPathEnum = [searchPath objectEnumerator];
boredzo@2467
   215
			NSString *dir;
boredzo@2677
   216
			while ((dir = [searchPathEnum nextObject])) {
ingmarstein@2470
   217
				if ([mgr isWritableFileAtPath:dir])
boredzo@2467
   218
					[result addObject:dir];
boredzo@2677
   219
			}
boredzo@2467
   220
boredzo@2467
   221
			return result;
boredzo@2467
   222
		}
boredzo@2467
   223
	} else {
boredzo@2467
   224
		//determine what to append to each Application Support folder.
boredzo@2467
   225
		NSString *subpath = nil;
boredzo@2467
   226
		switch (directory) {
boredzo@2467
   227
			case GrowlSupportDirectory:
boredzo@2467
   228
				//do nothing.
boredzo@2467
   229
				break;
boredzo@2467
   230
boredzo@2467
   231
			case GrowlScreenshotsDirectory:
boredzo@2467
   232
				subpath = NAME_OF_SCREENSHOTS_DIRECTORY;
boredzo@2467
   233
				break;
boredzo@2467
   234
boredzo@2467
   235
			case GrowlTicketsDirectory:
boredzo@2467
   236
				subpath = NAME_OF_TICKETS_DIRECTORY;
boredzo@2467
   237
				break;
boredzo@2467
   238
boredzo@2477
   239
			case GrowlPluginsDirectory:
boredzo@2477
   240
				subpath = NAME_OF_PLUGINS_DIRECTORY;
boredzo@2477
   241
				break;
boredzo@2477
   242
boredzo@2467
   243
			default:
boredzo@2467
   244
				NSLog(@"ERROR: GrowlPathUtil was asked for directory 0x%x, but it doesn't know what directory that is. Please tell the Growl developers.", directory);
boredzo@2467
   245
				return nil;
boredzo@2467
   246
		}
boredzo@2467
   247
		if (subpath)
boredzo@2467
   248
			subpath = [@"Application Support/Growl" stringByAppendingPathComponent:subpath];
boredzo@2467
   249
		else
boredzo@2467
   250
			subpath =  @"Application Support/Growl";
boredzo@2467
   251
boredzo@2467
   252
		/*get the search path, and append the subpath to all the items therein.
boredzo@2467
   253
		 *exclude results that don't exist.
boredzo@2467
   254
		 */
boredzo@2467
   255
		NSFileManager *mgr = [NSFileManager defaultManager];
boredzo@2467
   256
		BOOL isDir = NO;
boredzo@2467
   257
boredzo@2467
   258
		NSArray *searchPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, domainMask, /*expandTilde*/ YES);
boredzo@2467
   259
		NSMutableArray *mSearchPath = [NSMutableArray arrayWithCapacity:[searchPath count]];
boredzo@2467
   260
		NSEnumerator *searchPathEnum = [searchPath objectEnumerator];
boredzo@2467
   261
		NSString *path;
boredzo@2467
   262
		while ((path = [searchPathEnum nextObject])) {
boredzo@2467
   263
			path = [path stringByAppendingPathComponent:subpath];
ingmarstein@2470
   264
			if ([mgr fileExistsAtPath:path isDirectory:&isDir] && isDir)
boredzo@2467
   265
				[mSearchPath addObject:path];
boredzo@2467
   266
		}
boredzo@2467
   267
boredzo@2467
   268
		return mSearchPath;
boredzo@2467
   269
	}
boredzo@2467
   270
}
boredzo@2467
   271
boredzo@2677
   272
+ (NSArray *) searchPathForDirectory:(GrowlSearchPathDirectory) directory inDomains:(GrowlSearchPathDomainMask) domainMask {
boredzo@2467
   273
	//NO to emulate the default NSSearchPathForDirectoriesInDomains behaviour.
boredzo@2677
   274
	return [self searchPathForDirectory:directory inDomains:domainMask mustBeWritable:NO];
boredzo@2677
   275
}
boredzo@2677
   276
boredzo@2677
   277
+ (NSString *) growlSupportDirectory {
boredzo@2677
   278
	NSArray *searchPath = [self searchPathForDirectory:GrowlSupportDirectory inDomains:NSUserDomainMask mustBeWritable:YES];
ingmarstein@2470
   279
	if ([searchPath count])
boredzo@2467
   280
		return [searchPath objectAtIndex:0U];
boredzo@2467
   281
	else {
boredzo@2467
   282
		NSString *path = nil;
ingmarstein@2470
   283
boredzo@2467
   284
		//if this doesn't return any writable directories, path will still be nil.
boredzo@2677
   285
		searchPath = [self searchPathForDirectory:NSLibraryDirectory inDomains:NSAllDomainsMask mustBeWritable:YES];
ingmarstein@2470
   286
		if ([searchPath count]) {
ingmarstein@2472
   287
			path = [[searchPath objectAtIndex:0U] stringByAppendingPathComponent:@"Application Support/Growl"];
boredzo@2467
   288
			//try to create it. if that doesn't work, don't return it. return nil instead.
ingmarstein@2470
   289
			if (![[NSFileManager defaultManager] createDirectoryAtPath:path attributes:nil])
boredzo@2467
   290
				path = nil;
boredzo@2467
   291
		}
ingmarstein@2472
   292
boredzo@2467
   293
		return path;
boredzo@2467
   294
	}
boredzo@2467
   295
}
ingmarstein@1907
   296
boredzo@2677
   297
+ (NSString *) screenshotsDirectory {
boredzo@2677
   298
	NSArray *searchPath = [self searchPathForDirectory:GrowlScreenshotsDirectory inDomains:NSAllDomainsMask mustBeWritable:YES];
ingmarstein@2470
   299
	if ([searchPath count])
boredzo@2467
   300
		return [searchPath objectAtIndex:0U];
boredzo@2467
   301
	else {
boredzo@2467
   302
		NSString *path = nil;
boredzo@2467
   303
boredzo@2467
   304
		//if this doesn't return any writable directories, path will still be nil.
boredzo@2677
   305
		path = [self growlSupportDirectory];
ingmarstein@2472
   306
		if (path) {
ingmarstein@2472
   307
			path = [path stringByAppendingPathComponent:NAME_OF_SCREENSHOTS_DIRECTORY];
boredzo@2467
   308
			//try to create it. if that doesn't work, don't return it. return nil instead.
ingmarstein@2470
   309
			if (![[NSFileManager defaultManager] createDirectoryAtPath:path attributes:nil])
boredzo@2467
   310
				path = nil;
boredzo@2467
   311
		}
boredzo@2467
   312
boredzo@2467
   313
		return path;
boredzo@2467
   314
	}
boredzo@2467
   315
}
boredzo@2467
   316
boredzo@2677
   317
+ (NSString *) ticketsDirectory {
boredzo@2677
   318
	NSArray *searchPath = [self searchPathForDirectory:GrowlTicketsDirectory inDomains:NSAllDomainsMask mustBeWritable:YES];
ingmarstein@2470
   319
	if ([searchPath count])
boredzo@2467
   320
		return [searchPath objectAtIndex:0U];
boredzo@2467
   321
	else {
boredzo@2467
   322
		NSString *path = nil;
ingmarstein@2591
   323
boredzo@2467
   324
		//if this doesn't return any writable directories, path will still be nil.
boredzo@2677
   325
		path = [self growlSupportDirectory];
ingmarstein@2472
   326
		if (path) {
ingmarstein@2472
   327
			path = [path stringByAppendingPathComponent:NAME_OF_TICKETS_DIRECTORY];
boredzo@2467
   328
			//try to create it. if that doesn't work, don't return it. return nil instead.
ingmarstein@2470
   329
			if (![[NSFileManager defaultManager] createDirectoryAtPath:path attributes:nil])
boredzo@2467
   330
				path = nil;
boredzo@2467
   331
		}
ingmarstein@2591
   332
boredzo@2467
   333
		return path;
boredzo@2467
   334
	}
boredzo@2467
   335
}
boredzo@2467
   336
boredzo@2467
   337
#pragma mark -
boredzo@2467
   338
#pragma mark Screenshot names
ingmarstein@1907
   339
boredzo@2677
   340
+ (NSString *) nextScreenshotName {
boredzo@2677
   341
	return [self nextScreenshotNameInDirectory:nil];
boredzo@2677
   342
}
boredzo@2677
   343
boredzo@2677
   344
+ (NSString *) nextScreenshotNameInDirectory:(NSString *) directory {
ingmarstein@1907
   345
	NSFileManager *mgr = [NSFileManager defaultManager];
ingmarstein@1907
   346
boredzo@2467
   347
	if (!directory)
boredzo@2677
   348
		directory = [GrowlPathUtilities screenshotsDirectory];
boredzo@2467
   349
boredzo@2467
   350
	//build a set of all the files in the directory, without their filename extensions.
ingmarstein@1907
   351
	NSArray *origContents = [mgr directoryContentsAtPath:directory];
ingmarstein@1907
   352
	NSMutableSet *directoryContents = [[NSMutableSet alloc] initWithCapacity:[origContents count]];
ingmarstein@1907
   353
ingmarstein@1907
   354
	NSEnumerator *filesEnum = [origContents objectEnumerator];
ingmarstein@1907
   355
	NSString *existingFilename;
boredzo@2467
   356
	while ((existingFilename = [filesEnum nextObject]))
boredzo@2467
   357
		[directoryContents addObject:[existingFilename stringByDeletingPathExtension]];
boredzo@2467
   358
boredzo@2467
   359
	//look for a filename that doesn't exist (with any extension) in the directory.
boredzo@2467
   360
	NSString *filename = nil;
sdwilsh@3322
   361
	unsigned long long i;
sdwilsh@3323
   362
	for (i = 1ULL; i < ULLONG_MAX; ++i) {
ingmarstein@1907
   363
		[filename release];
boredzo@2467
   364
		filename = [[NSString alloc] initWithFormat:@"Screenshot %llu", i];
boredzo@2467
   365
		if (![directoryContents containsObject:filename])
ingmarstein@1907
   366
			break;
ingmarstein@1907
   367
	}
ingmarstein@1907
   368
	[directoryContents release];
ingmarstein@1907
   369
ingmarstein@1907
   370
	return [filename autorelease];
ingmarstein@1907
   371
}
ingmarstein@1907
   372
boredzo@2467
   373
#pragma mark -
boredzo@2467
   374
#pragma mark Tickets
boredzo@2467
   375
boredzo@2677
   376
+ (NSString *) defaultSavePathForTicketWithApplicationName:(NSString *) appName {
boredzo@2677
   377
	return [[self ticketsDirectory] stringByAppendingPathComponent:[appName stringByAppendingPathExtension:GROWL_PATHEXTENSION_TICKET]];
boredzo@2677
   378
}
boredzo@2677
   379
boredzo@2677
   380
@end