From 3a7dc0b03bc51f46d762e2bbe6d7afdee59288ef Mon Sep 17 00:00:00 2001 From: stuconnolly Date: Thu, 26 Mar 2009 23:28:27 +0000 Subject: Completely redesigned query console that now uses a table view instead of a text view. This should significantly improve import speed, but most importantly resolves the crashes caused by the drawing that was being performed by the text view. Fixes issue #87 and implements #167. New console provides the following: - Live filtering - Ability to hide message time stamps - Ability to hide SELECT/SHOW statement messages - Ability to copy messages to pasteboard, including multiple messages - Ability to save the current filtered content to a file, with the option to include the message time stamps --- Source/SPQueryConsole.m | 436 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 374 insertions(+), 62 deletions(-) (limited to 'Source/SPQueryConsole.m') diff --git a/Source/SPQueryConsole.m b/Source/SPQueryConsole.m index 4c171de7..ffae7479 100644 --- a/Source/SPQueryConsole.m +++ b/Source/SPQueryConsole.m @@ -21,45 +21,156 @@ // More info at #import "SPQueryConsole.h" +#import "SPConsoleMessage.h" + +#define MESSAGE_TRUNCATE_CHARACTER_LENGTH 256 +#define MESSAGE_TIME_STAMP_FORMAT @"%H:%M:%S" #define DEFAULT_CONSOLE_LOG_FILENAME @"untitled" #define DEFAULT_CONSOLE_LOG_FILE_EXTENSION @"log" #define CONSOLE_WINDOW_AUTO_SAVE_NAME @"QueryConsole" +// Table view column identifiers +#define TABLEVIEW_MESSAGE_COLUMN_IDENTIFIER @"message" +#define TABLEVIEW_DATE_COLUMN_IDENTIFIER @"messageDate" + @interface SPQueryConsole (PrivateAPI) -- (void)_appendMessageToConsole:(NSString *)message withColor:(NSColor *)color; +- (NSString *)_getConsoleStringWithTimeStamps:(BOOL)timeStamps; + +- (void)_hideSelectShowStatements:(BOOL)show; +- (void)_filterConsoleUsingSearchString:(NSString *)string; +- (void)_addMessageToConsole:(NSString *)message isError:(BOOL)error; @end +static SPQueryConsole *sharedQueryConsole = nil; + @implementation SPQueryConsole -// ------------------------------------------------------------------------------- -// awakeFromNib -// -// Set the window's auto save name. -// ------------------------------------------------------------------------------- +@synthesize consoleFont; + +/* + * Returns the shared query console. + */ ++ (SPQueryConsole *)sharedQueryConsole +{ + @synchronized(self) { + if (sharedQueryConsole == nil) { + [[self alloc] init]; + } + } + + return sharedQueryConsole; +} + ++ (id)allocWithZone:(NSZone *)zone +{ + @synchronized(self) { + if (sharedQueryConsole == nil) { + sharedQueryConsole = [super allocWithZone:zone]; + + return sharedQueryConsole; + } + } + + return nil; // On subsequent allocation attempts return nil +} + +- (id)init +{ + if ((self = [super initWithWindowNibName:@"Console"])) { + messages = [[NSMutableArray alloc] init]; + messagesSubset = [[NSMutableArray alloc] init]; + messagesFilterSet = [[NSMutableArray alloc] init]; + + // Weak reference + messagesActiveSet = messages; + messagesFilterSet = messagesActiveSet; + } + + return self; +} + +/* + * The following base protocol methods are implemented to ensure the singleton status of this class. + */ + +- (id)copyWithZone:(NSZone *)zone { return self; } + +- (id)retain { return self; } + +- (unsigned)retainCount { return UINT_MAX; } + +- (id)autorelease { return self; } + +- (void)release { } + +/** + * Set the window's auto save name. + */ - (void)awakeFromNib { [self setWindowFrameAutosaveName:CONSOLE_WINDOW_AUTO_SAVE_NAME]; } -// ------------------------------------------------------------------------------- -// clearConsole: -// -// Clears the console by setting its displayed text to an empty string. -// ------------------------------------------------------------------------------- +/** + * Copy implementation for console table view. + */ +- (void)copy:(id)sender +{ + NSResponder *firstResponder = [[self window] firstResponder]; + + if ((firstResponder == consoleTableView) && ([consoleTableView numberOfSelectedRows] > 0)) { + + NSString *string = @""; + NSIndexSet *rows = [consoleTableView selectedRowIndexes]; + + NSUInteger i = [rows firstIndex]; + + while (i != NSNotFound) + { + if (i < [messagesFilterSet count]) { + SPConsoleMessage *message = [messagesFilterSet objectAtIndex:i]; + + NSString *consoleMessage = [message message]; + + // If the timestamp column is not hidden we need to include them in the copy + if (![[consoleTableView tableColumnWithIdentifier:TABLEVIEW_DATE_COLUMN_IDENTIFIER] isHidden]) { + + NSString *dateString = [[message messageDate] descriptionWithCalendarFormat:MESSAGE_TIME_STAMP_FORMAT timeZone:nil locale:nil]; + + consoleMessage = [NSString stringWithFormat:@"/* MySQL %@ */ %@", dateString, consoleMessage]; + } + + string = [string stringByAppendingFormat:@"%@\n", consoleMessage]; + } + + i = [rows indexGreaterThanIndex:i]; + } + + NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; + + // Copy the string to the pasteboard + [pasteBoard declareTypes:[NSArray arrayWithObjects:NSStringPboardType, nil] owner:nil]; + [pasteBoard setString:string forType:NSStringPboardType]; + } +} + +/** + * Clears the console by removing all of its messages. + */ - (IBAction)clearConsole:(id)sender { - [consoleTextView setString:@""]; + [messages removeAllObjects]; + + [consoleTableView reloadData]; } -// ------------------------------------------------------------------------------- -// saveConsoleAs: -// -// Presents the user with a save panel to the save the current console to a log file. -// ------------------------------------------------------------------------------- +/** + * Presents the user with a save panel to the save the current console to a log file. + */ - (IBAction)saveConsoleAs:(id)sender { NSSavePanel *panel = [NSSavePanel savePanel]; @@ -70,6 +181,9 @@ [panel setAllowsOtherFileTypes:YES]; [panel setCanSelectHiddenExtension:YES]; + [panel setAccessoryView:saveLogView]; + + [panel beginSheetForDirectory:nil file:DEFAULT_CONSOLE_LOG_FILENAME modalForWindow:[self window] modalDelegate:self didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:) contextInfo:NULL]; [panel beginSheetForDirectory:nil file:DEFAULT_CONSOLE_LOG_FILENAME modalForWindow:[self window] @@ -78,75 +192,273 @@ contextInfo:NULL]; } -// ------------------------------------------------------------------------------- -// showMessageInConsole: -// -// Shows the supplied message in the console. -// ------------------------------------------------------------------------------- -- (void)showMessageInConsole:(NSString *)message +/** + * Toggles the display of the message time stamp column in the table view. + */ +- (IBAction)toggleShowTimeStamps:(id)sender { - [self _appendMessageToConsole:message withColor:[NSColor blackColor]]; + [[consoleTableView tableColumnWithIdentifier:TABLEVIEW_DATE_COLUMN_IDENTIFIER] setHidden:(![sender intValue])]; } -// ------------------------------------------------------------------------------- -// showErrorInConsole: -// -// Shows the supplied error in the console. -// ------------------------------------------------------------------------------- -- (void)showErrorInConsole:(NSString *)error +/** + * Toggles the hiding of messages containing SELECT and SHOW statements + */ +- (IBAction)toggleShowSelectShowStatements:(id)sender { - [self _appendMessageToConsole:error withColor:[NSColor redColor]]; + [self _hideSelectShowStatements:(![sender intValue])]; } -// ------------------------------------------------------------------------------- -// consoleTextView -// -// Return a reference to the console's text view. -// ------------------------------------------------------------------------------- -- (NSTextView *)consoleTextView +/** + * Shows the supplied message in the console. + */ +- (void)showMessageInConsole:(NSString *)message { - return consoleTextView; + [self _addMessageToConsole:message isError:NO]; } -// ------------------------------------------------------------------------------- -// savePanelDidEnd:returnCode:contextInfo: -// -// Called when the NSSavePanel sheet ends. -// ------------------------------------------------------------------------------- +/** + * Shows the supplied error in the console. + */ +- (void)showErrorInConsole:(NSString *)error +{ + [self _addMessageToConsole:error isError:YES]; +} + +/** + * Called when the NSSavePanel sheet ends. Writes the console's current content to the selected file if required. + */ - (void)savePanelDidEnd:(NSSavePanel *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo { if (returnCode == NSOKButton) { - [[[consoleTextView textStorage] string] writeToFile:[sheet filename] atomically:YES encoding:NSUTF8StringEncoding error:NULL]; + [[self _getConsoleStringWithTimeStamps:[includeTimeStampsButton intValue]] writeToFile:[sheet filename] atomically:YES encoding:NSUTF8StringEncoding error:NULL]; } } +#pragma mark - +#pragma mark Tableview delegate methods + +/** + * Table view delegate method. Returns the number of rows in the table veiw. + */ +- (int)numberOfRowsInTableView:(NSTableView *)tableView +{ + return [messagesFilterSet count]; +} + +/** + * Table view delegate method. Returns the specific object for the request column and row. + */ +- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSUInteger)row +{ + NSString *returnValue = nil; + + id object = [[messagesFilterSet objectAtIndex:row] valueForKey:[tableColumn identifier]]; + + if ([[tableColumn identifier] isEqualToString:TABLEVIEW_DATE_COLUMN_IDENTIFIER]) { + + NSString *dateString = [(NSDate *)object descriptionWithCalendarFormat:MESSAGE_TIME_STAMP_FORMAT timeZone:nil locale:nil]; + + returnValue = [NSString stringWithFormat:@"/* MySQL %@ */", dateString]; + } + else { + if ([(NSString *)object length] > MESSAGE_TRUNCATE_CHARACTER_LENGTH) { + object = [NSString stringWithFormat:@"%@...", [object substringToIndex:MESSAGE_TRUNCATE_CHARACTER_LENGTH]]; + } + + returnValue = object; + } + + NSMutableDictionary *stringAtributes = nil; + + if (consoleFont) { + stringAtributes = [NSMutableDictionary dictionaryWithObject:consoleFont forKey:NSFontAttributeName]; + } + + // If this is an error message give it a red colour + if ([(SPConsoleMessage *)[messagesFilterSet objectAtIndex:row] isError]) { + if (stringAtributes) { + [stringAtributes setObject:[NSColor redColor] forKey:NSForegroundColorAttributeName]; + } + else { + stringAtributes = [NSMutableDictionary dictionaryWithObject:[NSColor redColor] forKey:NSForegroundColorAttributeName]; + } + } + + return [[[NSAttributedString alloc] initWithString:returnValue attributes:stringAtributes] autorelease]; +} + +#pragma mark - +#pragma mark Other + +/** + * Called whenver the test within the search field changes. + */ +- (void)controlTextDidChange:(NSNotification *)notification +{ + id object = [notification object]; + + if ([object isEqualTo:consoleSearchField]) { + [self _filterConsoleUsingSearchString:[[object stringValue] lowercaseString]]; + } +} + +/** + * Menu item validation for console table view contextual menu. + */ +- (BOOL)validateMenuItem:(NSMenuItem *)menuItem +{ + BOOL validate = NO; + + if ([menuItem action] == @selector(copy:)) { + validate = ([consoleTableView numberOfSelectedRows] > 0); + } + + return validate; +} + +/** + * Standard dealloc. + */ +- (void)dealloc +{ + messagesSubset = nil; + + [messages release], messages = nil; + [messagesSubset release], messagesSubset = nil; + [messagesFilterSet release], messagesFilterSet = nil; + + [super dealloc]; +} + @end @implementation SPQueryConsole (PrivateAPI) -// ------------------------------------------------------------------------------- -// _appendMessageToConsole:withColor: -// -// Appeds the supplied string to the query console, coloring the text using the -// supplied color. -// ------------------------------------------------------------------------------- -- (void)_appendMessageToConsole:(NSString *)message withColor:(NSColor *)color +/** + * Creates and returns a string made entirely of all of the console's messages and includes the message + * time stamps if specified. + */ +- (NSString *)_getConsoleStringWithTimeStamps:(BOOL)timeStamps { - int begin, end; + NSMutableString *consoleString = [[[NSMutableString alloc] init] autorelease]; - // Set the selected range of the text view to be the very last character - [consoleTextView setSelectedRange:NSMakeRange([[consoleTextView string] length], 0)]; - begin = [[consoleTextView string] length]; + for (SPConsoleMessage *message in messagesFilterSet) + { + if (timeStamps) { + NSString *dateString = [[message messageDate] descriptionWithCalendarFormat:MESSAGE_TIME_STAMP_FORMAT timeZone:nil locale:nil]; + + [consoleString appendString:[NSString stringWithFormat:@"/* MySQL %@ */ ", dateString]]; + } + + [consoleString appendString:[NSString stringWithFormat:@"%@\n", [message message]]]; + } + + return consoleString; +} + +/** + * Either hides or shows all SELECT and SHOW statements within the console. + */ +- (void)_hideSelectShowStatements:(BOOL)show +{ + if (!show) { + messagesActiveSet = messages; + messagesFilterSet = messagesActiveSet; + + [consoleTableView reloadData]; + + return; + } + + messagesActiveSet = [messages mutableCopy]; + + // Filter out messages that have a prefix of either SELECT or SHOW + for (SPConsoleMessage *message in messages) + { + if ([[message message] hasPrefix:@"SELECT"] || [[message message] hasPrefix:@"SHOW"]) { + [messagesActiveSet removeObject:message]; + } + } + + messagesFilterSet = messagesActiveSet; + + [consoleTableView reloadData]; +} + +/** + * Filters the messages array using the supplued search string. + */ +- (void)_filterConsoleUsingSearchString:(NSString *)searchString +{ + // Display start progress spinner + [progressIndicator setHidden:NO]; + [progressIndicator startAnimation:self]; - // Apped the message to the current text storage using the text view's current typing attributes - [[consoleTextView textStorage] appendAttributedString:[[NSAttributedString alloc] initWithString:message attributes:[consoleTextView typingAttributes]]]; - end = [[consoleTextView string] length]; + // Don't allow clearing the console while filtering its content + [saveConsoleButton setEnabled:NO]; + [clearConsoleButton setEnabled:NO]; - // Color the text we just added - [consoleTextView setTextColor:color range:NSMakeRange(begin, (end - begin))]; + [saveConsoleButton setTitle:@"Save View As..."]; + + // If there's no search string assign the active messages array back to the message array + if ([searchString length] == 0) { + [messagesFilterSet removeAllObjects]; + + messagesFilterSet = messagesActiveSet; + + [consoleTableView reloadData]; + [consoleTableView scrollRowToVisible:([messagesFilterSet count] - 1)]; + + [saveConsoleButton setEnabled:YES]; + [clearConsoleButton setEnabled:YES]; + + [saveConsoleButton setTitle:@"Save As..."]; + + // Display start progress spinner + [progressIndicator setHidden:YES]; + [progressIndicator stopAnimation:self]; + + return; + } + + // Remove all objects in the subset + [messagesSubset removeAllObjects]; + + // Filter the messages + for (SPConsoleMessage *message in messagesActiveSet) + { + if ([[message message] rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound) { + [messagesSubset addObject:message]; + } + } + + messagesFilterSet = messagesSubset; + + [consoleTableView reloadData]; + [consoleTableView scrollRowToVisible:([messagesFilterSet count] - 1)]; + + if ([messagesFilterSet count] > 0) { + [saveConsoleButton setEnabled:YES]; + } + + // Display start progress spinner + [progressIndicator setHidden:YES]; + [progressIndicator stopAnimation:self]; +} - // Scroll to the text we just added - [consoleTextView scrollRangeToVisible:[consoleTextView selectedRange]]; +/** + * Adds the supplied message to the query console. + */ +- (void)_addMessageToConsole:(NSString *)message isError:(BOOL)error +{ + SPConsoleMessage *consoleMessage = [SPConsoleMessage consoleMessageWithMessage:[[message stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] stringByAppendingString:@";"] date:[NSDate date]]; + + [consoleMessage setIsError:error]; + + [messages addObject:consoleMessage]; + + [consoleTableView reloadData]; + [consoleTableView scrollRowToVisible:([messages count] - 1)]; } @end -- cgit v1.2.3