aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrowanbeentje <rowan@beent.je>2009-10-03 21:47:55 +0000
committerrowanbeentje <rowan@beent.je>2009-10-03 21:47:55 +0000
commitd794d105545e92a75bdaf697580ee7f12b566531 (patch)
tree18a8cf4bc60380284d28b151eb89357cf0df76bd
parent7abd1679730327d15a33cc516e84e0e368f53696 (diff)
downloadsequelpro-d794d105545e92a75bdaf697580ee7f12b566531.tar.gz
sequelpro-d794d105545e92a75bdaf697580ee7f12b566531.tar.bz2
sequelpro-d794d105545e92a75bdaf697580ee7f12b566531.zip
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.
-rw-r--r--Source/CustomQuery.m8
-rw-r--r--Source/SPGrowlController.h18
-rw-r--r--Source/SPGrowlController.m105
-rw-r--r--Source/TableDocument.m10
-rw-r--r--Source/TableDump.m12
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 <Cocoa/Cocoa.h>
#import <Growl/Growl.h>
+#define SP_LONGRUNNING_NOTIFICATION_TIME 3.0
+
@interface SPGrowlController : NSObject <GrowlApplicationBridgeDelegate>
+{
+ 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 <http://code.google.com/p/sequel-pro/>
#import "SPGrowlController.h"
+#include <mach/mach_time.h>
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