From e739aab4fc249fac5c82ebf8e2ac5e603341118c Mon Sep 17 00:00:00 2001 From: rowanbeentje Date: Sat, 18 Apr 2009 01:30:52 +0000 Subject: - Rework the query console logic. This fixes a memory leak caused by a mutableCopy when toggling SELECT/SHOWS; it allows the SELECT/SHOWS toggle and text filters to stack together whatever order they are changed in; and it allows both filtering and hiding SELECT/SHOWs to be applied 'live', ie messages added to the console will have the same logic applied to them. - Clean up trailing whitespace in the file --- Source/SPQueryConsole.h | 5 +- Source/SPQueryConsole.m | 292 ++++++++++++++++++++++++++---------------------- 2 files changed, 162 insertions(+), 135 deletions(-) (limited to 'Source') diff --git a/Source/SPQueryConsole.h b/Source/SPQueryConsole.h index 155517f7..cbe89662 100644 --- a/Source/SPQueryConsole.h +++ b/Source/SPQueryConsole.h @@ -31,7 +31,10 @@ IBOutlet NSButton *includeTimeStampsButton, *saveConsoleButton, *clearConsoleButton; NSFont *consoleFont; - NSMutableArray *messages, *messagesActiveSet, *messagesFilterSet, *messagesSubset; + NSMutableArray *messagesFullSet, *messagesFilteredSet, *messagesVisibleSet; + BOOL showSelectStatementsAreDisabled; + BOOL filterIsActive; + NSMutableString *activeFilterString; } @property (readwrite, retain) NSFont *consoleFont; diff --git a/Source/SPQueryConsole.m b/Source/SPQueryConsole.m index 0ba6c982..7106f49a 100644 --- a/Source/SPQueryConsole.m +++ b/Source/SPQueryConsole.m @@ -39,9 +39,9 @@ - (NSString *)_getConsoleStringWithTimeStamps:(BOOL)timeStamps; -- (void)_hideSelectShowStatements:(BOOL)show; -- (void)_filterConsoleUsingSearchString:(NSString *)string; +- (void)_updateFilterState; - (void)_addMessageToConsole:(NSString *)message isError:(BOOL)error; +- (BOOL)_messageMatchesCurrentFilters:(NSString *)message; @end @@ -81,15 +81,17 @@ static SPQueryConsole *sharedQueryConsole = 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; + messagesFullSet = [[NSMutableArray alloc] init]; + messagesFilteredSet = [[NSMutableArray alloc] init]; + + showSelectStatementsAreDisabled = NO; + filterIsActive = NO; + activeFilterString = [[NSMutableString alloc] init]; + + // Weak reference to active messages set - starts off as full set + messagesVisibleSet = messagesFullSet; } - + return self; } @@ -121,37 +123,37 @@ static SPQueryConsole *sharedQueryConsole = nil; - (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]; - + { + if (i < [messagesVisibleSet count]) { + SPConsoleMessage *message = [messagesVisibleSet 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]; @@ -163,8 +165,9 @@ static SPQueryConsole *sharedQueryConsole = nil; */ - (IBAction)clearConsole:(id)sender { - [messages removeAllObjects]; - + [messagesFullSet removeAllObjects]; + [messagesFilteredSet removeAllObjects]; + [consoleTableView reloadData]; } @@ -174,15 +177,15 @@ static SPQueryConsole *sharedQueryConsole = nil; - (IBAction)saveConsoleAs:(id)sender { NSSavePanel *panel = [NSSavePanel savePanel]; - + [panel setRequiredFileType:DEFAULT_CONSOLE_LOG_FILE_EXTENSION]; - + [panel setExtensionHidden:NO]; [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 @@ -205,7 +208,10 @@ static SPQueryConsole *sharedQueryConsole = nil; */ - (IBAction)toggleShowSelectShowStatements:(id)sender { - [self _hideSelectShowStatements:(![sender intValue])]; + // Store the state of the toggle for later quick reference + showSelectStatementsAreDisabled = ![sender intValue]; + + [self _updateFilterState]; } /** @@ -229,7 +235,7 @@ static SPQueryConsole *sharedQueryConsole = nil; */ - (NSUInteger)consoleMessageCount { - return [messages count]; + return [messagesFullSet count]; } /** @@ -250,40 +256,40 @@ static SPQueryConsole *sharedQueryConsole = nil; */ - (int)numberOfRowsInTableView:(NSTableView *)tableView { - return [messagesFilterSet count]; + return [messagesVisibleSet 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]]; - + + id object = [[messagesVisibleSet 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 ([(SPConsoleMessage *)[messagesVisibleSet objectAtIndex:row] isError]) { if (stringAtributes) { [stringAtributes setObject:[NSColor redColor] forKey:NSForegroundColorAttributeName]; } @@ -304,9 +310,14 @@ static SPQueryConsole *sharedQueryConsole = nil; - (void)controlTextDidChange:(NSNotification *)notification { id object = [notification object]; - - if ([object isEqualTo:consoleSearchField]) { - [self _filterConsoleUsingSearchString:[[object stringValue] lowercaseString]]; + + if ([object isEqualTo:consoleSearchField]) { + + // Store the state of the text filter and the current filter string for later quick reference + [activeFilterString setString:[[object stringValue] lowercaseString]]; + filterIsActive = [activeFilterString length]?YES:NO; + + [self _updateFilterState]; } } @@ -314,15 +325,15 @@ static SPQueryConsole *sharedQueryConsole = nil; * Menu item validation for console table view contextual menu. */ - (BOOL)validateMenuItem:(NSMenuItem *)menuItem -{ +{ if ([menuItem action] == @selector(copy:)) { return ([consoleTableView numberOfSelectedRows] > 0); } - + if ([menuItem action] == @selector(clearConsole:)) { return ([self consoleMessageCount] > 0); } - + return [[self window] validateMenuItem:menuItem]; } @@ -331,12 +342,12 @@ static SPQueryConsole *sharedQueryConsole = nil; */ - (void)dealloc { - messagesSubset = nil; - - [messages release], messages = nil; - [messagesSubset release], messagesSubset = nil; - [messagesFilterSet release], messagesFilterSet = nil; - + messagesVisibleSet = nil; + + [messagesFullSet release], messagesFullSet = nil; + [messagesFilteredSet release], messagesFilteredSet = nil; + [activeFilterString release], activeFilterString = nil; + [super dealloc]; } @@ -351,107 +362,86 @@ static SPQueryConsole *sharedQueryConsole = nil; - (NSString *)_getConsoleStringWithTimeStamps:(BOOL)timeStamps { NSMutableString *consoleString = [[[NSMutableString alloc] init] autorelease]; - - for (SPConsoleMessage *message in messagesFilterSet) + + for (SPConsoleMessage *message in messagesVisibleSet) { 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]; + return consoleString; } + /** - * Filters the messages array using the supplued search string. + * Updates the filtered result set based on any filter string and whether or not + * all SELECT nd SHOW statements should be shown within the console. */ -- (void)_filterConsoleUsingSearchString:(NSString *)searchString +- (void)_updateFilterState { + // Display start progress spinner [progressIndicator setHidden:NO]; [progressIndicator startAnimation:self]; - + // Don't allow clearing the console while filtering its content [saveConsoleButton setEnabled:NO]; [clearConsoleButton setEnabled:NO]; - - [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; - + + [messagesFilteredSet removeAllObjects]; + + // If filtering is disabled and all show/selects are shown, empty the filtered + // result set and set the full set to visible. + if (!filterIsActive && !showSelectStatementsAreDisabled) { + messagesVisibleSet = messagesFullSet; + [consoleTableView reloadData]; - [consoleTableView scrollRowToVisible:([messagesFilterSet count] - 1)]; - + [consoleTableView scrollRowToVisible:([messagesVisibleSet count] - 1)]; + [saveConsoleButton setEnabled:YES]; [clearConsoleButton setEnabled:YES]; - + [saveConsoleButton setTitle:@"Save As..."]; - - // Display start progress spinner + + // Hide 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]; + } + + // Cache frequently used selector, avoiding dynamic binding overhead + IMP messageMatchesFilters = [self methodForSelector:@selector(_messageMatchesCurrentFilters:)]; + + // Loop through all the messages in the full set to determine which should be + // added to the filtered set. + for (SPConsoleMessage *message in messagesFullSet) { + + // Add a reference to the message to the filtered set if filters are active and the + // current message matches them + if ((messageMatchesFilters)(self, @selector(_messageMatchesCurrentFilters:), [message message])) { + [messagesFilteredSet addObject:message]; } } - - messagesFilterSet = messagesSubset; - - [consoleTableView reloadData]; - [consoleTableView scrollRowToVisible:([messagesFilterSet count] - 1)]; - - if ([messagesFilterSet count] > 0) { + + // Ensure that the filtered set is marked as the currently visible set. + messagesVisibleSet = messagesFilteredSet; + + [consoleTableView reloadData]; + [consoleTableView scrollRowToVisible:([messagesVisibleSet count] - 1)]; + + if ([messagesVisibleSet count] > 0) { [saveConsoleButton setEnabled:YES]; + [clearConsoleButton setEnabled:YES]; } - - // Display start progress spinner + + [saveConsoleButton setTitle:@"Save View As..."]; + + // Hide progress spinner [progressIndicator setHidden:YES]; [progressIndicator stopAnimation:self]; } @@ -460,15 +450,49 @@ static SPQueryConsole *sharedQueryConsole = nil; * 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]; - + + [messagesFullSet addObject:consoleMessage]; + + // If filtering is active, determine whether to add a reference to the filtered set + if ((showSelectStatementsAreDisabled || filterIsActive) + && [self _messageMatchesCurrentFilters:[consoleMessage message]]) + { + [messagesFilteredSet addObject:[messagesFullSet lastObject]]; + } + + // Reload the table and scroll to the new message [consoleTableView reloadData]; - [consoleTableView scrollRowToVisible:([messages count] - 1)]; + [consoleTableView scrollRowToVisible:([messagesVisibleSet count] - 1)]; +} + +/** + * Checks whether the supplied message text matches the current filter text, if any, + * and whether it should be hidden if the SELECT/SHOW toggle is off. + */ +- (BOOL)_messageMatchesCurrentFilters:(NSString *)message +{ + BOOL messageMatchesCurrentFilters = YES; + + // Check whether to hide the message based on the current filter text, if any + if (filterIsActive + && [message rangeOfString:activeFilterString options:NSCaseInsensitiveSearch].location == NSNotFound) + { + messageMatchesCurrentFilters = NO; + } + + // If hiding SELECTs and SHOWs is toggled to on, check whether the message is a SELECT or SHOW + if (messageMatchesCurrentFilters + && showSelectStatementsAreDisabled + && ([message hasPrefix:@"SELECT"] || [message hasPrefix:@"SHOW"])) + { + messageMatchesCurrentFilters = NO; + } + + return messageMatchesCurrentFilters; } @end -- cgit v1.2.3