diff options
Diffstat (limited to 'Source/SPQueryController.m')
-rw-r--r-- | Source/SPQueryController.m | 632 |
1 files changed, 632 insertions, 0 deletions
diff --git a/Source/SPQueryController.m b/Source/SPQueryController.m new file mode 100644 index 00000000..c7b61195 --- /dev/null +++ b/Source/SPQueryController.m @@ -0,0 +1,632 @@ +// +// $Id$ +// +// SPQueryController.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on Jan 30, 2009 +// Copyright (c) 2009 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPQueryController.h" +#import "SPConsoleMessage.h" +#import "SPArrayAdditions.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 @"sql" + +#define CONSOLE_WINDOW_AUTO_SAVE_NAME @"QueryConsole" + +// Table view column identifiers +#define TABLEVIEW_MESSAGE_COLUMN_IDENTIFIER @"message" +#define TABLEVIEW_DATE_COLUMN_IDENTIFIER @"messageDate" + +@interface SPQueryController (PrivateAPI) + +- (NSString *)_getConsoleStringWithTimeStamps:(BOOL)timeStamps; + +- (void)_updateFilterState; +- (void)_addMessageToConsole:(NSString *)message isError:(BOOL)error; +- (BOOL)_messageMatchesCurrentFilters:(NSString *)message; + +@end + +static SPQueryController *sharedQueryController = nil; + +@implementation SPQueryController + +@synthesize consoleFont; + +/* + * Returns the shared query console. + */ ++ (SPQueryController *)sharedQueryController +{ + @synchronized(self) { + if (sharedQueryController == nil) { + [[self alloc] init]; + } + } + + return sharedQueryController; +} + ++ (id)allocWithZone:(NSZone *)zone +{ + @synchronized(self) { + if (sharedQueryController == nil) { + sharedQueryController = [super allocWithZone:zone]; + + return sharedQueryController; + } + } + + return nil; // On subsequent allocation attempts return nil +} + +- (id)init +{ + if ((self = [super initWithWindowNibName:@"Console"])) { + messagesFullSet = [[NSMutableArray alloc] init]; + messagesFilteredSet = [[NSMutableArray alloc] init]; + + showSelectStatementsAreDisabled = NO; + showHelpStatementsAreDisabled = NO; + filterIsActive = NO; + activeFilterString = [[NSMutableString alloc] init]; + + // Weak reference to active messages set - starts off as full set + messagesVisibleSet = messagesFullSet; + + untitledDocumentCounter = 1; + + favoritesContainer = [[NSMutableDictionary alloc] init]; + historyContainer = [[NSMutableDictionary alloc] init]; + } + + 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 and initialise display + */ +- (void)awakeFromNib +{ + NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; + + [self setWindowFrameAutosaveName:CONSOLE_WINDOW_AUTO_SAVE_NAME]; + [[consoleTableView tableColumnWithIdentifier:TABLEVIEW_DATE_COLUMN_IDENTIFIER] setHidden:![prefs boolForKey:@"ConsoleShowTimestamps"]]; + showSelectStatementsAreDisabled = ![prefs boolForKey:@"ConsoleShowSelectsAndShows"]; + showHelpStatementsAreDisabled = ![prefs boolForKey:@"ConsoleShowHelps"]; + + [self _updateFilterState]; + + [loggingDisabledTextField setStringValue:([prefs boolForKey:@"ConsoleEnableLogging"]) ? @"" : @"Query logging is currently disabled"]; +} + +/** + * 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 < [messagesVisibleSet count]) { + SPConsoleMessage *message = NSArrayObjectAtIndex(messagesVisibleSet, 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 +{ + [messagesFullSet removeAllObjects]; + [messagesFilteredSet removeAllObjects]; + + [consoleTableView reloadData]; +} + +/** + * 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]; + + [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]; +} + +/** + * Toggles the display of the message time stamp column in the table view. + */ +- (IBAction)toggleShowTimeStamps:(id)sender +{ + [[consoleTableView tableColumnWithIdentifier:TABLEVIEW_DATE_COLUMN_IDENTIFIER] setHidden:([sender state])]; +} + +/** + * Toggles the hiding of messages containing SELECT and SHOW statements + */ +- (IBAction)toggleShowSelectShowStatements:(id)sender +{ + // Store the state of the toggle for later quick reference + showSelectStatementsAreDisabled = [sender state]; + + [self _updateFilterState]; +} + +/** + * Toggles the hiding of messages containing HELP statements + */ +- (IBAction)toggleShowHelpStatements:(id)sender +{ + // Store the state of the toggle for later quick reference + showHelpStatementsAreDisabled = [sender state]; + + [self _updateFilterState]; +} + +/** + * Shows the supplied message in the console. + */ +- (void)showMessageInConsole:(NSString *)message +{ + [self _addMessageToConsole:message isError:NO]; +} + +/** + * Shows the supplied error in the console. + */ +- (void)showErrorInConsole:(NSString *)error +{ + [self _addMessageToConsole:error isError:YES]; +} + +/** + * Returns the number of messages currently in the console. + */ +- (NSUInteger)consoleMessageCount +{ + return [messagesFullSet count]; +} + +/** + * 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) { + [[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 [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 = [[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 *)[messagesVisibleSet 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 DocumentsController + +- (NSURL *)registerDocumentWithFileURL:(NSURL *)fileURL andContextInfo:(NSMutableDictionary *)contextInfo +{ + + // Register a new untiled document and return its URL + if(fileURL == nil) { + NSURL *new = [NSURL URLWithString:[[NSString stringWithFormat:@"Untitled %d", untitledDocumentCounter] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + untitledDocumentCounter++; + + if(![favoritesContainer objectForKey:[new absoluteString]]) + [favoritesContainer setObject:[NSMutableArray array] forKey:[new absoluteString]]; + if(![historyContainer objectForKey:[new absoluteString]]) + [historyContainer setObject:[NSMutableArray array] forKey:[new absoluteString]]; + + return new; + } + + // Register a spf file to manage all query favorites and query history items + // file path based in a dictionary whereby the key represents the file name. + if(![favoritesContainer objectForKey:[fileURL absoluteString]]) { + if(contextInfo != nil && [contextInfo objectForKey:@"queryFavorites"] && [[contextInfo objectForKey:@"queryFavorites"] count]) { + NSMutableArray *arr = [[NSMutableArray alloc] init]; + [arr addObjectsFromArray:[contextInfo objectForKey:@"queryFavorites"]]; + [favoritesContainer setObject:arr forKey:[fileURL absoluteString]]; + [arr release]; + } else { + NSMutableArray *arr = [[NSMutableArray alloc] init]; + [favoritesContainer setObject:arr forKey:[fileURL absoluteString]]; + [arr release]; + } + } + if(![historyContainer objectForKey:[fileURL absoluteString]]) { + if(contextInfo != nil && [contextInfo objectForKey:@"queryHistory"] && [[contextInfo objectForKey:@"queryHistory"] count]) { + NSMutableArray *arr = [[NSMutableArray alloc] init]; + [arr addObjectsFromArray:[contextInfo objectForKey:@"queryHistory"]]; + [historyContainer setObject:arr forKey:[fileURL absoluteString]]; + [arr release]; + } else { + NSMutableArray *arr = [[NSMutableArray alloc] init]; + [historyContainer setObject:arr forKey:[fileURL absoluteString]]; + [arr release]; + } + } + + return fileURL; + +} + +- (void)removeRegisteredDocumentWithFileURL:(NSURL *)fileURL +{ + + if([favoritesContainer objectForKey:[fileURL absoluteString]]) + [favoritesContainer removeObjectForKey:[fileURL absoluteString]]; + if([historyContainer objectForKey:[fileURL absoluteString]]) + [historyContainer removeObjectForKey:[fileURL absoluteString]]; + +} + +- (void)addFavorite:(NSString *)favorite forFileURL:(NSURL *)fileURL +{ + +} + +- (void)addHistory:(NSString *)history forFileURL:(NSURL *)fileURL +{ + +} + +- (void)favoritesForFileURL:(NSURL *)fileURL +{ + +} + +- (void)historyForFileURL:(NSURL *)fileURL +{ + +} + +#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]) { + + // 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]; + } +} + +/** + * This method is called as part of Key Value Observing which is used to watch for prefernce changes which effect the interface. + */ +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if ([keyPath isEqualToString:@"ConsoleEnableLogging"]) { + [loggingDisabledTextField setStringValue:([[change objectForKey:NSKeyValueChangeNewKey] boolValue]) ? @"" : @"Query logging is currently disabled"]; + } +} + +/** + * 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]; +} + +- (void)updateEntries +{ + [consoleTableView reloadData]; + [consoleTableView scrollRowToVisible:([messagesVisibleSet count] - 1)]; +} + +/** + * Standard dealloc. + */ +- (void)dealloc +{ + messagesVisibleSet = nil; + + [messagesFullSet release], messagesFullSet = nil; + [messagesFilteredSet release], messagesFilteredSet = nil; + [activeFilterString release], activeFilterString = nil; + + [favoritesContainer release]; + [historyContainer release]; + + [super dealloc]; +} + +@end + +@implementation SPQueryController (PrivateAPI) + +/** + * 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 +{ + NSMutableString *consoleString = [[[NSMutableString alloc] init] autorelease]; + + 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; +} + + +/** + * 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)_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]; + + [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 && !showHelpStatementsAreDisabled) { + messagesVisibleSet = messagesFullSet; + + [consoleTableView reloadData]; + [consoleTableView scrollRowToVisible:([messagesVisibleSet count] - 1)]; + + [saveConsoleButton setEnabled:YES]; + [clearConsoleButton setEnabled:YES]; + + [saveConsoleButton setTitle:@"Save As..."]; + + // Hide progress spinner + [progressIndicator setHidden:YES]; + [progressIndicator stopAnimation:self]; + return; + } + + // 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]; + } + } + + // 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]; + } + + [saveConsoleButton setTitle:@"Save View As..."]; + + // Hide progress spinner + [progressIndicator setHidden:YES]; + [progressIndicator stopAnimation:self]; +} + +/** + * Adds the supplied message to the query console. + */ +- (void)_addMessageToConsole:(NSString *)message isError:(BOOL)error +{ + SPConsoleMessage *consoleMessage = [SPConsoleMessage consoleMessageWithMessage:[[[message stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] stringByReplacingOccurrencesOfString:@"\n" withString:@" "] stringByAppendingString:@";"] date:[NSDate date]]; + + [consoleMessage setIsError:error]; + + [messagesFullSet addObject:consoleMessage]; + + // If filtering is active, determine whether to add a reference to the filtered set + if ((showSelectStatementsAreDisabled || showHelpStatementsAreDisabled || filterIsActive) + && [self _messageMatchesCurrentFilters:[consoleMessage message]]) + { + [messagesFilteredSet addObject:[messagesFullSet lastObject]]; + [saveConsoleButton setEnabled:YES]; + [clearConsoleButton setEnabled:YES]; + } + + // Reload the table and scroll to the new message if it's visible (for speed) + if ( [[self window] isVisible] ) { + [consoleTableView reloadData]; + [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 uppercaseString] hasPrefix:@"SELECT"] || [[message uppercaseString] hasPrefix:@"SHOW"])) + { + messageMatchesCurrentFilters = NO; + } + // If hiding HELP is toggled to on, check whether the message is a HELP + if (messageMatchesCurrentFilters + && showHelpStatementsAreDisabled + && ([[message uppercaseString] hasPrefix:@"HELP"])) + { + messageMatchesCurrentFilters = NO; + } + + return messageMatchesCurrentFilters; +} + +@end |