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.
2 Copyright (c) The Growl Project, 2004-2005
6 Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
9 1. Redistributions of source code must retain the above copyright
10 notice, this list of conditions and the following disclaimer.
11 2. Redistributions in binary form must reproduce the above copyright
12 notice, this list of conditions and the following disclaimer in the
13 documentation and/or other materials provided with the distribution.
14 3. Neither the name of Growl nor the names of its contributors
15 may be used to endorse or promote products derived from this software
16 without specific prior written permission.
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.
27 // Created by Kevin Ballard on 10/29/04.
28 // Copyright 2004 Kevin Ballard. All rights reserved.
31 #import "GrowlSafari.h"
32 #import "GSWebBookmark.h"
33 #import <Growl/Growl.h>
34 #import <objc/objc-runtime.h>
37 #define SAFARI_VERSION_4_0 5530
40 GrowlSafariDownloadStageActive = 1,
41 GrowlSafariDownloadStageDecompressing = 2,
42 GrowlSafariDownloadStageDiskImagePreparing = 4,
43 GrowlSafariDownloadStageDiskImageVerifying = 7,
44 GrowlSafariDownloadStageDiskImageVerified = 8,
45 GrowlSafariDownloadStageDiskImageMounting = 9,
46 GrowlSafariDownloadStageDiskImageCleanup = 12,
47 GrowlSafariDownloadStageInactive = 13,
48 GrowlSafariDownloadStageFinished = 14
51 // How long should we wait (in seconds) before it's a long download?
52 static double longDownload = 15.0;
53 static int safariVersion;
54 static NSMutableDictionary *dates = nil;
56 // Using method swizzling as outlined here:
57 // http://www.cocoadev.com/index.pl?MethodSwizzling
58 // A couple of modifications made to support swizzling class methods
60 static BOOL PerformSwizzle(Class aClass, SEL orig_sel, SEL alt_sel, BOOL forInstance) {
61 // First, make sure the class isn't nil
63 Method orig_method = nil, alt_method = nil;
65 // Next, look for the methods
67 orig_method = class_getInstanceMethod(aClass, orig_sel);
68 alt_method = class_getInstanceMethod(aClass, alt_sel);
70 orig_method = class_getClassMethod(aClass, orig_sel);
71 alt_method = class_getClassMethod(aClass, alt_sel);
74 // If both are found, swizzle them
75 if (orig_method && alt_method) {
78 temp = orig_method->method_imp;
79 orig_method->method_imp = alt_method->method_imp;
80 alt_method->method_imp = temp;
84 // This bit stolen from SubEthaFari's source
85 NSLog(@"GrowlSafari Error: Original (selector %s) %@, Alternate (selector %s) %@",
87 orig_method ? @"was found" : @"not found",
89 alt_method ? @"was found" : @"not found");
92 NSLog(@"%@", @"GrowlSafari Error: No class to swizzle methods in");
98 static void setDownloadStarted(id dl) {
100 dates = [[NSMutableDictionary alloc] init];
102 [dates setObject:[NSDate date] forKey:[dl identifier]];
105 static NSDate *dateStarted(id dl) {
107 return [dates objectForKey:[dl identifier]];
112 static BOOL isLongDownload(id dl) {
113 NSDate *date = dateStarted(dl);
114 return (date && -[date timeIntervalSinceNow] > longDownload);
117 static void setDownloadFinished(id dl) {
118 [dates removeObjectForKey:[dl identifier]];
121 @implementation GrowlSafari
122 + (NSBundle *) bundle {
123 return [NSBundle bundleWithIdentifier:@"com.growl.GrowlSafari"];
126 + (NSString *) bundleVersion {
127 return [[[GrowlSafari bundle] infoDictionary] objectForKey:(NSString *)kCFBundleVersionKey];
130 + (void) initialize {
131 NSString *growlPath = [[[GrowlSafari bundle] privateFrameworksPath] stringByAppendingPathComponent:@"Growl.framework"];
132 NSBundle *growlBundle = [NSBundle bundleWithPath:growlPath];
134 if (growlBundle && [growlBundle load]) {
135 // Register ourselves as a Growl delegate
136 [GrowlApplicationBridge setGrowlDelegate:self];
138 safariVersion = [[[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleVersionKey] intValue];
139 // NSLog(@"%d",safariVersion);
141 if (safariVersion >= SAFARI_VERSION_4_0) {
142 // NSLog(@"Patching DownloadProgressEntry...");
143 Class class = NSClassFromString(@"DownloadProgressEntry");
144 PerformSwizzle(class, @selector(setDownloadStage:), @selector(mySetDownloadStage:), YES);
146 PerformSwizzle(class, @selector(_updateDiskImageStatus:), @selector(myUpdateDiskImageStatus:), YES);
148 PerformSwizzle(class, @selector(initWithDownload:mayOpenWhenDone:allowOverwrite:),
149 @selector(myInitWithDownload:mayOpenWhenDone:allowOverwrite:),
152 Class webBookmarkClass = NSClassFromString(@"WebBookmark");
153 if (webBookmarkClass)
154 [[GSWebBookmark class] poseAsClass:webBookmarkClass];
156 NSLog(@"Loaded GrowlSafari %@", [GrowlSafari bundleVersion]);
157 NSDictionary *infoDictionary = [GrowlApplicationBridge frameworkInfoDictionary];
158 NSLog(@"Using Growl.framework %@ (%@)",
159 [infoDictionary objectForKey:@"CFBundleShortVersionString"],
160 [infoDictionary objectForKey:(NSString *)kCFBundleVersionKey]);
162 NSLog(@"Safari too old (4.0 required); GrowlSafari disabled.");
165 NSLog(@"Could not load Growl.framework, GrowlSafari disabled");
170 #pragma mark GrowlApplicationBridge delegate methods
172 + (NSString *) applicationNameForGrowl {
173 return @"GrowlSafari";
176 + (NSImage *) applicationIconForGrowl {
177 return [NSImage imageNamed:@"NSApplicationIcon"];
180 + (NSDictionary *) registrationDictionaryForGrowl {
181 NSBundle *bundle = [GrowlSafari bundle];
182 NSArray *array = [[NSArray alloc] initWithObjects:
183 NSLocalizedStringFromTableInBundle(@"Short Download Complete", nil, bundle, @""),
184 NSLocalizedStringFromTableInBundle(@"Download Complete", nil, bundle, @""),
185 NSLocalizedStringFromTableInBundle(@"Disk Image Status", nil, bundle, @""),
186 NSLocalizedStringFromTableInBundle(@"Compression Status", nil, bundle, @""),
187 NSLocalizedStringFromTableInBundle(@"New feed entry", nil, bundle, @""),
189 NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
190 array, GROWL_NOTIFICATIONS_DEFAULT,
191 array, GROWL_NOTIFICATIONS_ALL,
198 + (void) growlNotificationWasClicked:(id)clickContext {
199 NSURL *url = [[NSURL alloc] initWithString:clickContext];
200 [[NSWorkspace sharedWorkspace] openURL:url];
204 + (void) notifyRSSUpdate:(WebBookmark *)bookmark newEntries:(int)newEntries {
205 NSBundle *bundle = [GrowlSafari bundle];
206 NSMutableString *description = [[NSMutableString alloc]
207 initWithFormat:newEntries == 1 ? NSLocalizedStringFromTableInBundle(@"%d new entry", nil, bundle, @"") : NSLocalizedStringFromTableInBundle(@"%d new entries", nil, bundle, @""),
209 [bookmark unreadRSSCount]];
210 if (newEntries != [bookmark unreadRSSCount])
211 [description appendFormat:NSLocalizedStringFromTableInBundle(@" (%d unread)", nil, bundle, @""), [bookmark unreadRSSCount]];
213 NSString *title = [bookmark title];
214 [GrowlApplicationBridge notifyWithTitle:(title ? title : [bookmark URLString])
215 description:description
216 notificationName:NSLocalizedStringFromTableInBundle(@"New feed entry", nil, bundle, @"")
220 clickContext:[bookmark URLString]];
221 [description release];
225 @implementation NSObject (GrowlSafariPatch)
226 - (void) mySetDownloadStage:(int)stage {
227 //int oldStage = [self downloadStage];
229 //NSLog(@"mySetDownloadStage:%d -> %d", oldStage, stage);
230 [self mySetDownloadStage:stage];
231 if (dateStarted(self)) {
232 if ( stage == GrowlSafariDownloadStageDecompressing ) {
233 NSBundle *bundle = [GrowlSafari bundle];
234 NSString *description = [[NSString alloc] initWithFormat:
235 NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, @""),
236 [[self gsDownloadPath] lastPathComponent]];
237 [GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Decompressing File", nil, bundle, @"")
238 description:description
239 notificationName:NSLocalizedStringFromTableInBundle(@"Compression Status", nil, bundle, @"")
244 [description release];
245 } else if ( stage == GrowlSafariDownloadStageDiskImageVerifying ) {
246 NSBundle *bundle = [GrowlSafari bundle];
247 NSString *description = [[NSString alloc] initWithFormat:
248 NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, @""),
249 [[self gsDownloadPath] lastPathComponent]];
250 [GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Verifying Disk Image", nil, bundle, @"")
251 description:description
252 notificationName:NSLocalizedStringFromTableInBundle(@"Disk Image Status", nil, bundle, @"")
257 [description release];
258 } else if ( stage == GrowlSafariDownloadStageFinished ) {
259 NSBundle *bundle = [GrowlSafari bundle];
260 NSString *notificationName = isLongDownload(self) ? NSLocalizedStringFromTableInBundle(@"Download Complete", nil, bundle, @"") : NSLocalizedStringFromTableInBundle(@"Short Download Complete", nil, bundle, @"");
261 setDownloadFinished(self);
262 NSString *description = [[NSString alloc] initWithFormat:
263 NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, "Message shown when a download is complete, where %@ becomes the filename"),
265 [GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Download Complete", nil, bundle, @"")
266 description:description
267 notificationName:notificationName
272 [description release];
274 } else if (stage == GrowlSafariDownloadStageActive) {
275 setDownloadStarted(self);
279 - (void) myUpdateDiskImageStatus:(NSDictionary *)status {
280 int oldStage = [self downloadStage];
281 [self myUpdateDiskImageStatus:status];
282 //NSLog(@"myUpdateDiskImageStatus:%@ stage=%d -> %d", status, oldStage, [self downloadStage]);
284 if (dateStarted(self)
285 && oldStage == GrowlSafariDownloadStageDiskImageVerified
286 && [self downloadStage] == GrowlSafariDownloadStageDiskImageMounting
287 && [[status objectForKey:@"status-stage"] isEqualToString:@"attach"]) {
288 NSBundle *bundle = [GrowlSafari bundle];
289 NSString *description = [[NSString alloc] initWithFormat:
290 NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, @""),
291 [[self gsDownloadPath] lastPathComponent]];
292 [GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Mounting Disk Image", nil, bundle, @"")
293 description:description
294 notificationName:NSLocalizedStringFromTableInBundle(@"Disk Image Status", nil, bundle, @"")
299 [description release];
303 // This is to make sure we're done with the pre-saved downloads
304 - (id) myInitWithDownload:(id)fp8 mayOpenWhenDone:(BOOL)fp12 allowOverwrite:(BOOL)fp16 {
305 id retval = [self myInitWithDownload:fp8 mayOpenWhenDone:fp12 allowOverwrite:fp16];
306 setDownloadStarted(self);
310 - (NSString*) gsDownloadPath {
311 return [self currentPath];