Extras/GrowlSafari/GrowlSafari.m
author Peter Hosey <hg@boredzo.org>
Thu Jul 02 06:31:09 2009 -0700 (2009-07-02)
changeset 4226 a6eb28fcc0b7
parent 4225 22ffbf7ae378
child 4237 d092b0dd14a5
permissions -rw-r--r--
Fix GrowlSafari under Safari 4 by updating the download stage numbers. For simplicity and ease of fixage, I've ripped out all the Safari 2 and 3 support—GrowlSafari requires Safari 4 now. For clarity, I've also added an enumeration to name all the stages.
tick@794
     1
/*
ingmarstein@1568
     2
 Copyright (c) The Growl Project, 2004-2005
tick@794
     3
 All rights reserved.
ingmarstein@1905
     4
ingmarstein@1905
     5
tick@794
     6
 Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
ingmarstein@1905
     7
ingmarstein@1905
     8
tick@794
     9
 1. Redistributions of source code must retain the above copyright
tick@794
    10
 notice, this list of conditions and the following disclaimer.
tick@794
    11
 2. Redistributions in binary form must reproduce the above copyright
tick@794
    12
 notice, this list of conditions and the following disclaimer in the
tick@794
    13
 documentation and/or other materials provided with the distribution.
tick@794
    14
 3. Neither the name of Growl nor the names of its contributors
tick@794
    15
 may be used to endorse or promote products derived from this software
tick@794
    16
 without specific prior written permission.
ingmarstein@1905
    17
ingmarstein@1905
    18
tick@794
    19
 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
ingmarstein@1905
    20
tick@794
    21
 */
tick@794
    22
aranor@568
    23
//
aranor@568
    24
//  GrowlSafari.m
aranor@568
    25
//  GrowlSafari
aranor@568
    26
//
aranor@568
    27
//  Created by Kevin Ballard on 10/29/04.
aranor@568
    28
//  Copyright 2004 Kevin Ballard. All rights reserved.
aranor@568
    29
//
aranor@568
    30
aranor@568
    31
#import "GrowlSafari.h"
ingmarstein@2112
    32
#import "GSWebBookmark.h"
ingmarstein@1943
    33
#import <Growl/Growl.h>
aranor@568
    34
#import <objc/objc-runtime.h>
aranor@568
    35
ingmarstein@2162
    36
hg@4226
    37
#define SAFARI_VERSION_4_0  5530
hg@4226
    38
hg@4226
    39
enum {
hg@4226
    40
	GrowlSafariDownloadStageActive = 1,
hg@4226
    41
	GrowlSafariDownloadStageDecompressing = 2,
hg@4226
    42
	GrowlSafariDownloadStageDiskImagePreparing = 4,
hg@4226
    43
	GrowlSafariDownloadStageDiskImageVerifying = 7,
hg@4226
    44
	GrowlSafariDownloadStageDiskImageVerified = 8,
hg@4226
    45
	GrowlSafariDownloadStageDiskImageMounting = 9,
hg@4226
    46
	GrowlSafariDownloadStageDiskImageCleanup = 12,
hg@4226
    47
	GrowlSafariDownloadStageInactive = 13,
hg@4226
    48
	GrowlSafariDownloadStageFinished = 14
hg@4226
    49
};
ingmarstein@2162
    50
ingmarstein@2162
    51
// How long should we wait (in seconds) before it's a long download?
ingmarstein@2162
    52
static double longDownload = 15.0;
ingmarstein@2162
    53
static int safariVersion;
ingmarstein@2162
    54
static NSMutableDictionary *dates = nil;
ingmarstein@2162
    55
aranor@568
    56
// Using method swizzling as outlined here:
aranor@568
    57
// http://www.cocoadev.com/index.pl?MethodSwizzling
aranor@568
    58
// A couple of modifications made to support swizzling class methods
aranor@568
    59
ingmarstein@1764
    60
static BOOL PerformSwizzle(Class aClass, SEL orig_sel, SEL alt_sel, BOOL forInstance) {
aranor@568
    61
    // First, make sure the class isn't nil
ingmarstein@1469
    62
	if (aClass) {
aranor@568
    63
		Method orig_method = nil, alt_method = nil;
ingmarstein@1469
    64
aranor@568
    65
		// Next, look for the methods
eevyl@590
    66
		if (forInstance) {
eevyl@590
    67
			orig_method = class_getInstanceMethod(aClass, orig_sel);
eevyl@590
    68
			alt_method = class_getInstanceMethod(aClass, alt_sel);
aranor@568
    69
		} else {
eevyl@590
    70
			orig_method = class_getClassMethod(aClass, orig_sel);
eevyl@590
    71
			alt_method = class_getClassMethod(aClass, alt_sel);
aranor@568
    72
		}
ingmarstein@1472
    73
aranor@568
    74
		// If both are found, swizzle them
ingmarstein@1469
    75
		if (orig_method && alt_method) {
aranor@568
    76
			IMP temp;
ingmarstein@1472
    77
aranor@568
    78
			temp = orig_method->method_imp;
aranor@568
    79
			orig_method->method_imp = alt_method->method_imp;
aranor@568
    80
			alt_method->method_imp = temp;
ingmarstein@1473
    81
ingmarstein@1473
    82
			return YES;
aranor@568
    83
		} else {
aranor@568
    84
			// This bit stolen from SubEthaFari's source
boredzo@1788
    85
			NSLog(@"GrowlSafari Error: Original (selector %s) %@, Alternate (selector %s) %@",
diggory@1810
    86
				  orig_sel,
diggory@1810
    87
				  orig_method ? @"was found" : @"not found",
diggory@1810
    88
				  alt_sel,
diggory@1810
    89
				  alt_method ? @"was found" : @"not found");
aranor@568
    90
		}
aranor@568
    91
	} else {
boredzo@1788
    92
		NSLog(@"%@", @"GrowlSafari Error: No class to swizzle methods in");
aranor@568
    93
	}
ingmarstein@1473
    94
ingmarstein@1473
    95
	return NO;
aranor@568
    96
}
aranor@568
    97
ingmarstein@1764
    98
static void setDownloadStarted(id dl) {
ingmarstein@1764
    99
	if (!dates)
ingmarstein@1764
   100
		dates = [[NSMutableDictionary alloc] init];
ingmarstein@1764
   101
ingmarstein@2162
   102
	[dates setObject:[NSDate date] forKey:[dl identifier]];
ingmarstein@1764
   103
}
ingmarstein@1764
   104
ingmarstein@1764
   105
static NSDate *dateStarted(id dl) {
ingmarstein@1764
   106
	if (dates)
ingmarstein@2162
   107
		return [dates objectForKey:[dl identifier]];
ingmarstein@1764
   108
ingmarstein@1764
   109
	return nil;
ingmarstein@1764
   110
}
ingmarstein@1764
   111
ingmarstein@1764
   112
static BOOL isLongDownload(id dl) {
ingmarstein@1764
   113
	NSDate *date = dateStarted(dl);
ingmarstein@1764
   114
	return (date && -[date timeIntervalSinceNow] > longDownload);
ingmarstein@1764
   115
}
ingmarstein@1764
   116
ingmarstein@1764
   117
static void setDownloadFinished(id dl) {
hg@4225
   118
	[dates removeObjectForKey:[dl identifier]];
ingmarstein@1764
   119
}
aranor@900
   120
aranor@568
   121
@implementation GrowlSafari
ingmarstein@1764
   122
+ (NSBundle *) bundle {
ingmarstein@2324
   123
	return [NSBundle bundleWithIdentifier:@"com.growl.GrowlSafari"];
ingmarstein@1764
   124
}
ingmarstein@1764
   125
ingmarstein@2113
   126
+ (NSString *) bundleVersion {
ingmarstein@2113
   127
	return [[[GrowlSafari bundle] infoDictionary] objectForKey:(NSString *)kCFBundleVersionKey];
ingmarstein@2113
   128
}
ingmarstein@2113
   129
ingmarstein@1764
   130
+ (void) initialize {
ingmarstein@1943
   131
	NSString *growlPath = [[[GrowlSafari bundle] privateFrameworksPath] stringByAppendingPathComponent:@"Growl.framework"];
ingmarstein@1943
   132
	NSBundle *growlBundle = [NSBundle bundleWithPath:growlPath];
ingmarstein@2103
   133
ingmarstein@2212
   134
	if (growlBundle && [growlBundle load]) {
ingmarstein@1943
   135
		// Register ourselves as a Growl delegate
ingmarstein@2103
   136
		[GrowlApplicationBridge setGrowlDelegate:self];
ingmarstein@2298
   137
evands@4077
   138
		safariVersion = [[[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleVersionKey] intValue];
evands@4077
   139
		//  NSLog(@"%d",safariVersion);
evands@4077
   140
hg@4226
   141
		if (safariVersion >= SAFARI_VERSION_4_0) {
hg@4226
   142
			//	NSLog(@"Patching DownloadProgressEntry...");
hg@4226
   143
			Class class = NSClassFromString(@"DownloadProgressEntry");
hg@4226
   144
			PerformSwizzle(class, @selector(setDownloadStage:), @selector(mySetDownloadStage:), YES);
hg@4226
   145
					
evands@4077
   146
			PerformSwizzle(class, @selector(_updateDiskImageStatus:), @selector(myUpdateDiskImageStatus:), YES);
hg@4226
   147
			
hg@4226
   148
			PerformSwizzle(class, @selector(initWithDownload:mayOpenWhenDone:allowOverwrite:),
hg@4226
   149
						   @selector(myInitWithDownload:mayOpenWhenDone:allowOverwrite:),
hg@4226
   150
						   YES);
hg@4226
   151
			
hg@4226
   152
			Class webBookmarkClass = NSClassFromString(@"WebBookmark");
hg@4226
   153
			if (webBookmarkClass)
hg@4226
   154
				[[GSWebBookmark class] poseAsClass:webBookmarkClass];
hg@4226
   155
hg@4226
   156
			NSLog(@"Loaded GrowlSafari %@", [GrowlSafari bundleVersion]);
hg@4226
   157
			NSDictionary *infoDictionary = [GrowlApplicationBridge frameworkInfoDictionary];
hg@4226
   158
			NSLog(@"Using Growl.framework %@ (%@)",
hg@4226
   159
				  [infoDictionary objectForKey:@"CFBundleShortVersionString"],
hg@4226
   160
				  [infoDictionary objectForKey:(NSString *)kCFBundleVersionKey]);
hg@4226
   161
		} else {
hg@4226
   162
			NSLog(@"Safari too old (4.0 required); GrowlSafari disabled.");
hg@4226
   163
		}
ingmarstein@2212
   164
	} else {
ingmarstein@2212
   165
		NSLog(@"Could not load Growl.framework, GrowlSafari disabled");
ingmarstein@1943
   166
	}
evands@4077
   167
	
ingmarstein@1943
   168
}
ingmarstein@1943
   169
ingmarstein@1943
   170
#pragma mark GrowlApplicationBridge delegate methods
ingmarstein@1943
   171
ingmarstein@1943
   172
+ (NSString *) applicationNameForGrowl {
ingmarstein@1943
   173
	return @"GrowlSafari";
ingmarstein@1943
   174
}
ingmarstein@1943
   175
ingmarstein@2245
   176
+ (NSImage *) applicationIconForGrowl {
ingmarstein@2324
   177
	return [NSImage imageNamed:@"NSApplicationIcon"];
ingmarstein@1943
   178
}
ingmarstein@1943
   179
ingmarstein@1943
   180
+ (NSDictionary *) registrationDictionaryForGrowl {
ingmarstein@642
   181
	NSBundle *bundle = [GrowlSafari bundle];
ingmarstein@1764
   182
	NSArray *array = [[NSArray alloc] initWithObjects:
ingmarstein@1764
   183
		NSLocalizedStringFromTableInBundle(@"Short Download Complete", nil, bundle, @""),
ingmarstein@642
   184
		NSLocalizedStringFromTableInBundle(@"Download Complete", nil, bundle, @""),
ingmarstein@642
   185
		NSLocalizedStringFromTableInBundle(@"Disk Image Status", nil, bundle, @""),
ingmarstein@642
   186
		NSLocalizedStringFromTableInBundle(@"Compression Status", nil, bundle, @""),
ingmarstein@2112
   187
		NSLocalizedStringFromTableInBundle(@"New feed entry", nil, bundle, @""),
ingmarstein@611
   188
		nil];
aranor@568
   189
	NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
aranor@568
   190
		array, GROWL_NOTIFICATIONS_DEFAULT,
aranor@568
   191
		array, GROWL_NOTIFICATIONS_ALL,
aranor@568
   192
		nil];
ingmarstein@1764
   193
	[array release];
ingmarstein@1943
   194
ingmarstein@1943
   195
	return dict;
aranor@568
   196
}
ingmarstein@2112
   197
ingmarstein@2112
   198
+ (void) growlNotificationWasClicked:(id)clickContext {
ingmarstein@2112
   199
	NSURL *url = [[NSURL alloc] initWithString:clickContext];
ingmarstein@2112
   200
	[[NSWorkspace sharedWorkspace] openURL:url];
ingmarstein@2112
   201
	[url release];
ingmarstein@2112
   202
}
ingmarstein@2112
   203
ingmarstein@2245
   204
+ (void) notifyRSSUpdate:(WebBookmark *)bookmark newEntries:(int)newEntries {
ingmarstein@2112
   205
	NSBundle *bundle = [GrowlSafari bundle];
ingmarstein@2113
   206
	NSMutableString	*description = [[NSMutableString alloc]
ingmarstein@2113
   207
		initWithFormat:newEntries == 1 ? NSLocalizedStringFromTableInBundle(@"%d new entry", nil, bundle, @"") : NSLocalizedStringFromTableInBundle(@"%d new entries", nil, bundle, @""),
ingmarstein@2113
   208
		newEntries,
ingmarstein@2112
   209
		[bookmark unreadRSSCount]];
ingmarstein@2112
   210
	if (newEntries != [bookmark unreadRSSCount])
ingmarstein@2113
   211
		[description appendFormat:NSLocalizedStringFromTableInBundle(@" (%d unread)", nil, bundle, @""), [bookmark unreadRSSCount]];
ingmarstein@2112
   212
ingmarstein@2112
   213
	NSString *title = [bookmark title];
ingmarstein@2112
   214
	[GrowlApplicationBridge notifyWithTitle:(title ? title : [bookmark URLString])
ingmarstein@2112
   215
								description:description
ingmarstein@2112
   216
						   notificationName:NSLocalizedStringFromTableInBundle(@"New feed entry", nil, bundle, @"")
evands@4077
   217
								   iconData:nil
ingmarstein@2112
   218
								   priority:0
ingmarstein@2112
   219
								   isSticky:NO
ingmarstein@2112
   220
							   clickContext:[bookmark URLString]];
ingmarstein@2113
   221
	[description release];
ingmarstein@2112
   222
}
aranor@568
   223
@end
aranor@568
   224
aranor@568
   225
@implementation NSObject (GrowlSafariPatch)
ingmarstein@1764
   226
- (void) mySetDownloadStage:(int)stage {
hg@4226
   227
	//int oldStage = [self downloadStage];
evands@4077
   228
	
ingmarstein@2162
   229
	//NSLog(@"mySetDownloadStage:%d -> %d", oldStage, stage);
aranor@568
   230
	[self mySetDownloadStage:stage];
ingmarstein@1764
   231
	if (dateStarted(self)) {
hg@4226
   232
		if ( stage == GrowlSafariDownloadStageDecompressing ) {
ingmarstein@1943
   233
			NSBundle *bundle = [GrowlSafari bundle];
ingmarstein@2162
   234
			NSString *description = [[NSString alloc] initWithFormat:
evands@4077
   235
				NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, @""),
evands@4077
   236
				[[self gsDownloadPath] lastPathComponent]];
ingmarstein@2103
   237
			[GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Decompressing File", nil, bundle, @"")
ingmarstein@2162
   238
										description:description
ingmarstein@1943
   239
								   notificationName:NSLocalizedStringFromTableInBundle(@"Compression Status", nil, bundle, @"")
ingmarstein@1943
   240
										   iconData:nil
ingmarstein@1943
   241
										   priority:0
ingmarstein@1943
   242
										   isSticky:NO
ingmarstein@1943
   243
									   clickContext:nil];
ingmarstein@2162
   244
			[description release];
hg@4226
   245
		} else if ( stage == GrowlSafariDownloadStageDiskImageVerifying ) {
ingmarstein@1943
   246
			NSBundle *bundle = [GrowlSafari bundle];
ingmarstein@2162
   247
			NSString *description = [[NSString alloc] initWithFormat:
hg@4226
   248
									 NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, @""),
hg@4226
   249
									 [[self gsDownloadPath] lastPathComponent]];
hg@4226
   250
			[GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Verifying Disk Image", nil, bundle, @"")
ingmarstein@2162
   251
										description:description
ingmarstein@1943
   252
								   notificationName:NSLocalizedStringFromTableInBundle(@"Disk Image Status", nil, bundle, @"")
ingmarstein@1943
   253
										   iconData:nil
ingmarstein@1943
   254
										   priority:0
ingmarstein@1943
   255
										   isSticky:NO
ingmarstein@1943
   256
									   clickContext:nil];
ingmarstein@2162
   257
			[description release];
hg@4226
   258
		} else if ( stage == GrowlSafariDownloadStageFinished ) {
aranor@900
   259
			NSBundle *bundle = [GrowlSafari bundle];
ingmarstein@1764
   260
			NSString *notificationName = isLongDownload(self) ? NSLocalizedStringFromTableInBundle(@"Download Complete", nil, bundle, @"") : NSLocalizedStringFromTableInBundle(@"Short Download Complete", nil, bundle, @"");
ingmarstein@1764
   261
			setDownloadFinished(self);
ingmarstein@2162
   262
			NSString *description = [[NSString alloc] initWithFormat:
evands@4077
   263
				NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, "Message shown when a download is complete, where %@ becomes the filename"),
ingmarstein@2162
   264
				[self filename]];
ingmarstein@2103
   265
			[GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Download Complete", nil, bundle, @"")
ingmarstein@2162
   266
										description:description
ingmarstein@1943
   267
								   notificationName:notificationName
ingmarstein@1943
   268
										   iconData:nil
ingmarstein@1943
   269
										   priority:0
ingmarstein@1943
   270
										   isSticky:NO
ingmarstein@1943
   271
									   clickContext:nil];
ingmarstein@2162
   272
			[description release];
aranor@900
   273
		}
hg@4226
   274
	} else if (stage == GrowlSafariDownloadStageActive) {
ingmarstein@1764
   275
		setDownloadStarted(self);
ingmarstein@1764
   276
	}
ingmarstein@1764
   277
}
ingmarstein@1764
   278
ingmarstein@1764
   279
- (void) myUpdateDiskImageStatus:(NSDictionary *)status {
ingmarstein@2162
   280
	int oldStage = [self downloadStage];
ingmarstein@642
   281
	[self myUpdateDiskImageStatus:status];
ingmarstein@2162
   282
	//NSLog(@"myUpdateDiskImageStatus:%@ stage=%d -> %d", status, oldStage, [self downloadStage]);
ingmarstein@2162
   283
ingmarstein@2162
   284
	if (dateStarted(self)
hg@4226
   285
			&& oldStage == GrowlSafariDownloadStageDiskImageVerified
hg@4226
   286
			&& [self downloadStage] == GrowlSafariDownloadStageDiskImageMounting
ingmarstein@2162
   287
			&& [[status objectForKey:@"status-stage"] isEqualToString:@"attach"]) {
ingmarstein@2162
   288
		NSBundle *bundle = [GrowlSafari bundle];
ingmarstein@2162
   289
		NSString *description = [[NSString alloc] initWithFormat:
evands@4077
   290
			NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, @""),
evands@4077
   291
			[[self gsDownloadPath] lastPathComponent]];
ingmarstein@2162
   292
		[GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Mounting Disk Image", nil, bundle, @"")
ingmarstein@2162
   293
									description:description
ingmarstein@2162
   294
							   notificationName:NSLocalizedStringFromTableInBundle(@"Disk Image Status", nil, bundle, @"")
ingmarstein@2162
   295
									   iconData:nil
ingmarstein@2162
   296
									   priority:0
ingmarstein@2162
   297
									   isSticky:NO
ingmarstein@2162
   298
								   clickContext:nil];
ingmarstein@2162
   299
		[description release];
aranor@568
   300
	}
aranor@568
   301
}
aranor@900
   302
aranor@900
   303
// This is to make sure we're done with the pre-saved downloads
ingmarstein@1764
   304
- (id) myInitWithDownload:(id)fp8 mayOpenWhenDone:(BOOL)fp12 allowOverwrite:(BOOL)fp16 {
ingmarstein@1764
   305
	id retval = [self myInitWithDownload:fp8 mayOpenWhenDone:fp12 allowOverwrite:fp16];
ingmarstein@1764
   306
	setDownloadStarted(self);
ingmarstein@1764
   307
	return retval;
ingmarstein@1473
   308
}
evands@4077
   309
evands@4077
   310
- (NSString*) gsDownloadPath {
hg@4226
   311
	return [self currentPath];
evands@4077
   312
}
evands@4077
   313
aranor@568
   314
@end