From d794d105545e92a75bdaf697580ee7f12b566531 Mon Sep 17 00:00:00 2001 From: rowanbeentje Date: Sat, 3 Oct 2009 21:47:55 +0000 Subject: Improve Growl interaction to reduce general Growl spammage and improve functionality: - Growls are now only shown by default if they are not fired from the frontmost window - Long-running tasks (>3 secs) will still Growl - Clicking on a Growl will now bring the associated window to the front This addresses the original concerns of Issue #98. --- Source/CustomQuery.m | 8 +++- Source/SPGrowlController.h | 18 +++++++- Source/SPGrowlController.m | 105 +++++++++++++++++++++++++++++++++++++++------ Source/TableDocument.m | 10 +++-- Source/TableDump.m | 12 ++++++ 5 files changed, 134 insertions(+), 19 deletions(-) diff --git a/Source/CustomQuery.m b/Source/CustomQuery.m index 3f2afc2f..f04f9fd1 100644 --- a/Source/CustomQuery.m +++ b/Source/CustomQuery.m @@ -350,6 +350,9 @@ // Notify listeners that a query has started [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; + // Start the notification timer to allow notifications to be shown even if frontmost for long queries + [[SPGrowlController sharedGrowlController] setVisibilityForNotificationName:@"Query Finished"]; + // Reset the current table view as necessary to avoid redraw and reload issues. // Restore the view position to the top left to be within the results for all datasets. [customQueryView scrollRowToVisible:0]; @@ -585,6 +588,7 @@ // If no results were returned, redraw the empty table and post notifications before returning. if ( ![fullResult count] ) { [customQueryView reloadData]; + [streamingResult release]; // Notify any listeners that the query has completed [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; @@ -592,9 +596,8 @@ // Perform the Growl notification for query completion [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Query Finished" description:[NSString stringWithFormat:NSLocalizedString(@"%@",@"description for query finished growl notification"), [errorText stringValue]] + window:tableWindow notificationName:@"Query Finished"]; - - [streamingResult release]; return; } @@ -673,6 +676,7 @@ // Query finished Growl notification [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Query Finished" description:[NSString stringWithFormat:NSLocalizedString(@"%@",@"description for query finished growl notification"), [errorText stringValue]] + window:tableWindow notificationName:@"Query Finished"]; } diff --git a/Source/SPGrowlController.h b/Source/SPGrowlController.h index 707f6601..cd0f1ded 100644 --- a/Source/SPGrowlController.h +++ b/Source/SPGrowlController.h @@ -26,22 +26,38 @@ #import #import +#define SP_LONGRUNNING_NOTIFICATION_TIME 3.0 + @interface SPGrowlController : NSObject +{ + NSString *timingNotificationName; + double timingNotificationStart; +} // Singleton controller + (SPGrowlController *)sharedGrowlController; // Post notification - (void)notifyWithTitle:(NSString *)title - description:(NSString *)description + description:(NSString *)description + window:(NSWindow *)window notificationName:(NSString *)name; +- (void)notifyWithObject:(NSDictionary *)notificationDictionary; + - (void)notifyWithTitle:(NSString *)title description:(NSString *)description + window:(NSWindow *)window notificationName:(NSString *)name iconData:(NSData *)data priority:(int)priority isSticky:(BOOL)sticky clickContext:(id)clickContext; +// Receive notification click +- (void) growlNotificationWasClicked:(NSDictionary *)clickContext; + +// Timing functions +- (void) setVisibilityForNotificationName:(NSString *)name; +- (double) milliTime; @end diff --git a/Source/SPGrowlController.m b/Source/SPGrowlController.m index 13e1698a..b939d878 100644 --- a/Source/SPGrowlController.m +++ b/Source/SPGrowlController.m @@ -24,6 +24,7 @@ // More info at #import "SPGrowlController.h" +#include static SPGrowlController *sharedGrowlController = nil; @@ -60,6 +61,8 @@ static SPGrowlController *sharedGrowlController = nil; { if ((self = [super init])) { [GrowlApplicationBridge setGrowlDelegate:self]; + timingNotificationName = nil; + timingNotificationStart = 0; } return self; @@ -77,29 +80,68 @@ static SPGrowlController *sharedGrowlController = nil; - (id)autorelease { return self; } -- (void)release { } +- (void)release +{ + if (timingNotificationName) [timingNotificationName release]; +} /** * Posts a Growl notification using the supplied details and default values. + * Calls the notification after a tiny delay to allow isKeyWindow to have updated + * after tasks. */ -- (void)notifyWithTitle:(NSString *)title description:(NSString *)description notificationName:(NSString *)name +- (void)notifyWithTitle:(NSString *)title description:(NSString *)description window:(NSWindow *)window notificationName:(NSString *)name { - [self notifyWithTitle:title - description:description - notificationName:name - iconData:nil - priority:0 - isSticky:NO - clickContext:nil]; + NSMutableDictionary *notificationDictionary = [NSMutableDictionary dictionary]; + [notificationDictionary setObject:title forKey:@"title"]; + [notificationDictionary setObject:description forKey:@"description"]; + [notificationDictionary setObject:window forKey:@"window"]; + [notificationDictionary setObject:name forKey:@"name"]; + [notificationDictionary setObject:[NSDictionary dictionaryWithObject:[NSNumber numberWithInteger:[window windowNumber]] forKey:@"notificationWindow"] forKey:@"clickContext"]; + + [self performSelector:@selector(notifyWithObject:) withObject:notificationDictionary afterDelay:0.1]; } - + +/** + * Posts a Growl notification, using a NSDictionary to contain all arguments. + * Allows calling either with an NSThread or afterDelay as it only accepts a + * single argument. + */ +- (void)notifyWithObject:(NSDictionary *)notificationDictionary +{ + [self notifyWithTitle:[notificationDictionary objectForKey:@"title"] + description:[notificationDictionary objectForKey:@"description"] + window:[notificationDictionary objectForKey:@"window"] + notificationName:[notificationDictionary objectForKey:@"name"] + iconData:nil + priority:0 + isSticky:NO + clickContext:[notificationDictionary objectForKey:@"clickContext"]]; +} + /** * Posts a Growl notification using the supplied details and effectively ignoring the default values. */ -- (void)notifyWithTitle:(NSString *)title description:(NSString *)description notificationName:(NSString *)name iconData:(NSData *)data priority:(int)priority isSticky:(BOOL)sticky clickContext:(id)clickContext +- (void)notifyWithTitle:(NSString *)title description:(NSString *)description window:(NSWindow *)window notificationName:(NSString *)name iconData:(NSData *)data priority:(int)priority isSticky:(BOOL)sticky clickContext:(id)clickContext { - // Post notification only if preference is set - if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GrowlEnabled"]) { + BOOL postNotification = YES; + + // Don't post the notification if the notification window is key and order front + // as that suggests the user is already viewing the notification result. + if ([window isKeyWindow]) postNotification = NO; + + // If a timing notification name exists, check to see if it matches the notification name; + // if it does, and the time exceeds the threshold, display the notification even for + // frontmost windows to provide feedback for long-running tasks. + if (timingNotificationName && [timingNotificationName isEqualToString:name]) { + if ([self milliTime] > (SP_LONGRUNNING_NOTIFICATION_TIME * 1000) + timingNotificationStart) { + postNotification = YES; + } + [timingNotificationName release], timingNotificationName = nil; + } + + // Post notification only if preference is set and visibility has been confirmed + if (postNotification && [[NSUserDefaults standardUserDefaults] boolForKey:@"GrowlEnabled"]) { [GrowlApplicationBridge notifyWithTitle:title description:description notificationName:name @@ -110,4 +152,41 @@ static SPGrowlController *sharedGrowlController = nil; } } +/** + * React to a click on the notification. + */ +- (void) growlNotificationWasClicked:(NSDictionary *)clickContext +{ + if (clickContext && [clickContext objectForKey:@"notificationWindow"]) { + NSWindow *targetWindow = [NSApp windowWithWindowNumber:[[clickContext objectForKey:@"notificationWindow"] integerValue]]; + if (targetWindow) { + [NSApp activateIgnoringOtherApps:YES]; + [targetWindow makeKeyAndOrderFront:self]; + } + } +} + +/** + * Start the notification timer for a specific notification name. Only one notification + * timer can run at once, and tracks the time between this start and the notification + * being posted; if the notification is posted after the header-defined boundary, the + * notification will then be shown even if the app is frontmost. + */ +- (void) setVisibilityForNotificationName:(NSString *)name +{ + if (timingNotificationName) [timingNotificationName release], timingNotificationName = nil; + timingNotificationName = [[NSString alloc] initWithString:name]; + timingNotificationStart = [self milliTime]; +} + +/** + * Get a monotonically increasing time, in milliseconds. + */ +- (double) milliTime +{ + uint64_t currentTime_t = mach_absolute_time(); + Nanoseconds elapsedTime = AbsoluteToNanoseconds(*(AbsoluteTime *)&(currentTime_t)); + + return (((double)UnsignedWideToUInt64(elapsedTime)) * 1e-6); +} @end diff --git a/Source/TableDocument.m b/Source/TableDocument.m index 2b710737..bd57a7ab 100644 --- a/Source/TableDocument.m +++ b/Source/TableDocument.m @@ -608,6 +608,7 @@ // Connected Growl notification [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Connected" description:[NSString stringWithFormat:NSLocalizedString(@"Connected to %@",@"description for connected growl notification"), [tableWindow title]] + window:tableWindow notificationName:@"Connected"]; @@ -1450,6 +1451,7 @@ // Table syntax copied Growl notification [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Syntax Copied" description:[NSString stringWithFormat:NSLocalizedString(@"Syntax for %@ table copied",@"description for table syntax copied growl notification"), [self table]] + window:tableWindow notificationName:@"Syntax Copied"]; } @@ -1762,6 +1764,7 @@ // Table syntax copied Growl notification [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Syntax Copied" description:[NSString stringWithFormat:NSLocalizedString(@"Syntax for %@ table copied", @"description for table syntax copied growl notification"), [self table]] + window:tableWindow notificationName:@"Syntax Copied"]; } } @@ -1895,9 +1898,10 @@ [mySQLConnection disconnect]; // Disconnected Growl notification - [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Disconnected" - description:[NSString stringWithFormat:NSLocalizedString(@"Disconnected from %@",@"description for disconnected growl notification"), [tableWindow title]] - notificationName:@"Disconnected"]; + [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Disconnected" + description:[NSString stringWithFormat:NSLocalizedString(@"Disconnected from %@",@"description for disconnected growl notification"), [tableWindow title]] + window:tableWindow + notificationName:@"Disconnected"]; } /** diff --git a/Source/TableDump.m b/Source/TableDump.m index 346ae099..7a02964a 100644 --- a/Source/TableDump.m +++ b/Source/TableDump.m @@ -247,6 +247,9 @@ if ( returnCode != NSOKButton ) return; + // Start the notification timer to allow notifications to be shown even if frontmost for long queries + [[SPGrowlController sharedGrowlController] setVisibilityForNotificationName:@"Export Finished"]; + // Save path to preferences [prefs setObject:[sheet directory] forKey:@"savePath"]; @@ -400,6 +403,7 @@ // Export finished Growl notification [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Export Finished" description:[NSString stringWithFormat:NSLocalizedString(@"Finished exporting to %@",@"description for finished exporting growl notification"), [[sheet filename] lastPathComponent]] + window:tableWindow notificationName:@"Export Finished"]; } @@ -487,6 +491,9 @@ NSStringEncoding sqlEncoding = NSUTF8StringEncoding; NSCharacterSet *whitespaceAndNewlineCharset = [NSCharacterSet whitespaceAndNewlineCharacterSet]; + // Start the notification timer to allow notifications to be shown even if frontmost for long queries + [[SPGrowlController sharedGrowlController] setVisibilityForNotificationName:@"Import Finished"]; + // Open a filehandle for the SQL file sqlFileHandle = [NSFileHandle fileHandleForReadingAtPath:filename]; if (!sqlFileHandle) { @@ -687,6 +694,7 @@ // Import finished Growl notification [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Import Finished" description:[NSString stringWithFormat:NSLocalizedString(@"Finished importing %@",@"description for finished importing growl notification"), [filename lastPathComponent]] + window:tableWindow notificationName:@"Import Finished"]; } @@ -718,6 +726,9 @@ NSStringEncoding csvEncoding = [MCPConnection encodingForMySQLEncoding:[[tableDocumentInstance connectionEncoding] UTF8String]]; if (fieldMappingArray) [fieldMappingArray release], fieldMappingArray = nil; + // Start the notification timer to allow notifications to be shown even if frontmost for long queries + [[SPGrowlController sharedGrowlController] setVisibilityForNotificationName:@"Import Finished"]; + // Open a filehandle for the CSV file csvFileHandle = [NSFileHandle fileHandleForReadingAtPath:filename]; if (!csvFileHandle) { @@ -968,6 +979,7 @@ // Import finished Growl notification [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Import Finished" description:[NSString stringWithFormat:NSLocalizedString(@"Finished importing %@",@"description for finished importing growl notification"), [filename lastPathComponent]] + window:tableWindow notificationName:@"Import Finished"]; // Update the content view -- cgit v1.2.3