Commits

Anonymous committed ec06b36

Added a command to the File menu (just after Open Recent, in case you're curious) to import saved tickets from Growl. The menu item presents a panel from which you can choose one or more tickets to import; each ticket you choose to import will be opened as a new document. Fixes #621.

Comments (0)

Files changed (9)

Developer Tools/Growl Registration Dictionary Editor/English.lproj/GRDEImport.nib/classes.nib

+{
+    IBClasses = (
+        {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }, 
+        {
+            ACTIONS = {importSelectedTickets = id; orderFrontImportPanel = id; }; 
+            CLASS = GRDEImporter; 
+            LANGUAGE = ObjC; 
+            OUTLETS = {importPanel = NSPanel; ticketsTableView = NSTableView; }; 
+            SUPERCLASS = NSObject; 
+        }
+    ); 
+    IBVersion = 1; 
+}

Developer Tools/Growl Registration Dictionary Editor/English.lproj/GRDEImport.nib/info.nib

+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IBDocumentLocation</key>
+	<string>69 14 356 240 0 0 1280 1002 </string>
+	<key>IBFramework Version</key>
+	<string>446.1</string>
+	<key>IBOpenObjects</key>
+	<array>
+		<integer>5</integer>
+	</array>
+	<key>IBSystem Version</key>
+	<string>8R2232</string>
+</dict>
+</plist>

Developer Tools/Growl Registration Dictionary Editor/English.lproj/GRDEImport.nib/keyedobjects.nib

Binary file added.

Developer Tools/Growl Registration Dictionary Editor/English.lproj/MainMenu.nib/classes.nib

             LANGUAGE = ObjC; 
             OUTLETS = {menuItemToInsertWriteCodeSampleToPasteboardMenuItemsAfter = NSMenuItem; }; 
             SUPERCLASS = NSObject; 
+        }, 
+        {
+            ACTIONS = {orderFrontImportPanel = id; }; 
+            CLASS = GRDEImporter; 
+            LANGUAGE = ObjC; 
+            SUPERCLASS = NSObject; 
         }
     ); 
     IBVersion = 1; 

Developer Tools/Growl Registration Dictionary Editor/English.lproj/MainMenu.nib/info.nib

 <plist version="1.0">
 <dict>
 	<key>IBDocumentLocation</key>
-	<string>86 100 356 240 0 0 1280 1002 </string>
+	<string>77 99 356 240 0 0 1280 1002 </string>
 	<key>IBEditorPositions</key>
 	<dict>
 		<key>29</key>

Developer Tools/Growl Registration Dictionary Editor/English.lproj/MainMenu.nib/keyedobjects.nib

Binary file modified.

Developer Tools/Growl Registration Dictionary Editor/GRDEImporter.h

+//
+//  GRDEImporter.h
+//  Growl Registration Dictionary Editor
+//
+//  Created by Peter Hosey on 2007-10-01.
+//  Copyright 2007 Peter Hosey. All rights reserved.
+//
+
+@interface GRDEImporter : NSObject {
+	NSArray *ticketPaths;
+	NSIndexSet *selectedTicketIndices;
+	IBOutlet NSPanel *importPanel;
+	IBOutlet NSTableView *ticketsTableView; //There being no way to hook up a double-click action in IB 2…
+}
+
+#pragma mark Accessors
+
+- (NSArray *) ticketPaths;
+- (unsigned) countOfTicketPaths;
+- (NSString *) objectInTicketPathsAtIndex:(unsigned)idx;
+
+//Dependent on ticketPaths.
+- (NSArray *) ticketApplicationNames;
+- (unsigned) countOfTicketApplicationNames;
+- (NSObject *) objectInTicketApplicationNamesAtIndex:(unsigned)idx;
+
+- (NSIndexSet *) selectedTicketIndices;
+
+#pragma mark Actions
+
+- (IBAction) orderFrontImportPanel:(id)sender;
+- (IBAction) importSelectedTickets:(id)sender;
+
+@end

Developer Tools/Growl Registration Dictionary Editor/GRDEImporter.m

+//
+//  GRDEImporter.m
+//  Growl Registration Dictionary Editor
+//
+//  Created by Peter Hosey on 2007-10-01.
+//  Copyright 2007 Peter Hosey. All rights reserved.
+//
+
+#import "GRDEImporter.h"
+
+#import "NSString+FinderLikeSorting.h"
+
+@interface GRDEImporter (PrivateSetterAccessors)
+//These are not public properties, but we do have setter accessors for them in order to take advantage of free KVO notifications. We declare them here to suppress unknown-method warnings in the rest of the file.
+- (void) setTicketPaths:(NSArray *)newTicketPaths;
+- (void) setSelectedTicketIndices:(NSIndexSet *)newSelectedTicketIndices;
+@end
+
+@implementation GRDEImporter
+
+#pragma mark Private methods
+
+//Scan the Growl tickets folders. This method is not lazy nor does it cache.
+- (NSArray *) currentListOfTicketPaths {
+	NSFileManager *mgr = [NSFileManager defaultManager];
+	NSArray *libraryFolders = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask, /*expandTilde*/ YES);
+
+	NSMutableArray *paths = [NSMutableArray array];
+
+	NSEnumerator *librariesEnum = [libraryFolders objectEnumerator];
+	NSString *libraryPath;
+	while ((libraryPath = [librariesEnum nextObject])) {
+		NSString *ticketsFolderPath = [[[libraryPath stringByAppendingPathComponent:@"Application Support"] stringByAppendingPathComponent:@"Growl"] stringByAppendingPathComponent:@"Tickets"];
+		NSArray *filenames = [mgr directoryContentsAtPath:ticketsFolderPath];
+		//If the array is nil, the most likely reason is that $LIBRARY/Application Support/Growl/Tickets does not exist. That's fine.
+		if (!filenames) continue;
+
+		NSEnumerator *filenamesEnum = [filenames objectEnumerator];
+		NSString *name;
+		while ((name = [filenamesEnum nextObject])) {
+			//Don't add filenames that aren't actually tickets. Mostly, this is so we ignore .DS_Store.
+			if ([[name pathExtension] isEqualToString:@"growlTicket"])
+				[paths addObject:[ticketsFolderPath stringByAppendingPathComponent:name]];
+		}
+	}
+
+	//Sort the paths by filename, the way the Finder does it.
+	NSSortDescriptor *desc = [[NSSortDescriptor alloc] initWithKey:@"lastPathComponent" ascending:YES selector:@selector(finderCompare:)];
+	NSArray *sortedPaths = [paths sortedArrayUsingDescriptors:[NSArray arrayWithObject:desc]];
+	[desc release];
+
+	return sortedPaths;
+
+	/*Note: We *could* have autoreleased the descriptor and simply jumped to sortedArrayUsingDescriptors: (return [paths sortedArrayblahblahblah]). That's a tail call, which gets us tail-call optimization.
+	 *Unfortunately, TCO means omitting a stack frame, which is a pain during debugging. If the app crashes in sortedArrayUsingDescriptors:, that method would not show up in the stack trace. Take it from me: that's VERY confusing (“The next frame on the stack is -_xyzPrivateMethod, but we don't call -_xyzPrivateMethod here!”).
+	 *This method is called infrequently at most, so the 0.000000000000001 seconds (made-up number) that that would gain us aren't worth it.
+	 */
+}
+
+#pragma mark Birth and death
+
+- (id) init {
+	if((self = [super init])) {
+		[self setTicketPaths:[self currentListOfTicketPaths]];
+		[self setSelectedTicketIndices:[NSIndexSet indexSet]];
+		[NSBundle loadNibNamed:@"GRDEImport" owner:self];
+	}
+	return self;
+}
+
+- (void) awakeFromNib {
+	if (importPanel) {
+		[importPanel setLevel:NSModalPanelWindowLevel];
+	}
+	if (ticketsTableView) {
+		[ticketsTableView setTarget:self];
+		[ticketsTableView setDoubleAction:@selector(importSelectedTickets:)];
+	}
+}
+
+- (void) dealloc {
+	[ticketPaths release];
+	[importPanel close];
+
+	[super dealloc];
+}
+
+#pragma mark Accessors
+
+- (NSArray *) ticketPaths {
+	return ticketPaths;
+}
+- (void) setTicketPaths:(NSArray *)newTicketPaths {
+	if(ticketPaths != newTicketPaths) {
+		[ticketPaths release];
+		ticketPaths = [newTicketPaths copy];
+	}
+}
+
+- (unsigned) countOfTicketPaths {
+	return [ticketPaths count];
+}
+- (NSString *) objectInTicketPathsAtIndex:(unsigned)idx {
+	return [ticketPaths objectAtIndex:idx];
+}
+
+- (NSArray *) ticketApplicationNames {
+	return [ticketPaths valueForKeyPath:@"lastPathComponent.stringByDeletingPathExtension"];
+}
+- (unsigned) countOfTicketApplicationNames {
+	return [ticketPaths count];
+}
+- (NSObject *) objectInTicketApplicationNamesAtIndex:(unsigned)idx {
+	return [[[ticketPaths objectAtIndex:idx] lastPathComponent] stringByDeletingPathExtension];
+}
+
+- (NSIndexSet *) selectedTicketIndices {
+	return selectedTicketIndices;
+}
+- (void) setSelectedTicketIndices:(NSIndexSet *)newSelectedTicketIndices {
+	if(selectedTicketIndices != newSelectedTicketIndices) {
+		[selectedTicketIndices release];
+		selectedTicketIndices = [newSelectedTicketIndices retain];
+	}
+}
+
+#pragma mark User-interface validation
+
+- (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)item {
+	/*The only thing this object validates is the menu item to summon the import panel.
+	 *That menu item should be enabled whenever Growl has at least one saved ticket for us to import.
+	 */
+	return ([[self currentListOfTicketPaths] count] > 0U);
+}
+
+#pragma mark Actions
+
+- (IBAction) orderFrontImportPanel:(id)sender {
+	//First, make sure our array of ticket paths is up-to-date.
+	[self setTicketPaths:[self currentListOfTicketPaths]];
+	//While we're at it, empty our selection.
+	[self setSelectedTicketIndices:[NSIndexSet indexSet]];
+
+	//Now, really bring forth the panel.
+	[importPanel makeKeyAndOrderFront:sender];
+}
+
+- (IBAction) importSelectedTickets:(id)sender {
+	NSDocumentController *docController = [NSDocumentController sharedDocumentController];
+
+	for (unsigned idx = [selectedTicketIndices firstIndex]; idx <= [selectedTicketIndices lastIndex]; idx = [selectedTicketIndices indexGreaterThanIndex:idx]) {
+		NSString *path = [ticketPaths objectAtIndex:idx];
+		NSError *error = nil;
+
+		//COMPAT 10.4: Tiger's NSDocumentController doesn't return a useful NSError when the file doesn't exist. It should return NSFileReadNoSuchFileError; instead, it returns NSFileReadUnknownError. Thus, we must check for file-not-found errors ourselves.
+		BOOL isDir;
+		BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir];
+		if (!exists) {
+			error = [NSError errorWithDomain:NSCocoaErrorDomain
+										code:NSFileReadNoSuchFileError
+									userInfo:[NSDictionary dictionaryWithObject:path forKey:NSFilePathErrorKey]];
+		} else if (isDir) {
+			error = [NSError errorWithDomain:NSCocoaErrorDomain
+										code:NSFileReadCorruptFileError //Close enough…
+									userInfo:[NSDictionary dictionaryWithObject:path forKey:NSFilePathErrorKey]];
+		} else {
+			NSDocument *doc = [docController openDocumentWithContentsOfURL:[NSURL fileURLWithPath:path]
+																   display:YES
+																	 error:&error];
+			[[[[doc windowControllers] objectAtIndex:0U] window] makeKeyAndOrderFront:nil];
+		}
+
+		if (error) {
+			[importPanel presentError:error
+					   modalForWindow:importPanel
+							 delegate:self
+				   didPresentSelector:@selector(didPresentErrorWithRecovery:contextInfo:)
+						  contextInfo:NULL];
+			[NSApp runModalForWindow:[importPanel attachedSheet]];
+		}
+	}
+
+	//We're done, so hide the panel.
+	[importPanel orderOut:nil];
+}
+- (void)didPresentErrorWithRecovery:(BOOL)didRecover contextInfo:(void *)contextInfo {
+	//For some reason, when the error sheet finishes, Cocoa activates the frontmost document window. This reactivates the importer panel.
+	if ([importPanel isVisible])
+		[importPanel performSelector:@selector(makeKeyAndOrderFront:)
+						  withObject:nil
+						  afterDelay:0.01];
+	[NSApp stopModal];
+}
+
+@end

Developer Tools/Growl Registration Dictionary Editor/Growl Registration Dictionary Editor.xcodeproj/project.pbxproj

 		072DC3D509F25F2000B8D4EC /* AXCArrayControllerWithDragAndDrop.m in Sources */ = {isa = PBXBuildFile; fileRef = 072DC3D409F25F2000B8D4EC /* AXCArrayControllerWithDragAndDrop.m */; };
 		0752F6BA09F878A400032AAC /* NSMutableDictionary+Intersection.m in Sources */ = {isa = PBXBuildFile; fileRef = 0752F6B909F878A400032AAC /* NSMutableDictionary+Intersection.m */; };
 		31135DFC0CB13CDD002BD7DA /* NSString+FinderLikeSorting.m in Sources */ = {isa = PBXBuildFile; fileRef = 31135DFB0CB13CDD002BD7DA /* NSString+FinderLikeSorting.m */; };
+		3130C6E20CB1212600811B2B /* GRDEImporter.m in Sources */ = {isa = PBXBuildFile; fileRef = 3130C6E10CB1212600811B2B /* GRDEImporter.m */; };
+		3130C6F10CB1246D00811B2B /* GRDEImport.nib in Resources */ = {isa = PBXBuildFile; fileRef = 3130C6EF0CB1246D00811B2B /* GRDEImport.nib */; };
 		3181A5540CA87EDE006B6B5D /* Notify code samples in Resources */ = {isa = PBXBuildFile; fileRef = 3181A5500CA87EDE006B6B5D /* Notify code samples */; };
 		3181A5560CA896AB006B6B5D /* GRDECodeSampleIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 3181A5550CA896AB006B6B5D /* GRDECodeSampleIcon.icns */; };
 		3181A5610CA89D0B006B6B5D /* GRDEAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 3181A5600CA89D0B006B6B5D /* GRDEAppDelegate.m */; };
 		2A37F4C5FDCFA73011CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = "<absolute>"; };
 		31135DFA0CB13CDD002BD7DA /* NSString+FinderLikeSorting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+FinderLikeSorting.h"; sourceTree = "<group>"; };
 		31135DFB0CB13CDD002BD7DA /* NSString+FinderLikeSorting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+FinderLikeSorting.m"; sourceTree = "<group>"; };
+		3130C6E00CB1212600811B2B /* GRDEImporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRDEImporter.h; sourceTree = "<group>"; };
+		3130C6E10CB1212600811B2B /* GRDEImporter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRDEImporter.m; sourceTree = "<group>"; };
+		3130C6F00CB1246D00811B2B /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/GRDEImport.nib; sourceTree = "<group>"; };
 		3181A5500CA87EDE006B6B5D /* Notify code samples */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Notify code samples"; sourceTree = "<group>"; };
 		3181A5550CA896AB006B6B5D /* GRDECodeSampleIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; name = GRDECodeSampleIcon.icns; path = "GRDE Code Sample icon/GRDECodeSampleIcon.icns"; sourceTree = "<group>"; };
 		3181A55F0CA89D0B006B6B5D /* GRDEAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRDEAppDelegate.h; sourceTree = "<group>"; };
 			children = (
 				3181A55F0CA89D0B006B6B5D /* GRDEAppDelegate.h */,
 				3181A5600CA89D0B006B6B5D /* GRDEAppDelegate.m */,
+				3130C6E00CB1212600811B2B /* GRDEImporter.h */,
+				3130C6E10CB1212600811B2B /* GRDEImporter.m */,
 				2A37F4AEFDCFA73011CA2CEA /* GRDEDocument.h */,
 				2A37F4ACFDCFA73011CA2CEA /* GRDEDocument.m */,
 				072DC32809F21DD800B8D4EC /* GRDENotification.h */,
 				8D15AC360486D014006FF6A4 /* Info.plist */,
 				089C165FFE840EACC02AAC07 /* InfoPlist.strings */,
 				071A884E09F8C3B10061AFE5 /* Localizable.strings */,
+				3130C6EF0CB1246D00811B2B /* GRDEImport.nib */,
 			);
 			name = Resources;
 			sourceTree = "<group>";
 				071A885009F8C3B10061AFE5 /* Localizable.strings in Resources */,
 				3181A5540CA87EDE006B6B5D /* Notify code samples in Resources */,
 				3181A5560CA896AB006B6B5D /* GRDECodeSampleIcon.icns in Resources */,
+				3130C6F10CB1246D00811B2B /* GRDEImport.nib in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
 				072DC3D509F25F2000B8D4EC /* AXCArrayControllerWithDragAndDrop.m in Sources */,
 				0752F6BA09F878A400032AAC /* NSMutableDictionary+Intersection.m in Sources */,
 				3181A5610CA89D0B006B6B5D /* GRDEAppDelegate.m in Sources */,
+				3130C6E20CB1212600811B2B /* GRDEImporter.m in Sources */,
 				31135DFC0CB13CDD002BD7DA /* NSString+FinderLikeSorting.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			name = Credits.rtf;
 			sourceTree = "<group>";
 		};
+		3130C6EF0CB1246D00811B2B /* GRDEImport.nib */ = {
+			isa = PBXVariantGroup;
+			children = (
+				3130C6F00CB1246D00811B2B /* English */,
+			);
+			name = GRDEImport.nib;
+			sourceTree = "<group>";
+		};
 /* End PBXVariantGroup section */
 
 /* Begin XCBuildConfiguration section */