diff options
Diffstat (limited to 'Source/SPCustomQuery.m')
-rw-r--r-- | Source/SPCustomQuery.m | 3287 |
1 files changed, 3287 insertions, 0 deletions
diff --git a/Source/SPCustomQuery.m b/Source/SPCustomQuery.m new file mode 100644 index 00000000..143e40f8 --- /dev/null +++ b/Source/SPCustomQuery.m @@ -0,0 +1,3287 @@ +// +// $Id$ +// +// SPCustomQuery.m +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Wed May 01 2002. +// Copyright (c) 2002-2003 Lorenz Textor. 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 "SPCustomQuery.h" +#import "SPSQLParser.h" +#import "SPGrowlController.h" +#import "SPStringAdditions.h" +#import "SPTextViewAdditions.h" +#import "SPArrayAdditions.h" +#import "SPDataAdditions.h" +#import "SPDataCellFormatter.h" +#import "SPDatabaseDocument.h" +#import "SPTablesList.h" +#import "RegexKitLite.h" +#import "SPFieldEditorController.h" +#import "SPTextAndLinkCell.h" +#import "SPTooltip.h" +#import "SPQueryFavoriteManager.h" +#import "SPQueryController.h" +#import "SPConstants.h" +#import "SPEncodingPopupAccessory.h" +#import "SPDataStorage.h" +#import "SPAlertSheets.h" +#import "SPMainThreadTrampoline.h" + +#import <BWToolkitFramework/BWToolkitFramework.h> + +@implementation SPCustomQuery + +#pragma mark IBAction methods + +/* + * Split all the queries in the text view, split them into individual queries, + * and run sequentially. + */ +- (IBAction)runAllQueries:(id)sender +{ + SPSQLParser *queryParser; + NSArray *queries; + + // Prevent multiple runs by holding the keys down + if ([tableDocumentInstance isWorking]) return; + + // Fixes bug in key equivalents. + if ([[NSApp currentEvent] type] == NSKeyUp) { + return; + } + + // Retrieve the custom query string and split it into separate SQL queries + queryParser = [[SPSQLParser alloc] initWithString:[textView string]]; + [queryParser setDelimiterSupport:YES]; + queries = [queryParser splitStringByCharacter:';']; + [queryParser release]; + + oldThreadedQueryRange = [textView selectedRange]; + // Unselect a selection if given to avoid interferring with error highlighting + [textView setSelectedRange:NSMakeRange(oldThreadedQueryRange.location, 0)]; + // Reset queryStartPosition + queryStartPosition = 0; + + reloadingExistingResult = NO; + [self clearResultViewDetailsToRestore]; + + // Remember query start position for error highlighting + queryTextViewStartPosition = 0; + + [self performQueries:queries withCallback:@selector(runAllQueriesCallback)]; +} + +- (void) runAllQueriesCallback +{ + + // If no error was selected, reconstruct a given selection. This + // may no longer be valid if the query text has changed in the + // meantime, so error-checking is required. + if (oldThreadedQueryRange.location + oldThreadedQueryRange.length <= [[textView string] length]) { + + if ([textView selectedRange].length == 0) + [textView setSelectedRange:oldThreadedQueryRange]; + + // Invoke textStorageDidProcessEditing: for syntax highlighting and auto-uppercase + NSRange oldRange = [textView selectedRange]; + [textView setSelectedRange:NSMakeRange(oldThreadedQueryRange.location,0)]; + [textView insertText:@""]; + [textView setSelectedRange:oldRange]; + } +} + +/* + * Depending on selection, run either the query containing the selection caret (if the caret is + * at a single point within the text view), or run the selected text (if a text range is selected). + */ +- (IBAction)runSelectedQueries:(id)sender +{ + NSArray *queries; + NSString *query = nil; + NSRange selectedRange = [textView selectedRange]; + SPSQLParser *queryParser; + + // Prevent multiple runs by holding the keys down + if ([tableDocumentInstance isWorking]) return; + + // If the current selection is a single caret position, run the current query. + if (selectedRange.length == 0) { + // BOOL doLookBehind = YES; + // query = [self queryAtPosition:selectedRange.location lookBehind:&doLookBehind]; + if(currentQueryRange.length) + query = [[textView string] substringWithRange:currentQueryRange]; + if (!query) { + NSBeep(); + return; + } + queries = [NSArray arrayWithObject:query]; + + // Remember query start position for error highlighting + queryTextViewStartPosition = currentQueryRange.location; + + // Otherwise, run the selected text. + } else { + queryParser = [[SPSQLParser alloc] initWithString:[[textView string] substringWithRange:selectedRange]]; + [queryParser setDelimiterSupport:YES]; + queries = [queryParser splitStringByCharacter:';']; + [queryParser release]; + + // Remember query start position for error highlighting + queryTextViewStartPosition = selectedRange.location; + } + + // Invoke textStorageDidProcessEditing: for syntax highlighting and auto-uppercase + // and preserve the selection + [textView setSelectedRange:NSMakeRange(selectedRange.location, 0)]; + [textView insertText:@""]; + + // Inserting empty text may have cancelled a partial accent - range check before + // restoring the selection. + if (selectedRange.location > [[textView string] length]) selectedRange.location = [[textView string] length]; + [textView setSelectedRange:selectedRange]; + + reloadingExistingResult = NO; + [self clearResultViewDetailsToRestore]; + + [self performQueries:queries withCallback:NULL]; +} + +/** + * Insert the choosen favorite query in the query textView or save query to favorites or opens window to edit favorites + */ +- (IBAction)chooseQueryFavorite:(id)sender +{ + if ([queryFavoritesButton indexOfSelectedItem] == 1) { + + // This should never evaluate to true as we are now performing menu validation, meaning the 'Save Query to Favorites' menu item will + // only be enabled if the query text view has at least one character present. + if ([[textView string] isEqualToString:@""]) { + SPBeginAlertSheet(NSLocalizedString(@"Empty query", @"empty query message"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocumentInstance parentWindow], self, nil, nil, + NSLocalizedString(@"Cannot save an empty query.", @"empty query informative message")); + return; + } + + if ([tableDocumentInstance isUntitled]) [saveQueryFavoriteGlobal setState:NSOnState]; + [NSApp beginSheet:queryFavoritesSheet + modalForWindow:[tableDocumentInstance parentWindow] + modalDelegate:self + didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) + contextInfo:@"addSelectionToNewQueryFavorite"]; + + } + if ([queryFavoritesButton indexOfSelectedItem] == 2) { + + // This should never evaluate to true as we are now performing menu validation, meaning the 'Save Query to Favorites' menu item will + // only be enabled if the query text view has at least one character present. + if ([[textView string] isEqualToString:@""]) { + SPBeginAlertSheet(NSLocalizedString(@"Empty query", @"empty query message"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocumentInstance parentWindow], self, nil, nil, + NSLocalizedString(@"Cannot save an empty query.", @"empty query informative message")); + return; + } + + if ([tableDocumentInstance isUntitled]) [saveQueryFavoriteGlobal setState:NSOnState]; + [NSApp beginSheet:queryFavoritesSheet + modalForWindow:[tableDocumentInstance parentWindow] + modalDelegate:self + didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) + contextInfo:@"addAllToNewQueryFavorite"]; + } + else if ([queryFavoritesButton indexOfSelectedItem] == 3) { + + // init query favorites controller + [prefs synchronize]; + if(favoritesManager) [favoritesManager release]; + favoritesManager = [[SPQueryFavoriteManager alloc] initWithDelegate:self]; + + // Open query favorite manager + [NSApp beginSheet:[favoritesManager window] + modalForWindow:[tableDocumentInstance parentWindow] + modalDelegate:favoritesManager + didEndSelector:nil + contextInfo:nil]; + } + else if ([queryFavoritesButton indexOfSelectedItem] > 5) { + // Choose favorite + BOOL replaceContent = [prefs boolForKey:SPQueryFavoriteReplacesContent]; + + if([[NSApp currentEvent] modifierFlags] & (NSShiftKeyMask|NSControlKeyMask|NSAlternateKeyMask|NSCommandKeyMask)) + replaceContent = !replaceContent; + if(replaceContent) { + [textView setSelectedRange:NSMakeRange(0,[[textView string] length])]; + [textView breakUndoCoalescing]; + [textView insertText:@""]; + } + + // The actual query strings have been already stored as tooltip + [textView insertAsSnippet:[[queryFavoritesButton selectedItem] toolTip] atRange:NSMakeRange([textView selectedRange].location, 0)]; + } +} + +/* + * Insert the choosen history query in the query textView + */ +- (IBAction)chooseQueryHistory:(id)sender +{ + + [prefs synchronize]; + + // Choose history item + if ([queryHistoryButton indexOfSelectedItem] > 6) { + + BOOL replaceContent = [prefs boolForKey:SPQueryHistoryReplacesContent]; + [textView breakUndoCoalescing]; + if([[NSApp currentEvent] modifierFlags] & (NSShiftKeyMask|NSControlKeyMask|NSAlternateKeyMask|NSCommandKeyMask)) + replaceContent = !replaceContent; + if(replaceContent) + [textView setSelectedRange:NSMakeRange(0,[[textView string] length])]; + + [textView insertText:[[[SPQueryController sharedQueryController] historyForFileURL:[tableDocumentInstance fileURL]] objectAtIndex:[queryHistoryButton indexOfSelectedItem]-7]]; + } +} + +/* + * Closes the sheet + */ +- (IBAction)closeSheet:(id)sender +{ + [NSApp endSheet:[sender window] returnCode:[sender tag]]; + [[sender window] orderOut:self]; +} + +/* + * Perform simple actions (which don't require their own method), triggered by selecting the appropriate menu item + * in the "gear" action menu displayed beneath the cusotm query view. + */ +- (IBAction)gearMenuItemSelected:(id)sender +{ + + if ( sender == previousHistoryMenuItem ) { + NSInteger numberOfHistoryItems = [[SPQueryController sharedQueryController] numberOfHistoryItemsForFileURL:[tableDocumentInstance fileURL]]; + currentHistoryOffsetIndex++; + if ( numberOfHistoryItems > 0 && currentHistoryOffsetIndex < numberOfHistoryItems && currentHistoryOffsetIndex >= 0) { + historyItemWasJustInserted = YES; + [textView breakUndoCoalescing]; + NSString *historyString = [[[SPQueryController sharedQueryController] historyForFileURL:[tableDocumentInstance fileURL]] objectAtIndex:currentHistoryOffsetIndex]; + NSRange rangeOfInsertedString = NSMakeRange([textView selectedRange].location, [historyString length]); + [textView insertText:historyString]; + [textView setSelectedRange:rangeOfInsertedString]; + } else { + currentHistoryOffsetIndex--; + NSBeep(); + } + historyItemWasJustInserted = NO; + } + + if ( sender == nextHistoryMenuItem ) { + NSInteger numberOfHistoryItems = [[SPQueryController sharedQueryController] numberOfHistoryItemsForFileURL:[tableDocumentInstance fileURL]]; + currentHistoryOffsetIndex--; + if ( numberOfHistoryItems > 0 && currentHistoryOffsetIndex < numberOfHistoryItems && currentHistoryOffsetIndex >= 0) { + historyItemWasJustInserted = YES; + [textView breakUndoCoalescing]; + NSString *historyString = [[[SPQueryController sharedQueryController] historyForFileURL:[tableDocumentInstance fileURL]] objectAtIndex:currentHistoryOffsetIndex]; + NSRange rangeOfInsertedString = NSMakeRange([textView selectedRange].location, [historyString length]); + [textView insertText:historyString]; + [textView setSelectedRange:rangeOfInsertedString]; + } else { + currentHistoryOffsetIndex++; + NSBeep(); + } + historyItemWasJustInserted = NO; + } + + // "Shift Right" menu item - indent the selection with an additional tab. + if (sender == shiftRightMenuItem) { + [textView shiftSelectionRight]; + } + + // "Shift Left" menu item - un-indent the selection by one tab if possible. + if (sender == shiftLeftMenuItem) { + [textView shiftSelectionLeft]; + } + + // "Comment Line/Selection" menu item - Add or remove "-- " for each line + // in a line or selection resp. or wrap the selection into /* */ + // if the selection does not end at the end of a line (in-line comment) + if (sender == commentLineOrSelectionMenuItem) { + [self commentOut]; + } + + // "Comment Current Query" menu item - Add or remove "-- " for each line + // in the current query + if (sender == commentCurrentQueryMenuItem) { + [self commentOutCurrentQueryTakingSelection:NO]; + } + + // "Completion List" menu item - used to autocomplete. Uses a different shortcut to avoid the menu button flickering + // on normal autocomplete usage. + if (sender == completionListMenuItem) { + if([[NSApp currentEvent] modifierFlags] & (NSControlKeyMask)) + [textView doCompletionByUsingSpellChecker:NO fuzzyMode:YES autoCompleteMode:NO]; + else + [textView doCompletionByUsingSpellChecker:NO fuzzyMode:NO autoCompleteMode:NO]; + } + + // "Editor font..." menu item to bring up the font panel + if (sender == editorFontMenuItem) { + [[NSFontPanel sharedFontPanel] setPanelFont:[textView font] isMultiple:NO]; + [[NSFontPanel sharedFontPanel] setDelegate:self]; + [[NSFontPanel sharedFontPanel] makeKeyAndOrderFront:self]; + } + + // "Indent new lines" toggle + if (sender == autoindentMenuItem) { + BOOL enableAutoindent = !([autoindentMenuItem state] == NSOffState); + [prefs setBool:enableAutoindent forKey:SPCustomQueryAutoIndent]; + [prefs synchronize]; + [autoindentMenuItem setState:enableAutoindent?NSOnState:NSOffState]; + [textView setAutoindent:enableAutoindent]; + } + + // "Auto-pair characters" toggle + if (sender == autopairMenuItem) { + BOOL enableAutopair = !([autopairMenuItem state] == NSOffState); + [prefs setBool:enableAutopair forKey:SPCustomQueryAutoPairCharacters]; + [prefs synchronize]; + [autopairMenuItem setState:enableAutopair?NSOnState:NSOffState]; + [textView setAutopair:enableAutopair]; + } + + // "Auto-help" toggle + if (sender == autohelpMenuItem) { + BOOL enableAutohelp = !([autohelpMenuItem state] == NSOffState); + [prefs setBool:enableAutohelp forKey:SPCustomQueryUpdateAutoHelp]; + [prefs synchronize]; + [autohelpMenuItem setState:enableAutohelp?NSOnState:NSOffState]; + [textView setAutohelp:enableAutohelp]; + } + + // "Auto-uppercase keywords" toggle + if (sender == autouppercaseKeywordsMenuItem) { + BOOL enableAutouppercaseKeywords = !([autouppercaseKeywordsMenuItem state] == NSOffState); + [prefs setBool:enableAutouppercaseKeywords forKey:SPCustomQueryAutoUppercaseKeywords]; + [prefs synchronize]; + [autouppercaseKeywordsMenuItem setState:enableAutouppercaseKeywords?NSOnState:NSOffState]; + [textView setAutouppercaseKeywords:enableAutouppercaseKeywords]; + } +} + +- (IBAction)saveQueryHistory:(id)sender +{ + NSSavePanel *panel = [NSSavePanel savePanel]; + + [panel setRequiredFileType:SPFileExtensionSQL]; + + [panel setExtensionHidden:NO]; + [panel setAllowsOtherFileTypes:YES]; + [panel setCanSelectHiddenExtension:YES]; + [panel setCanCreateDirectories:YES]; + + [panel setAccessoryView:[SPEncodingPopupAccessory encodingAccessory:[prefs integerForKey:SPLastSQLFileEncoding] includeDefaultEntry:NO encodingPopUp:&encodingPopUp]]; + + [encodingPopUp setEnabled:YES]; + + [panel beginSheetForDirectory:nil file:@"history" modalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:) contextInfo:@"saveHistory"]; +} + +- (IBAction)copyQueryHistory:(id)sender +{ + + NSPasteboard *pb = [NSPasteboard generalPasteboard]; + + [pb declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil]; + [pb setString:[self buildHistoryString] forType:NSStringPboardType]; + +} + +// "Clear History" menu item - clear query history +- (IBAction)clearQueryHistory:(id)sender +{ + + NSString *infoString; + + if ([tableDocumentInstance isUntitled]) + infoString = NSLocalizedString(@"Are you sure you want to clear the global history list? This action cannot be undone.", @"clear global history list informative message"); + else + infoString = [NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to clear the history list for “%@”? This action cannot be undone.", @"clear history list for “%@” informative message"), [tableDocumentInstance displayName]]; + + NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"Clear History?", @"clear history message") + defaultButton:NSLocalizedString(@"Clear", @"clear button") + alternateButton:NSLocalizedString(@"Cancel", @"cancel button") + otherButton:nil + informativeTextWithFormat:infoString]; + + [alert setAlertStyle:NSCriticalAlertStyle]; + + NSArray *buttons = [alert buttons]; + + // Change the alert's cancel button to have the key equivalent of return + [[buttons objectAtIndex:0] setKeyEquivalent:@"r"]; + [[buttons objectAtIndex:0] setKeyEquivalentModifierMask:NSCommandKeyMask]; + [[buttons objectAtIndex:1] setKeyEquivalent:@"\r"]; + + [alert beginSheetModalForWindow:[tableDocumentInstance parentWindow] + modalDelegate:self + didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) + contextInfo:@"clearHistory"]; + +} + +/* + * Set font panel's valid modes + */ +- (NSUInteger)validModesForFontPanel:(NSFontPanel *)fontPanel +{ + return (NSFontPanelSizeModeMask|NSFontPanelCollectionModeMask); +} + +/** + * Toggle whether the query info pane is visible. + */ +- (IBAction)toggleQueryInfoPaneCollapse:(id)sender +{ + [queryInfoPaneSplitView toggleCollapse:sender]; + + [sender setToolTip:([sender state] == NSOffState) ? NSLocalizedString(@"Show Query Information", @"Show Query Information") : NSLocalizedString(@"Hide Query Information", @"Hide Query Information")]; +} + +#pragma mark - +#pragma mark Query actions + +/* + * Performs the mysql-query given by the user + * sets the tableView columns corresponding to the mysql-result + */ +- (void)performQueries:(NSArray *)queries withCallback:(SEL)customQueryCallbackMethod; +{ + NSString *taskString; + if ([queries count] > 1) { + taskString = [NSString stringWithFormat:NSLocalizedString(@"Running query %i of %lu...", @"Running multiple queries string"), 1, (unsigned long)[queries count]]; + } else { + taskString = NSLocalizedString(@"Running query...", @"Running single query string"); + } + [tableDocumentInstance startTaskWithDescription:taskString]; + [errorText setStringValue:taskString]; + [affectedRowsText setStringValue:@""]; + + NSValue *encodedCallbackMethod = nil; + if (customQueryCallbackMethod) + encodedCallbackMethod = [NSValue valueWithBytes:&customQueryCallbackMethod objCType:@encode(SEL)]; + NSDictionary *taskArguments = [NSDictionary dictionaryWithObjectsAndKeys:queries, @"queries", encodedCallbackMethod, @"callback", nil]; + + // If a helper thread is already running, execute inline - otherwise detach a new thread for the queries + if ([NSThread isMainThread]) { + [NSThread detachNewThreadSelector:@selector(performQueriesTask:) toTarget:self withObject:taskArguments]; + } else { + [self performQueriesTask:taskArguments]; + } +} + +- (void)performQueriesTask:(NSDictionary *)taskArguments +{ + NSAutoreleasePool *queryRunningPool = [[NSAutoreleasePool alloc] init]; + NSArray *queries = [taskArguments objectForKey:@"queries"]; + MCPStreamingResult *streamingResult = nil; + NSMutableString *errors = [NSMutableString string]; + SEL callbackMethod = NULL; + NSString *taskButtonString; + + NSInteger i, totalQueriesRun = 0, totalAffectedRows = 0; + double executionTime = 0; + NSInteger firstErrorOccuredInQuery = -1; + BOOL suppressErrorSheet = NO; + BOOL tableListNeedsReload = NO; + BOOL databaseWasChanged = NO; + // BOOL queriesSeparatedByDelimiter = NO; + + NSCharacterSet *whitespaceAndNewlineSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; + [tableDocumentInstance setQueryMode:SPCustomQueryQueryMode]; + + // Notify listeners that a query has started + [[NSNotificationCenter defaultCenter] postNotificationOnMainThreadWithName:@"SMySQLQueryWillBePerformed" object:tableDocumentInstance]; + + // 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. + if(editedRow == -1) { + [[customQueryView onMainThread] scrollRowToVisible:0]; + [[customQueryView onMainThread] scrollColumnToVisible:0]; + } + + // Remove all the columns if not reloading the table + if(!reloadingExistingResult) { + if (cqColumnDefinition) [cqColumnDefinition release], cqColumnDefinition = nil; + [[self onMainThread] updateTableView]; + } + + // Disable automatic query retries on failure for the custom queries + [mySQLConnection setAllowQueryRetries:NO]; + + NSUInteger queryCount = [queries count]; + NSMutableArray *tempQueries = [NSMutableArray arrayWithCapacity:queryCount]; + + // Enable task cancellation + if (queryCount > 1) + taskButtonString = NSLocalizedString(@"Stop queries", @"Stop queries string"); + else + taskButtonString = NSLocalizedString(@"Stop query", @"Stop query string"); + [tableDocumentInstance enableTaskCancellationWithTitle:taskButtonString callbackObject:nil callbackFunction:NULL]; + + // Perform the supplied queries in series + for ( i = 0 ; i < queryCount ; i++ ) { + + if (i > 0) { + NSString *taskString = [NSString stringWithFormat:NSLocalizedString(@"Running query %ld of %lu...", @"Running multiple queries string"), (long)(i+1), (unsigned long)queryCount]; + [tableDocumentInstance setTaskDescription:taskString]; + [[errorText onMainThread] setStringValue:taskString]; + } + + NSString *query = [NSArrayObjectAtIndex(queries, i) stringByTrimmingCharactersInSet:whitespaceAndNewlineSet]; + + // Don't run blank queries, or queries which only contain whitespace. + if (![query length]) + continue; + + // store trimmed queries for usedQueries and history + [tempQueries addObject:query]; + + // Run the query, timing execution (note this also includes network and overhead) + streamingResult = [[mySQLConnection streamingQueryString:query] retain]; + executionTime += [mySQLConnection lastQueryExecutionTime]; + totalQueriesRun++; + + // If this is the last query, retrieve and store the result; otherwise, + // discard the result without fully loading. + if (totalQueriesRun == queryCount || [mySQLConnection queryCancelled]) { + + // Retrieve and cache the column definitions for the result array + if (cqColumnDefinition) [cqColumnDefinition release]; + cqColumnDefinition = [[streamingResult fetchResultFieldsStructure] retain]; + + if(!reloadingExistingResult) { + [[self onMainThread] updateTableView]; + } + + [self processResultIntoDataStorage:streamingResult]; + } else { + [streamingResult cancelResultLoad]; + } + + // Record any affected rows + if ( [mySQLConnection affectedRows] != -1 ) + totalAffectedRows += [mySQLConnection affectedRows]; + else if ( [streamingResult numOfRows] ) + totalAffectedRows += [streamingResult numOfRows]; + + [streamingResult release]; + + // Store any error messages + if ([mySQLConnection queryErrored] || [mySQLConnection queryCancelled]) { + + NSString *errorString; + if ([mySQLConnection queryCancelled]) { + if ([mySQLConnection queryCancellationUsedReconnect]) + errorString = NSLocalizedString(@"Query cancelled. Please note that to cancel the query the connection had to be reset; transactions and connection variables were reset.", @"Query cancel by resetting connection error"); + else + errorString = NSLocalizedString(@"Query cancelled.", @"Query cancelled error"); + } else { + errorString = [mySQLConnection getLastErrorMessage]; + } + + // If the query errored, append error to the error log for display at the end + if ( queryCount > 1 ) { + if(firstErrorOccuredInQuery == -1) + firstErrorOccuredInQuery = i+1; + + if(!suppressErrorSheet) + { + // Update error text for the user + [errors appendString:[NSString stringWithFormat:NSLocalizedString(@"[ERROR in query %ld] %@\n", @"error text when multiple custom query failed"), + (long)(i+1), + errorString]]; + [[errorText onMainThread] setStringValue:errors]; + + // ask the user to continue after detecting an error + if (![mySQLConnection queryCancelled]) { + NSAlert *alert = [[[NSAlert alloc] init] autorelease]; + [alert addButtonWithTitle:NSLocalizedString(@"Run All", @"run all button")]; + [alert addButtonWithTitle:NSLocalizedString(@"Continue", @"continue button")]; + [alert addButtonWithTitle:NSLocalizedString(@"Stop", @"stop button")]; + [alert setMessageText:NSLocalizedString(@"MySQL Error", @"mysql error message")]; + [alert setInformativeText:[mySQLConnection getLastErrorMessage]]; + [alert setAlertStyle:NSWarningAlertStyle]; + NSInteger choice = [[alert onMainThread] runModal]; + switch (choice){ + case NSAlertFirstButtonReturn: + suppressErrorSheet = YES; + case NSAlertSecondButtonReturn: + break; + default: + if(i < queryCount-1) // output that message only if it was not the last one + [errors appendString:NSLocalizedString(@"Execution stopped!\n", @"execution stopped message")]; + i = queryCount; // break for loop; for safety reasons stop the execution of the following queries + } + } + } else { + [errors appendString:[NSString stringWithFormat:NSLocalizedString(@"[ERROR in query %ld] %@\n", @"error text when multiple custom query failed"), + (long)(i+1), + errorString]]; + } + } else { + [errors setString:errorString]; + } + } else { + // Check if table/db list needs an update + // The regex is a compromise between speed and usefullness. TODO: further improvements are needed + if(!tableListNeedsReload && [query isMatchedByRegex:@"(?i)^\\s*\\b(create|alter|drop|rename)\\b\\s+."]) + tableListNeedsReload = YES; + if(!databaseWasChanged && [query isMatchedByRegex:@"(?i)^\\s*\\b(use|drop\\s+database|drop\\s+schema)\\b\\s+."]) + databaseWasChanged = YES; + } + // If the query was cancelled, end all queries. + if ([mySQLConnection queryCancelled]) break; + } + + // Reload table list if at least one query began with drop, alter, rename, or create + if(tableListNeedsReload || databaseWasChanged) { + // Build database pulldown menu + [tableDocumentInstance setDatabases:self]; + + if (databaseWasChanged) + // Reset the current database + [tableDocumentInstance refreshCurrentDatabase]; + + // Reload table list + [tablesListInstance updateTables:self]; + + } + + if(usedQuery) + [usedQuery release]; + + // if(!queriesSeparatedByDelimiter) // TODO: How to combine queries delimited by DELIMITER? + usedQuery = [[NSString stringWithString:[tempQueries componentsJoinedByString:@";\n"]] retain]; + + lastExecutedQuery = [[tempQueries lastObject] retain]; + + // Perform empty query if no query is given + if ( !queryCount ) { + streamingResult = [mySQLConnection streamingQueryString:@""]; + [streamingResult cancelResultLoad]; + [errors setString:[mySQLConnection getLastErrorMessage]]; + } + + // add query to history + if(!reloadingExistingResult && [usedQuery length]) + [self performSelectorOnMainThread:@selector(addHistoryEntry:) withObject:usedQuery waitUntilDone:NO]; + + // Update status/errors text + NSDictionary *statusDetails = [NSDictionary dictionaryWithObjectsAndKeys: + errors, @"errorString", + [NSNumber numberWithInteger:firstErrorOccuredInQuery], @"firstErrorQueryNumber", + nil]; + [self performSelectorOnMainThread:@selector(updateStatusInterfaceWithDetails:) withObject:statusDetails waitUntilDone:YES]; + + // Set up the status string + if ( [mySQLConnection queryCancelled] ) { + if (totalQueriesRun > 1) { + [[affectedRowsText onMainThread] setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Cancelled in query %ld, after %@", @"text showing multiple queries were cancelled"), + (long)totalQueriesRun, + [NSString stringForTimeInterval:executionTime] + ]]; + } else { + [[affectedRowsText onMainThread] setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Cancelled after %@", @"text showing a query was cancelled"), + [NSString stringForTimeInterval:executionTime] + ]]; + } + } else if ( totalQueriesRun > 1 ) { + if (totalAffectedRows==1) { + [[affectedRowsText onMainThread] setStringValue:[NSString stringWithFormat:NSLocalizedString(@"1 row affected in total, by %ld queries taking %@", @"text showing one row has been affected by multiple queries"), + (long)totalQueriesRun, + [NSString stringForTimeInterval:executionTime] + ]]; + + } else { + [[affectedRowsText onMainThread] setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%ld rows affected in total, by %ld queries taking %@", @"text showing how many rows have been affected by multiple queries"), + (long)totalAffectedRows, + (long)totalQueriesRun, + [NSString stringForTimeInterval:executionTime] + ]]; + + } + } else { + if (totalAffectedRows==1) { + [[affectedRowsText onMainThread] setStringValue:[NSString stringWithFormat:NSLocalizedString(@"1 row affected, taking %@", @"text showing one row has been affected by a single query"), + [NSString stringForTimeInterval:executionTime] + ]]; + } else { + [[affectedRowsText onMainThread] setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%ld rows affected, taking %@", @"text showing how many rows have been affected by a single query"), + (long)totalAffectedRows, + [NSString stringForTimeInterval:executionTime] + ]]; + + } + } + + // Restore automatic query retries + [mySQLConnection setAllowQueryRetries:YES]; + + [tableDocumentInstance setQueryMode:SPInterfaceQueryMode]; + + // If no results were returned, redraw the empty table and post notifications before returning. + if ( !resultDataCount ) { + [customQueryView performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:YES]; + + // Notify any listeners that the query has completed + [[NSNotificationCenter defaultCenter] postNotificationOnMainThreadWithName:@"SMySQLQueryHasBeenPerformed" object:tableDocumentInstance]; + + // Perform the Growl notification for query completion + [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Query Finished" + description:[NSString stringWithFormat:NSLocalizedString(@"%@",@"description for query finished growl notification"), [errorText stringValue]] + document:tableDocumentInstance + notificationName:@"Query Finished"]; + + // Set up the callback if present + if ([taskArguments objectForKey:@"callback"]) { + [[taskArguments objectForKey:@"callback"] getValue:&callbackMethod]; + [self performSelectorOnMainThread:callbackMethod withObject:nil waitUntilDone:NO]; + } + + [tableDocumentInstance endTask]; + [queryRunningPool release]; + + return; + } + + // Find result table name for copying as SQL INSERT. + // If more than one table name is found set resultTableName to nil. + // resultTableName will be set to the original table name (not defined via AS) provided by mysql return + // and the resultTableName can differ due to case-sensitive/insensitive settings!. + NSString *resultTableName = [[cqColumnDefinition objectAtIndex:0] objectForKey:@"org_table"]; + for(id field in cqColumnDefinition) { + if(![[field objectForKey:@"org_table"] isEqualToString:resultTableName]) { + resultTableName = nil; + break; + } + } + + [customQueryView reloadData]; + + // Restore the result view origin if appropriate + if (!NSEqualRects(selectionViewportToRestore, NSZeroRect)) { + + // Scroll the viewport to the saved location + selectionViewportToRestore.size = [customQueryView visibleRect].size; + [customQueryView scrollRectToVisible:selectionViewportToRestore]; + } + + // Restore selection indexes if appropriate + if (selectionIndexToRestore) { + BOOL previousTableRowsSelectable = tableRowsSelectable; + tableRowsSelectable = YES; + [customQueryView selectRowIndexes:selectionIndexToRestore byExtendingSelection:NO]; + tableRowsSelectable = previousTableRowsSelectable; + } + + // Init copyTable with necessary information for copying selected rows as SQL INSERT + [customQueryView setTableInstance:self withTableData:resultData withColumns:cqColumnDefinition withTableName:resultTableName withConnection:mySQLConnection]; + + //query finished + [[NSNotificationCenter defaultCenter] postNotificationOnMainThreadWithName:@"SMySQLQueryHasBeenPerformed" object:tableDocumentInstance]; + + // Query finished Growl notification + [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Query Finished" + description:[NSString stringWithFormat:NSLocalizedString(@"%@",@"description for query finished growl notification"), [errorText stringValue]] + document:tableDocumentInstance + notificationName:@"Query Finished"]; + + // Set up the callback if present + if ([taskArguments objectForKey:@"callback"]) { + [[taskArguments objectForKey:@"callback"] getValue:&callbackMethod]; + [self performSelectorOnMainThread:callbackMethod withObject:nil waitUntilDone:YES]; + } + + [tableDocumentInstance endTask]; + [queryRunningPool release]; +} + +/* + * Processes a supplied streaming result set, loading it into the data array. + */ +- (void)processResultIntoDataStorage:(MCPStreamingResult *)theResult +{ + NSArray *tempRow; + NSUInteger rowsProcessed = 0; + NSUInteger nextTableDisplayBoundary = 50; + NSAutoreleasePool *dataLoadingPool; + BOOL tableViewRedrawn = NO; + + // Remove all items from the table + resultDataCount = 0; + [customQueryView performSelectorOnMainThread:@selector(noteNumberOfRowsChanged) withObject:nil waitUntilDone:YES]; + pthread_mutex_lock(&resultDataLock); + [resultData removeAllRows]; + pthread_mutex_unlock(&resultDataLock); + + // Set the column count on the data store + [resultData setColumnCount:[theResult numOfFields]]; + + // Set up an autorelease pool for row processing + dataLoadingPool = [[NSAutoreleasePool alloc] init]; + + // Loop through the result rows as they become available + while (tempRow = [theResult fetchNextRowAsArray]) { + + pthread_mutex_lock(&resultDataLock); + SPDataStorageAddRow(resultData, tempRow); + resultDataCount++; + pthread_mutex_unlock(&resultDataLock); + + // Update the count of rows processed + rowsProcessed++; + + // Update the table view with new results every now and then + if (rowsProcessed > nextTableDisplayBoundary) { + [customQueryView performSelectorOnMainThread:@selector(noteNumberOfRowsChanged) withObject:nil waitUntilDone:NO]; + if (!tableViewRedrawn) { + [customQueryView performSelectorOnMainThread:@selector(displayIfNeeded) withObject:nil waitUntilDone:NO]; + tableViewRedrawn = YES; + } + nextTableDisplayBoundary *= 2; + } + + // Drain and reset the autorelease pool every ~1024 rows + if (!(rowsProcessed % 1024)) { + [dataLoadingPool drain]; + dataLoadingPool = [[NSAutoreleasePool alloc] init]; + } + } + + [customQueryView performSelectorOnMainThread:@selector(noteNumberOfRowsChanged) withObject:nil waitUntilDone:NO]; + [customQueryView setNeedsDisplay:YES]; + + // Clean up the autorelease pool + [dataLoadingPool drain]; +} + +/* + * Retrieve the range of the query at a position specified + * within the custom query text view. + */ +- (NSRange)queryRangeAtPosition:(NSUInteger)position lookBehind:(BOOL *)doLookBehind +{ + SPSQLParser *customQueryParser; + NSArray *queries; + NSString *query = nil; + NSRange queryRange; + + NSUInteger i, j, queryPosition = 0; + NSUInteger queryCount; + + NSCharacterSet *whitespaceAndNewlineSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; + NSCharacterSet *whitespaceSet = [NSCharacterSet whitespaceCharacterSet]; + + // If the supplied position is negative or beyond the end of the string, return nil. + if (position < 0 || position > [[textView string] length]) + return NSMakeRange(NSNotFound, 0); + + // Split the current text into ranges of queries + // only if the textView was really changed, otherwise use the cache + if([[textView textStorage] editedMask] != 0) { + customQueryParser = [[SPSQLParser alloc] initWithString:[textView string]]; + [customQueryParser setDelimiterSupport:YES]; + queries = [[NSArray alloc] initWithArray:[customQueryParser splitStringIntoRangesByCharacter:';']]; + numberOfQueries = [queries count]; + if(currentQueryRanges) + [currentQueryRanges release]; + currentQueryRanges = [[NSArray arrayWithArray:queries] retain]; + [customQueryParser release]; + } else { + queries = [[NSArray alloc] initWithArray:currentQueryRanges]; + } + + queryCount = [queries count]; + + // Walk along the array of queries to identify the current query - taking into account + // the extra semicolon at the end of each query + for (i = 0; i < queryCount; i++ ) { + + queryRange = [NSArrayObjectAtIndex(queries, i) rangeValue]; + queryPosition = NSMaxRange(queryRange); + queryStartPosition = queryRange.location; + + if (queryPosition >= position) { + + // If lookbehind is enabled, check whether the current position could be considered to + // be within the previous query. A position just after a semicolon is always considered + // to be within the previous query; otherwise, if there is only whitespace *and newlines* + // before the next character, also consider the position to belong to the previous query. + if (*doLookBehind) { + BOOL positionAssociatedWithPreviousQuery = NO; + + // If the caret is at the very start of the string, always associate + if (position == queryStartPosition) positionAssociatedWithPreviousQuery = YES; + + // If the caret is in between a user-defined delimiter whose length is >1, always associate + if (!positionAssociatedWithPreviousQuery && i && NSMaxRange([NSArrayObjectAtIndex(queries, i-1) rangeValue]) < position && position < queryStartPosition) positionAssociatedWithPreviousQuery = YES; + + // Otherwise associate if only whitespace since previous, and a newline before next. + if (!positionAssociatedWithPreviousQuery) { + @try{ + NSString *stringToPrevious = [[textView string] substringWithRange:NSMakeRange(queryStartPosition, position - queryStartPosition)]; + NSString *stringToEnd = [[textView string] substringWithRange:NSMakeRange(position, queryPosition - position)]; + if (![[stringToPrevious stringByTrimmingCharactersInSet:whitespaceAndNewlineSet] length]) { + for (j = 0; j < [stringToEnd length]; j++) { + if ([whitespaceSet characterIsMember:[stringToEnd characterAtIndex:j]]) continue; + if ([whitespaceAndNewlineSet characterIsMember:[stringToEnd characterAtIndex:j]]) { + positionAssociatedWithPreviousQuery = YES; + } + break; + } + } + } @catch(id ae) {} + } + + // If there is a previous query and the position should be associated with it, do so. + if (i && positionAssociatedWithPreviousQuery && [[[[textView string] substringWithRange:[NSArrayObjectAtIndex(queries, i-1) rangeValue]] stringByTrimmingCharactersInSet:whitespaceAndNewlineSet] length]) { + queryRange = [[queries objectAtIndex:i-1] rangeValue]; + break; + } + + // Lookbehind failed - set the pointer to NO so the parent knows. + *doLookBehind = NO; + } + break; + } + } + + // For lookbehinds catch position at the very end of a string ending in a semicolon + if (*doLookBehind && position == [[textView string] length]) + { + queryRange = [[queries lastObject] rangeValue]; + } + + [queries release]; + + + queryRange = NSIntersectionRange(queryRange, NSMakeRange(0, [[textView string] length])); + if (!queryRange.length) { + return NSMakeRange(NSNotFound, 0); + } + + query = [[textView string] substringWithRange:queryRange]; + + // Highlight by setting a background color the current query + // and ignore leading/trailing white spaces + NSInteger biasStart = [query rangeOfRegex:@"^\\s*"].length; + NSInteger biasEnd = [query rangeOfRegex:@"\\s*$"].length; + queryRange.location += biasStart; + queryRange.length -= biasEnd+biasStart; + + // Ensure the string isn't empty. + // (We could also strip comments for this check, but that prevents use of conditional comments) + if(queryRange.length < 1 || queryRange.length > [query length]) { + return NSMakeRange(NSNotFound, 0); + } + + // Return the located query range + return queryRange; +} + +/* + * Retrieve the range of the query for the passed index seen from a start position + * specified within the custom query text view. + */ +- (NSRange)queryTextRangeForQuery:(NSInteger)anIndex startPosition:(NSUInteger)position +{ + SPSQLParser *customQueryParser; + NSArray *queries; + + // If the supplied position is negative or beyond the end of the string, return nil. + if (position < 0 || position > [[textView string] length]) + return NSMakeRange(NSNotFound,0); + + // Split the current text into ranges of queries + customQueryParser = [[SPSQLParser alloc] initWithString:[[textView string] substringWithRange:NSMakeRange(position, [[textView string] length]-position)]]; + [customQueryParser setDelimiterSupport:YES]; + queries = [[NSArray alloc] initWithArray:[customQueryParser splitStringIntoRangesByCharacter:';']]; + [customQueryParser release]; + + // Check for a valid index + anIndex--; + if(anIndex < 0 || anIndex >= [queries count]) + { + [queries release]; + return NSMakeRange(NSNotFound, 0); + } + + NSRange theQueryRange = [[queries objectAtIndex:anIndex] rangeValue]; + NSString *theQueryString = [[textView string] substringWithRange:theQueryRange]; + + [queries release]; + + // Remove all leading and trailing white spaces + NSInteger offset = [theQueryString rangeOfRegex:@"^(\\s*)"].length; + theQueryRange.location += offset; + theQueryRange.length -= offset; + offset = [theQueryString rangeOfRegex:@"(\\s*)$"].length; + theQueryRange.length -= offset; + return theQueryRange; +} + +/* + * Retrieve the query at a position specified within the custom query + * text view. This will return nil if the position specified is beyond + * the available string or if an empty query would be returned. + * If lookBehind is set, returns the *previous* query, but only if the + * caret should be associated with the previous query based on whitespace. + */ +- (NSString *)queryAtPosition:(NSUInteger)position lookBehind:(BOOL *)doLookBehind +{ + + BOOL lookBehind = *doLookBehind; + NSRange queryRange = [self queryRangeAtPosition:position lookBehind:&lookBehind]; + *doLookBehind = lookBehind; + + return (queryRange.length) ? [[textView string] substringWithRange:queryRange] : nil; +} + +- (void)selectCurrentQuery +{ + if(currentQueryRange.length) + [textView setSelectedRange:currentQueryRange]; +} + +/* + * Add or remove "⁄* *⁄" for each line in the current query + * a given selection + */ +- (void)commentOutCurrentQueryTakingSelection:(BOOL)takeSelection +{ + + BOOL isUncomment = NO; + + NSRange oldRange = [textView selectedRange]; + + NSRange workingRange = oldRange; + if(!takeSelection) + workingRange = currentQueryRange; + + NSMutableString *n = [NSMutableString string]; + + [n setString:[[textView string] substringWithRange:workingRange]]; + + if([n isMatchedByRegex:@"\\n\\Z"]) { + workingRange.length--; + [n replaceOccurrencesOfRegex:@"\\n\\Z" withString:@""]; + } + + // Escape given */ by *\/ + [n replaceOccurrencesOfRegex:@"\\*/(?=.)" withString:@"*\\\\/"]; + [n replaceOccurrencesOfRegex:@"\\*/(?=\\n)" withString:@"*\\\\/"]; + + // Wrap current query into /* */ + [n replaceOccurrencesOfRegex:@"^" withString:@"/* "]; + [n appendString:@" */"]; + + // Check if current query/selection is already commented out, if so uncomment it + if([n isMatchedByRegex:@"^/\\* \\s*/\\*\\s*(.|\\n)*?\\s*\\*/ \\*/\\s*$"]) { + [n replaceOccurrencesOfRegex:@"^/\\* \\s*/\\*\\s*" withString:@""]; + [n replaceOccurrencesOfRegex:@"\\s*\\*/ \\*/\\s*\\Z" withString:@""]; + // unescape *\/ + [n replaceOccurrencesOfRegex:@"\\*\\\\/" withString:@"*/"]; + isUncomment = YES; + } + + // Replace current query/selection by (un)commented string + [textView setSelectedRange:workingRange]; + [textView insertText:n]; + + // If commenting out locate the caret just after the first /* to allow to enter + // something like /*!400000 or similar + if(!isUncomment) + [textView setSelectedRange:NSMakeRange(workingRange.location+2,0)]; + +} + +/* + * Add or remove "-- " for each line in the current query or selection, + * if the selection is in-line wrap selection into ⁄* block comments and + * place the caret after ⁄* to allow to enter !xxxxxx e.g. + */ +- (void)commentOut +{ + + NSRange oldRange = [textView selectedRange]; + + if(oldRange.length) { // (un)comment selection + [self commentOutCurrentQueryTakingSelection:YES]; + } else { // single line + + // get the current line range + NSRange lineRange = [[textView string] lineRangeForRange:oldRange]; + NSMutableString *n = [NSMutableString string]; + + // Put "-- " in front of the current line + [n setString:[NSString stringWithFormat:@"-- %@", [[textView string] substringWithRange:lineRange]]]; + + // Check if current line is already commented out, if so uncomment it + // and preserve the original indention via regex:@"^-- (\\s*)" + if([n isMatchedByRegex:@"^-- \\s*(--\\s|#)"]) { + [n replaceOccurrencesOfRegex:@"^-- \\s*(--\\s|#)" + withString:[n substringWithRange:[n rangeOfRegex:@"^-- (\\s*)" + options:RKLNoOptions + inRange:NSMakeRange(0,[n length]) + capture:1 + error: nil]]]; + } else if ([n isMatchedByRegex:@"^-- \\s*/\\*.*? ?\\*/\\s*$"]) { + [n replaceOccurrencesOfRegex:@"^-- \\s*/\\* ?" + withString:[n substringWithRange:[n rangeOfRegex:@"^-- (\\s*)" + options:RKLNoOptions + inRange:NSMakeRange(0,[n length]) + capture:1 + error: nil]]]; + [n replaceOccurrencesOfRegex:@" ?\\*/\\s*$" + withString:[n substringWithRange:[n rangeOfRegex:@" ?\\*/(\\s*)$" + options:RKLNoOptions + inRange:NSMakeRange(0,[n length]) + capture:1 + error: nil]]]; + } + + // Replace current line by (un)commented string + // The caret will be placed at the beginning of the next line if present to + // allow a fast (un)commenting of lines + [textView setSelectedRange:lineRange]; + [textView insertText:n]; + + } + +} + +/* + * Update the interface to reflect the query error state. + * Should be performed on the main thread. + */ +- (void) updateStatusInterfaceWithDetails:(NSDictionary *)errorDetails +{ + NSString *errorsString = [errorDetails objectForKey:@"errorString"]; + NSInteger firstErrorOccuredInQuery = [[errorDetails objectForKey:@"firstErrorQueryNumber"] integerValue]; + + // If errors occur, display them + if ( [mySQLConnection queryCancelled] || ([errorsString length] && !queryIsTableSorter)) { + // set the error text + [errorText setStringValue:errorsString]; + + // try to select the line x of the first error if error message with ID 1064 contains "at line x" + // by capturing the last number of the error string + NSRange errorLineNumberRange = [errorsString rangeOfRegex:@"([0-9]+)[^0-9]*$" options:RKLNoOptions inRange:NSMakeRange(0, [errorsString length]) capture:1L error:nil]; + + // if error ID 1064 and a line number was found + if([mySQLConnection getLastErrorID] == 1064 && errorLineNumberRange.length) + { + // Get the line number + NSUInteger errorAtLine = [[errorsString substringWithRange:errorLineNumberRange] integerValue]; + NSUInteger lineOffset = [textView getLineNumberForCharacterIndex:[self queryTextRangeForQuery:firstErrorOccuredInQuery startPosition:queryStartPosition].location] - 1; + + // Check for near message + NSRange errorNearMessageRange = [errorsString rangeOfRegex:@"[( ]'(.+)'[ -]" options:(RKLMultiline|RKLDotAll) inRange:NSMakeRange(0, [errorsString length]) capture:1L error:nil]; + if(errorNearMessageRange.length) // if a "near message" was found + { + NSUInteger bufferLength = [[textView string] length]; + + NSRange bufferRange = NSMakeRange(0, bufferLength); + + // Build the range to search for nearMessage (beginning from queryStartPosition to try to avoid mismatching) + NSRange theRange = NSMakeRange(queryStartPosition, bufferLength-queryStartPosition); + theRange = NSIntersectionRange(bufferRange, theRange); + + // Get the range in textView of the near message + NSRange textNearMessageRange = [[[textView string] substringWithRange:theRange] rangeOfString:[errorsString substringWithRange:errorNearMessageRange] options:NSLiteralSearch]; + + // Correct the near message range relative to queryStartPosition + textNearMessageRange = NSMakeRange(textNearMessageRange.location+queryStartPosition, textNearMessageRange.length); + textNearMessageRange = NSIntersectionRange(bufferRange, textNearMessageRange); + + // Select the near message and scroll to it + if(textNearMessageRange.length > 0) { + [textView setSelectedRange:textNearMessageRange]; + [textView scrollRangeToVisible:textNearMessageRange]; + } + } else { + [textView selectLineNumber:errorAtLine+lineOffset ignoreLeadingNewLines:YES]; + } + } else { // Select first erroneous query entirely + + NSRange queryRange; + if(firstErrorOccuredInQuery == -1) // for current or previous query + { + BOOL isLookBehind = YES; + queryRange = [self queryRangeAtPosition:[textView selectedRange].location lookBehind:&isLookBehind]; + if(queryRange.length) + [textView setSelectedRange:queryRange]; + } else { + // select the query for which the first error was detected + queryRange = [self queryTextRangeForQuery:firstErrorOccuredInQuery startPosition:queryStartPosition]; + queryRange = NSIntersectionRange(NSMakeRange(0, [[textView string] length]), queryRange); + [textView setSelectedRange:queryRange]; + [textView scrollRangeToVisible:queryRange]; + } + } + + } else if ( [errorsString length] && queryIsTableSorter ) { + [errorText setStringValue:NSLocalizedString(@"Couldn't sort column.", @"text shown if an error occured while sorting the result table")]; + NSBeep(); + } else { + [errorText setStringValue:NSLocalizedString(@"There were no errors.", @"text shown when query was successfull")]; + } +} + +#pragma mark - +#pragma mark Accessors + +/* + * Returns the current result (as shown in custom result view) as array, + * the first object containing the field names as array, + * the following objects containing the rows as array + */ +- (NSArray *)currentResult +{ + NSArray *tableColumns = [customQueryView tableColumns]; + NSEnumerator *enumerator = [tableColumns objectEnumerator]; + id tableColumn; + NSMutableArray *currentResult = [NSMutableArray array]; + NSMutableArray *tempRow = [NSMutableArray array]; + NSInteger i; + + //set field names as first line + while ( (tableColumn = [enumerator nextObject]) ) { + [tempRow addObject:[[tableColumn headerCell] stringValue]]; + } + [currentResult addObject:[NSArray arrayWithArray:tempRow]]; + + //add rows + for ( i = 0 ; i < [self numberOfRowsInTableView:customQueryView] ; i++) { + [tempRow removeAllObjects]; + enumerator = [tableColumns objectEnumerator]; + while ( (tableColumn = [enumerator nextObject]) ) { + [tempRow addObject:[self tableView:customQueryView objectValueForTableColumn:tableColumn row:i]]; + } + [currentResult addObject:[NSArray arrayWithArray:tempRow]]; + } + return currentResult; +} + +#pragma mark - +#pragma mark Additional methods + +/* + * Sets the connection (received from SPDatabaseDocument) and makes things that have to be done only once + */ +- (void)setConnection:(MCPConnection *)theConnection +{ + mySQLConnection = theConnection; + currentQueryRanges = nil; + + // Set up the interface + + [customQueryView setVerticalMotionCanBeginDrag:NO]; + [autoindentMenuItem setState:([prefs boolForKey:SPCustomQueryAutoIndent]?NSOnState:NSOffState)]; + [autopairMenuItem setState:([prefs boolForKey:SPCustomQueryAutoPairCharacters]?NSOnState:NSOffState)]; + [autohelpMenuItem setState:([prefs boolForKey:SPCustomQueryUpdateAutoHelp]?NSOnState:NSOffState)]; + [autouppercaseKeywordsMenuItem setState:([prefs boolForKey:SPCustomQueryAutoUppercaseKeywords]?NSOnState:NSOffState)]; + + if ( [[SPQueryController sharedQueryController] historyForFileURL:[tableDocumentInstance fileURL]] ) + [self performSelectorOnMainThread:@selector(historyItemsHaveBeenUpdated:) withObject:self waitUntilDone:YES]; + + // Populate query favorites + [self queryFavoritesHaveBeenUpdated:nil]; + + // Disable runSelectionMenuItem in the gear menu + [runSelectionMenuItem setEnabled:NO]; +} + +/* + * Inserts the query in the textView and performs query + */ +- (void)doPerformQueryService:(NSString *)query +{ + [textView shouldChangeTextInRange:NSMakeRange(0, [[textView string] length]) replacementString:query]; + [textView setString:query]; + [textView didChangeText]; + [textView scrollRangeToVisible:NSMakeRange([query length], 0)]; + [self runAllQueries:self]; +} +- (void)doPerformLoadQueryService:(NSString *)query +{ + [textView shouldChangeTextInRange:NSMakeRange(0, [[textView string] length]) replacementString:query]; + [textView setString:query]; + [textView didChangeText]; + [textView scrollRangeToVisible:NSMakeRange([query length], 0)]; +} + +- (NSString *)usedQuery +{ + return usedQuery; +} + +#pragma mark - +#pragma mark Retrieving and setting table state + +/** + * Update the results table view state to match the current column definitions. + * Should be called on the main thread. + */ +- (void) updateTableView +{ + NSArray *theColumns; + NSTableColumn *theCol; + + // Remove all existing columns from the table + theColumns = [customQueryView tableColumns]; + while ([theColumns count]) { + [customQueryView removeTableColumn:NSArrayObjectAtIndex(theColumns, 0)]; + } + + // Update font size on the table + NSFont *tableFont = [NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPGlobalResultTableFont]]; + [customQueryView setRowHeight:2.0f+NSSizeToCGSize([[NSString stringWithString:@"{ǞṶḹÜ∑zgyf"] sizeWithAttributes:[NSDictionary dictionaryWithObject:tableFont forKey:NSFontAttributeName]]).height]; + + // If there are no table columns to add, return + if (!cqColumnDefinition || ![cqColumnDefinition count]) return; + + // Add the new table columns + for (NSDictionary *columnDefinition in cqColumnDefinition) { + theCol = [[NSTableColumn alloc] initWithIdentifier:[columnDefinition objectForKey:@"datacolumnindex"]]; + [theCol setResizingMask:NSTableColumnUserResizingMask]; + [theCol setEditable:YES]; + SPTextAndLinkCell *dataCell = [[[SPTextAndLinkCell alloc] initTextCell:@""] autorelease]; + [dataCell setEditable:YES]; + [dataCell setFormatter:[[SPDataCellFormatter new] autorelease]]; + [dataCell setFont:tableFont]; + + [dataCell setLineBreakMode:NSLineBreakByTruncatingTail]; + [theCol setDataCell:dataCell]; + [[theCol headerCell] setStringValue:[columnDefinition objectForKey:@"name"]]; + + // Set the width of this column to saved value if exists and maps to a real column + if ([columnDefinition objectForKey:@"org_name"] && [(NSString *)[columnDefinition objectForKey:@"org_name"] length]) { + NSNumber *colWidth = [[[[prefs objectForKey:SPTableColumnWidths] objectForKey:[NSString stringWithFormat:@"%@@%@", [columnDefinition objectForKey:@"db"], [tableDocumentInstance host]]] objectForKey:[columnDefinition objectForKey:@"org_table"]] objectForKey:[columnDefinition objectForKey:@"org_name"]]; + if ( colWidth ) { + [theCol setWidth:[colWidth doubleValue]]; + } + } + + [customQueryView addTableColumn:theCol]; + [theCol release]; + } + + [customQueryView sizeLastColumnToFit]; + + //tries to fix problem with last row (otherwise to small) + //sets last column to width of the first if smaller than 30 + //problem not fixed for resizing window + if ( [[customQueryView tableColumnWithIdentifier:[NSNumber numberWithInteger:[theColumns count]-1]] width] < 30 ) + [[customQueryView tableColumnWithIdentifier:[NSNumber numberWithInteger:[theColumns count]-1]] + setWidth:[[customQueryView tableColumnWithIdentifier:[NSNumber numberWithInteger:0]] width]]; +} + +/** + * Provide a getter for the custom query result table's selected rows index set + */ +- (NSIndexSet *) resultSelectedRowIndexes +{ + return [customQueryView selectedRowIndexes]; +} + +/** + * Provide a getter for the custom query result table's current viewport + */ +- (NSRect) resultViewport +{ + return [customQueryView visibleRect]; +} + +/** + * Set the selected row indexes to restore on next custom query result table load + */ +- (void) setResultSelectedRowIndexesToRestore:(NSIndexSet *)theIndexSet +{ + if (selectionIndexToRestore) [selectionIndexToRestore release], selectionIndexToRestore = nil; + + if (theIndexSet) selectionIndexToRestore = [[NSIndexSet alloc] initWithIndexSet:theIndexSet]; +} + +/** + * Set the viewport to restore on next table load + */ +- (void) setResultViewportToRestore:(NSRect)theViewport +{ + selectionViewportToRestore = theViewport; +} + +/** + * Convenience method for storing all current settings for restoration + */ +- (void) storeCurrentResultViewForRestoration +{ + [self setResultSelectedRowIndexesToRestore:[self resultSelectedRowIndexes]]; + [self setResultViewportToRestore:[self resultViewport]]; +} + +/** + * Convenience method for clearing any settings to restore + */ +- (void) clearResultViewDetailsToRestore +{ + [self setResultSelectedRowIndexesToRestore:nil]; + [self setResultViewportToRestore:NSZeroRect]; +} + +#pragma mark - +#pragma mark Field Editing + +/* + * Collect all columns for a given 'tableForColumn' table and + * return a WHERE clause for identifying the field in quesyion. + */ +- (NSString *)argumentForRow:(NSUInteger)rowIndex ofTable:(NSString *)tableForColumn andDatabase:(NSString *)database +{ + NSArray *dataRow; + NSDictionary *theRow; + id field; + + //Look for all columns which are coming from "tableForColumn" + NSMutableArray *columnsForFieldTableName = [NSMutableArray array]; + for(field in cqColumnDefinition) { + if([[field objectForKey:@"org_table"] isEqualToString:tableForColumn]) + [columnsForFieldTableName addObject:field]; + } + + // Try to identify the field bijectively + NSMutableString *fieldIDQueryStr = [NSMutableString string]; + [fieldIDQueryStr setString:@"WHERE ("]; + + // --- Build WHERE clause --- + dataRow = [resultData rowContentsAtIndex:rowIndex]; + + // Get the primary key if there is one + MCPResult *theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM %@.%@", + [database backtickQuotedString], [tableForColumn backtickQuotedString]]]; + [theResult setReturnDataAsStrings:YES]; + if ([theResult numOfRows]) [theResult dataSeek:0]; + NSInteger i; + for ( i = 0 ; i < [theResult numOfRows] ; i++ ) { + theRow = [theResult fetchRowAsDictionary]; + if ( [[theRow objectForKey:@"Key"] isEqualToString:@"PRI"] ) { + for(field in columnsForFieldTableName) { + id aValue = [dataRow objectAtIndex:[[field objectForKey:@"datacolumnindex"] integerValue]]; + if([[field objectForKey:@"org_name"] isEqualToString:[theRow objectForKey:@"Field"]]) { + [fieldIDQueryStr appendFormat:@"%@.%@.%@ = %@)", + [database backtickQuotedString], + [tableForColumn backtickQuotedString], + [[theRow objectForKey:@"Field"] backtickQuotedString], + [aValue description]]; + return fieldIDQueryStr; + } + } + } + } + + // If there is no primary key, all found fields belonging to the same table are used in the argument + for(field in columnsForFieldTableName) { + id aValue = [dataRow objectAtIndex:[[field objectForKey:@"datacolumnindex"] integerValue]]; + if ([aValue isKindOfClass:[NSNull class]] || [aValue isNSNull]) { + [fieldIDQueryStr appendFormat:@"%@ IS NULL", [[field objectForKey:@"org_name"] backtickQuotedString]]; + } else { + [fieldIDQueryStr appendFormat:@"%@=", [[field objectForKey:@"org_name"] backtickQuotedString]]; + if ([[field objectForKey:@"typegrouping"] isEqualToString:@"textdata"]) + [fieldIDQueryStr appendFormat:@"'%@'", [mySQLConnection prepareString:aValue]]; + else if ([[field objectForKey:@"typegrouping"] isEqualToString:@"blobdata"]) + [fieldIDQueryStr appendFormat:@"X'%@'", [mySQLConnection prepareBinaryData:aValue]]; + else if ([[field objectForKey:@"typegrouping"] isEqualToString:@"integer"]) + [fieldIDQueryStr appendFormat:@"%@", [aValue description]]; + else + [fieldIDQueryStr appendFormat:@"'%@'", [mySQLConnection prepareString:aValue]]; + } + + [fieldIDQueryStr appendString:@" AND "]; + } + // Remove last " AND " + if([fieldIDQueryStr length]>12) + [fieldIDQueryStr replaceCharactersInRange:NSMakeRange([fieldIDQueryStr length]-5,5) withString:@")"]; + + return fieldIDQueryStr; +} + +#pragma mark - +#pragma mark TableView datasource methods + +/** + * Returns the number of rows in the result set table view. + */ +- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView +{ + if (aTableView == customQueryView) { + return (resultData == nil) ? 0 : resultDataCount; + } + else { + return 0; + } +} + +/** + * This function changes the text color of text/blob fields whose content is NULL. + */ +- (void)tableView:(CMCopyTable *)aTableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn*)aTableColumn row:(NSInteger)rowIndex +{ + if (aTableView == customQueryView) { + + // For NULL cell's display the user's NULL value placeholder in grey to easily distinguish it from other values + if ([cell respondsToSelector:@selector(setTextColor:)]) { + NSUInteger columnIndex = [[aTableColumn identifier] integerValue]; + id theValue = nil; + + // While the table is being loaded, additional validation is required - data + // locks must be used to avoid crashes, and indexes higher than the available + // rows or columns may be requested. Use gray to show loading in these cases. + if (isWorking) { + pthread_mutex_lock(&resultDataLock); + if (rowIndex < resultDataCount && columnIndex < [resultData columnCount]) { + theValue = SPDataStorageObjectAtRowAndColumn(resultData, rowIndex, columnIndex); + } + pthread_mutex_unlock(&resultDataLock); + + if (!theValue) { + [cell setTextColor:[NSColor lightGrayColor]]; + return; + } + } else { + theValue = SPDataStorageObjectAtRowAndColumn(resultData, rowIndex, columnIndex); + } + + [cell setTextColor:[theValue isNSNull] ? [NSColor lightGrayColor] : [NSColor blackColor]]; + } + } +} + +/** + * Returns the object for the requested column and row index. + */ +- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex +{ + if (aTableView == customQueryView) { + NSUInteger columnIndex = [[aTableColumn identifier] integerValue]; + id theValue = nil; + + // While the table is being loaded, additional validation is required - data + // locks must be used to avoid crashes, and indexes higher than the available + // rows or columns may be requested. Return "..." to indicate loading in these + // cases. + if (isWorking) { + pthread_mutex_lock(&resultDataLock); + if (rowIndex < resultDataCount && columnIndex < [resultData columnCount]) { + theValue = [[SPDataStorageObjectAtRowAndColumn(resultData, rowIndex, columnIndex) copy] autorelease]; + } + pthread_mutex_unlock(&resultDataLock); + + if (!theValue) return @"..."; + } else { + theValue = SPDataStorageObjectAtRowAndColumn(resultData, rowIndex, columnIndex); + } + + if ([theValue isKindOfClass:[NSData class]]) + return [theValue shortStringRepresentationUsingEncoding:[mySQLConnection encoding]]; + + if ([theValue isNSNull]) + return [prefs objectForKey:SPNullValue]; + + return theValue; + } + else { + return @""; + } +} + +- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex +{ + if (aTableView == customQueryView) { + + // Field editing + if (fieldIDQueryString == nil) return; + + NSDictionary *columnDefinition; + + // Retrieve the column defintion + for(id c in cqColumnDefinition) { + if([[c objectForKey:@"datacolumnindex"] isEqualToNumber:[aTableColumn identifier]]) { + columnDefinition = [NSDictionary dictionaryWithDictionary:c]; + break; + } + } + + // Resolve the original table name for current column if AS was used + NSString *tableForColumn = [columnDefinition objectForKey:@"org_table"]; + + if(!tableForColumn || ![tableForColumn length]) { + [errorText setStringValue:[NSString stringWithFormat:@"Couldn't identify field origin unambiguously. The column '%@' contains data from more than one table.", [columnDefinition objectForKey:@"name"]]]; + NSBeep(); + return; + } + + // Resolve the original column name if AS was used + NSString *columnName = [columnDefinition objectForKey:@"org_name"]; + + // NSString *fieldIDQueryString = [self argumentForRow:rowIndex ofTable:tableForColumn]; + + // Check if the IDstring identifies the current field bijectively + NSInteger numberOfPossibleUpdateRows = [[[[mySQLConnection queryString:[NSString stringWithFormat:@"SELECT COUNT(1) FROM %@.%@ %@", [[columnDefinition objectForKey:@"db"] backtickQuotedString], [tableForColumn backtickQuotedString], fieldIDQueryString]] fetchRowAsArray] objectAtIndex:0] integerValue]; + if(numberOfPossibleUpdateRows == 1) { + // [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:tableDocumentInstance]; + + NSString *newObject = nil; + if ( [anObject isKindOfClass:[NSCalendarDate class]] ) { + newObject = [NSString stringWithFormat:@"'%@'", [mySQLConnection prepareString:[anObject description]]]; + } else if ( [anObject isKindOfClass:[NSNumber class]] ) { + newObject = [anObject stringValue]; + } else if ( [anObject isKindOfClass:[NSData class]] ) { + newObject = [NSString stringWithFormat:@"X'%@'", [mySQLConnection prepareBinaryData:anObject]]; + } else { + if ( [[anObject description] isEqualToString:@"CURRENT_TIMESTAMP"] ) { + newObject = @"CURRENT_TIMESTAMP"; + } else if([anObject isEqualToString:[prefs stringForKey:SPNullValue]]) { + newObject = @"NULL"; + } else if ([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"bit"]) { + newObject = ((![[anObject description] length] || [[anObject description] isEqualToString:@"0"])?@"0":@"1"); + } else if ([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"date"] + && [[anObject description] isEqualToString:@"NOW()"]) { + newObject = @"NOW()"; + } else { + newObject = [NSString stringWithFormat:@"'%@'", [mySQLConnection prepareString:[anObject description]]]; + } + } + + [mySQLConnection queryString: + [NSString stringWithFormat:@"UPDATE %@.%@ SET %@.%@.%@=%@ %@ LIMIT 1", + [[columnDefinition objectForKey:@"db"] backtickQuotedString], [tableForColumn backtickQuotedString], + [[columnDefinition objectForKey:@"db"] backtickQuotedString], [tableForColumn backtickQuotedString], [columnName backtickQuotedString], newObject, fieldIDQueryString]]; + + // [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:tableDocumentInstance]; + + // Check for errors while UPDATE + if ([mySQLConnection queryErrored]) { + SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), NSLocalizedString(@"Cancel", @"cancel button"), nil, [tableDocumentInstance parentWindow], self, nil, nil, + [NSString stringWithFormat:NSLocalizedString(@"Couldn't write field.\nMySQL said: %@", @"message of panel when error while updating field to db"), [mySQLConnection getLastErrorMessage]]); + + return; + } + + + // This shouldn't happen – for safety reasons + if ( ![mySQLConnection affectedRows] ) { + if ( [prefs boolForKey:SPShowNoAffectedRowsError] ) { + SPBeginAlertSheet(NSLocalizedString(@"Warning", @"warning"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocumentInstance parentWindow], self, nil, nil, + NSLocalizedString(@"The row was not written to the MySQL database. You probably haven't changed anything.\nReload the table to be sure that the row exists and use a primary key for your table.\n(This error can be turned off in the preferences.)", @"message of panel when no rows have been affected after writing to the db")); + } else { + NSBeep(); + } + return; + } + + // On success reload table data by executing the last query if reloading is enabled + if ([prefs boolForKey:SPReloadAfterEditingRow]) { + reloadingExistingResult = YES; + [self storeCurrentResultViewForRestoration]; + + [self performQueries:[NSArray arrayWithObject:lastExecutedQuery] withCallback:NULL]; + } else { + // otherwise, just update the data in the data storage + SPDataStorageReplaceObjectAtRowAndColumn(resultData, rowIndex, [[aTableColumn identifier] intValue], anObject); + } + } else { + SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocumentInstance parentWindow], self, nil, nil, + [NSString stringWithFormat:NSLocalizedString(@"Updating field content failed. Couldn't identify field origin unambiguously (%ld match%@). It's very likely that while editing this field the table `%@` was changed by an other user.", @"message of panel when error while updating field to db after enabling it"), + (long)numberOfPossibleUpdateRows, (numberOfPossibleUpdateRows>1)?@"es":@"", tableForColumn]); + + } + + } +} + +/* + * Change the sort order by clicking at a column header + */ +- (void)tableView:(NSTableView*)tableView didClickTableColumn:(NSTableColumn *)tableColumn +{ + + // Prevent sorting while a query is running + if ([tableDocumentInstance isWorking]) return; + if (!cqColumnDefinition || ![cqColumnDefinition count]) return; + + NSMutableString *queryString = [NSMutableString stringWithString:lastExecutedQuery]; + + //sets order descending if a header is clicked twice + if ( sortField && [[tableColumn identifier] isEqualToNumber:sortField] ) { + isDesc = !isDesc; + } else { + isDesc = NO; + if (sortField) [customQueryView setIndicatorImage:nil inTableColumn:[customQueryView tableColumnWithIdentifier:sortField]]; + } + + if (sortField) [sortField release]; + sortField = [[NSNumber alloc] initWithInteger:[[tableColumn identifier] integerValue]]; + + // Order by the column position number to avoid ambiguous name errors + NSString* newOrder = [NSString stringWithFormat:@" ORDER BY %ld %@ ", (long)([[tableColumn identifier] integerValue]+1), (isDesc)?@"DESC":@"ASC"]; + + // Remove any comments + [queryString replaceOccurrencesOfRegex:@"--.*?\n" withString:@""]; + [queryString replaceOccurrencesOfRegex:@"--.*?$" withString:@""]; + [queryString replaceOccurrencesOfRegex:@"/\\*(.|\n)*?\\*/" withString:@""]; + + // Remove all quoted strings as a temp string to match the correct clauses + NSRange matchedRange; + NSInteger i; + NSMutableString *tmpString = [NSMutableString stringWithString:queryString]; + NSMutableString *qq = [NSMutableString string]; + matchedRange = [tmpString rangeOfRegex:@"\"(?:[^\"\\\\]*+|\\\\.)*\""]; + // Replace all "..." with _'s + while(matchedRange.length) { + [qq setString:@""]; + for(i=0; i<matchedRange.length; i++) [qq appendString:@"_"]; + [tmpString replaceCharactersInRange:matchedRange withString:qq]; + [tmpString flushCachedRegexData]; + matchedRange = [tmpString rangeOfRegex:@"\"(?:[^\"\\\\]*+|\\\\.)*\""]; + } + // Replace all '...' with _'s + matchedRange = [tmpString rangeOfRegex:@"'(?:[^'\\\\]*+|\\\\.)*'"]; + while(matchedRange.length) { + [qq setString:@""]; + for(i=0; i<matchedRange.length; i++) [qq appendString:@"_"]; + [tmpString replaceCharactersInRange:matchedRange withString:qq]; + [tmpString flushCachedRegexData]; + matchedRange = [tmpString rangeOfRegex:@"'(?:[^'\\\\]*+|\\\\.)*'"]; + } + // Replace all `...` with _'s + matchedRange = [tmpString rangeOfRegex:@"`(?:[^`\\\\]*+|\\\\.)*`"]; + while(matchedRange.length) { + [qq setString:@""]; + for(i=0; i<matchedRange.length; i++) [qq appendString:@"_"]; + [tmpString replaceCharactersInRange:matchedRange withString:qq]; + [tmpString flushCachedRegexData]; + matchedRange = [tmpString rangeOfRegex:@"`(?:[^`\\\\]*+|\\\\.)*`"]; + } + + // Check for an existing ORDER clause (in the temp string), + // if so replace it by the new one (in the actual string) + // Test for ORDER clause inside a statement + if([tmpString isMatchedByRegex:@"(?i)\\s+ORDER\\s+BY\\s+(.|\\n)+(\\s+(DESC|ASC))?(\\s|\\n)+(?=(LI|PR|IN|FO|LO))"]) + { + matchedRange = [tmpString rangeOfRegex:@"(?i)\\s+ORDER\\s+BY\\s+(.|\\n)+(\\s+(DESC|ASC))?(\\s|\\n)+(?=(LI|PR|IN|FO|LO))"]; + [queryString replaceCharactersInRange:matchedRange withString:newOrder]; + } + // Test for ORDER clause at the end + else if ([tmpString isMatchedByRegex:@"(?i)\\s+ORDER\\s+BY\\s+(.|\\n)+((\\s|\\n)+(DESC|ASC))?"]) + { + matchedRange = [tmpString rangeOfRegex:@"(?i)\\s+ORDER\\s+BY\\s+(.|\\n)+((\\s|\\n)+(DESC|ASC))?"]; + [queryString replaceCharactersInRange:matchedRange withString:newOrder]; + } + // No ORDER clause found + // ORDER clause has to be inserted before LIMIT, PROCEDURE, INTO, FOR, or LOCK due to MySQL syntax for SELECT + else if([tmpString isMatchedByRegex:@"(?i)\\bSELECT\\b((.|\\n)+?)\\s*(?=(\\sLIMIT\\s|\\sPROCEDURE\\s|\\sINTO\\s|\\sFOR\\s|\\sLOCK\\s))"]) + { + matchedRange = [tmpString rangeOfRegex:@"(?i)\\bSELECT\\b((.|\\n)+?)(?=(\\sLIMIT\\s|\\sPROCEDURE\\s|\\sINTO\\s|\\sFOR\\s|\\sLOCK\\s))" capture:1]; + NSString *orderHeader = [NSString stringWithFormat:@"%@ %@", [queryString substringWithRange:matchedRange], newOrder]; + [queryString replaceCharactersInRange:matchedRange withString:orderHeader]; + } + // Otherwise append the new ORDER clause at the end + else + [queryString appendFormat:@" %@", newOrder]; + + reloadingExistingResult = YES; + [self storeCurrentResultViewForRestoration]; + queryIsTableSorter = YES; + sortColumn = tableColumn; + [self performQueries:[NSArray arrayWithObject:queryString] withCallback:@selector(tableSortCallback)]; +} + +- (void) tableSortCallback +{ + queryIsTableSorter = NO; + + if ([mySQLConnection queryErrored]) { + sortColumn = nil; + return; + } + + //sets highlight and indicatorImage + [customQueryView setHighlightedTableColumn:sortColumn]; + if ( isDesc ) + [customQueryView setIndicatorImage:[NSImage imageNamed:@"NSDescendingSortIndicator"] inTableColumn:sortColumn]; + else + [customQueryView setIndicatorImage:[NSImage imageNamed:@"NSAscendingSortIndicator"] inTableColumn:sortColumn]; + +} + + +#pragma mark - +#pragma mark TableView Drag & Drop datasource methods + +- (BOOL)tableView:(NSTableView *)aTableView writeRowsWithIndexes:(NSIndexSet *)rows toPasteboard:(NSPasteboard*)pboard +{ + if ( aTableView == customQueryView ) { + NSString *tmp = [customQueryView draggedRowsAsTabString]; + if ( nil != tmp ) + { + [pboard declareTypes:[NSArray arrayWithObjects: NSTabularTextPboardType, + NSStringPboardType, nil] + owner:nil]; + [pboard setString:tmp forType:NSStringPboardType]; + [pboard setString:tmp forType:NSTabularTextPboardType]; + return YES; + } + return NO; + } else { + return NO; + } +} + +/*- (NSDragOperation)tableView:(NSTableView*)aTableView validateDrop:(id <NSDraggingInfo>)info proposedRow:(int)row + proposedDropOperation:(NSTableViewDropOperation)operation +{ + NSArray *pboardTypes = [[info draggingPasteboard] types]; + int originalRow; + + if ( aTableView == queryFavoritesView ) { + if ([pboardTypes count] == 1 && row != -1) + { + if ([[pboardTypes objectAtIndex:0] isEqualToString:@"SequelProPasteboard"]==YES && operation==NSTableViewDropAbove) + { + originalRow = [[[info draggingPasteboard] stringForType:@"SequelProPasteboard"] intValue]; + + if (row != originalRow && row != (originalRow+1)) + { + return NSDragOperationMove; + } + } + } + return NSDragOperationNone; + } else { + return NSDragOperationNone; + } +} + +- (BOOL)tableView:(NSTableView*)aTableView acceptDrop:(id <NSDraggingInfo>)info row:(int)row dropOperation:(NSTableViewDropOperation)operation +{ + int originalRow; + int destinationRow; + NSMutableDictionary *draggedRow; + + if ( aTableView == queryFavoritesView ) { + originalRow = [[[info draggingPasteboard] stringForType:@"SequelProPasteboard"] intValue]; + destinationRow = row; + + if ( destinationRow > originalRow ) + destinationRow--; + + draggedRow = [queryFavorites objectAtIndex:originalRow]; + [queryFavorites removeObjectAtIndex:originalRow]; + [queryFavorites insertObject:draggedRow atIndex:destinationRow]; + + [queryFavoritesView reloadData]; + [queryFavoritesView selectRowIndexes:[NSIndexSet indexSetWithIndex:destinationRow] byExtendingSelection:NO]; + + return YES; + } else { + return NO; + } +}*/ + +#pragma mark - +#pragma mark TableView delegate methods + +/** + * Show the table cell content as tooltip + * - for text displays line breaks and tabs as well + * - if blob data can be interpret as image data display the image as transparent thumbnail + * (up to now using base64 encoded HTML data) + */ +- (NSString *)tableView:(NSTableView *)aTableView toolTipForCell:(SPTextAndLinkCell *)aCell rect:(NSRectPointer)rect tableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)row mouseLocation:(NSPoint)mouseLocation +{ + + if([[aCell stringValue] length] < 2 || [tableDocumentInstance isWorking]) return nil; + + NSImage *image; + + NSPoint pos = [NSEvent mouseLocation]; + pos.y -= 20; + + // Try to get the original data. If not possible return nil. + // @try clause is used due to the multifarious cases of + // possible exceptions (eg for reloading tables etc.) + id theValue; + @try{ + theValue = SPDataStorageObjectAtRowAndColumn(resultData, row, [[aTableColumn identifier] integerValue]); + } + @catch(id ae) { + return nil; + } + + // Get the original data for trying to display the blob data as an image + if ([theValue isKindOfClass:[NSData class]]) { + image = [[[NSImage alloc] initWithData:theValue] autorelease]; + if(image) { + [SPTooltip showWithObject:image atLocation:pos ofType:@"image"]; + return nil; + } + } + + // Show the cell string value as tooltip (including line breaks and tabs) + // by using the cell's font + [SPTooltip showWithObject:[aCell stringValue] + atLocation:pos + ofType:@"text" + displayOptions:[NSDictionary dictionaryWithObjectsAndKeys: + [[aCell font] familyName], @"fontname", + [NSString stringWithFormat:@"%f",[[aCell font] pointSize]], @"fontsize", + nil]]; + + return nil; +} + +/* + * Double-click action on a field + */ +- (BOOL)tableView:(NSTableView *)aTableView shouldEditTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex +{ + + // Only allow editing if a task is not active + if ([tableDocumentInstance isWorking]) return NO; + + // Check if the field can identified bijectively + if ( aTableView == customQueryView ) { + + + NSDictionary *columnDefinition; + BOOL noTableName = NO; + BOOL isFieldEditable; + BOOL isBlob; + NSInteger numberOfPossibleUpdateRows = -1; + + // Retrieve the column defintion + for(id c in cqColumnDefinition) { + if([[c objectForKey:@"datacolumnindex"] isEqualToNumber:[aTableColumn identifier]]) { + columnDefinition = [NSDictionary dictionaryWithDictionary:c]; + break; + } + } + + // Check if current field is a blob + if([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"textdata"] + || [[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"blobdata"]) + isBlob = YES; + else + isBlob = NO; + + // Resolve the original table name for current column if AS was used + NSString *tableForColumn = [columnDefinition objectForKey:@"org_table"]; + + // Get the database name which the field belongs to + NSString *dbForColumn = [columnDefinition objectForKey:@"db"]; + + // No table/database name found indicates that the field's column contains data from more than one table as for UNION + // or the field data are not bound to any table as in SELECT 1 or if column database is unset + if(!tableForColumn || ![tableForColumn length] || ![dbForColumn length]) + noTableName = YES; + + if(!noTableName) { + // if table and database name are given check if field can be identified unambiguously + fieldIDQueryString = [self argumentForRow:rowIndex ofTable:tableForColumn andDatabase:[columnDefinition objectForKey:@"db"]]; + + // Actual check whether field can be identified bijectively + numberOfPossibleUpdateRows = [[[[mySQLConnection queryString:[NSString stringWithFormat:@"SELECT COUNT(1) FROM %@.%@ %@", [[columnDefinition objectForKey:@"db"] backtickQuotedString], [tableForColumn backtickQuotedString], fieldIDQueryString]] fetchRowAsArray] objectAtIndex:0] integerValue]; + + isFieldEditable = (numberOfPossibleUpdateRows == 1) ? YES : NO; + + if(!isFieldEditable) + if(numberOfPossibleUpdateRows == 0) + [errorText setStringValue:[NSString stringWithFormat:@"Field is not editable. No matching record found. Try to add the primary key field or more fields in your SELECT statement for table '%@' to identify field origin unambiguously.", tableForColumn]]; + else + [errorText setStringValue:[NSString stringWithFormat:@"Field is not editable. Couldn't identify field origin unambiguously (%ld match%@).", (long)numberOfPossibleUpdateRows, (numberOfPossibleUpdateRows>1)?@"es":@""]]; + + } else { + // no table/databse name are given + isFieldEditable = NO; + fieldIDQueryString = nil; + [errorText setStringValue:NSLocalizedString(@"Field is not editable. Field has no or multiple table or database origin(s).",@"field is not editable due to no table/database")]; + } + + + SPFieldEditorController *fieldEditor = [[SPFieldEditorController alloc] init]; + + // Remember edited row for reselecting and setting the scroll view after reload + editedRow = rowIndex; + editedScrollViewRect = [customQueryScrollView documentVisibleRect]; + + // Set max text length + if ([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"string"] + && [columnDefinition valueForKey:@"char_length"]) + [fieldEditor setTextMaxLength:[[columnDefinition valueForKey:@"char_length"] integerValue]]; + + id originalData = [resultData cellDataAtRow:rowIndex column:[[aTableColumn identifier] integerValue]]; + if ([originalData isNSNull]) originalData = [prefs objectForKey:SPNullValue]; + + id editData = [[fieldEditor editWithObject:originalData + fieldName:[columnDefinition objectForKey:@"name"] + usingEncoding:[mySQLConnection encoding] + isObjectBlob:isBlob + isEditable:isFieldEditable + withWindow:[tableDocumentInstance parentWindow]] retain]; + + if ( editData ) + [self tableView:aTableView setObjectValue:[editData copy] forTableColumn:aTableColumn row:rowIndex]; + + [fieldEditor release]; + + if ( editData ) [editData release]; + + return NO; + + } else { + return YES; + } +} + +/** + * Prevent the selection of rows while the table is still loading + */ +- (BOOL)tableView:(NSTableView *)aTableView shouldSelectRow:(NSInteger)rowIndex +{ + return tableRowsSelectable; +} + +#pragma mark - +#pragma mark TableView notifications + +/** + * Saves the new column size in the preferences for columns which map to fields + */ +- (void)tableViewColumnDidResize:(NSNotification *)aNotification +{ + // Abort if still loading the table + if (![cqColumnDefinition count]) return; + + // Retrieve the original index of the column from the identifier + NSInteger columnIndex = [[[[aNotification userInfo] objectForKey:@"NSTableColumn"] identifier] integerValue]; + NSDictionary *columnDefinition = NSArrayObjectAtIndex(cqColumnDefinition, columnIndex); + + // Don't save if the column doesn't map to an underlying SQL field + if (![columnDefinition objectForKey:@"org_name"] || ![(NSString *)[columnDefinition objectForKey:@"org_name"] length]) + return; + + NSMutableDictionary *tableColumnWidths; + NSString *host_db = [NSString stringWithFormat:@"%@@%@", [columnDefinition objectForKey:@"db"], [tableDocumentInstance host]]; + NSString *table = [columnDefinition objectForKey:@"org_table"]; + NSString *col = [columnDefinition objectForKey:@"org_name"]; + + // Retrieve or instantiate the tableColumnWidths object + if ([prefs objectForKey:SPTableColumnWidths] != nil) { + tableColumnWidths = [NSMutableDictionary dictionaryWithDictionary:[prefs objectForKey:SPTableColumnWidths]]; + } else { + tableColumnWidths = [NSMutableDictionary dictionary]; + } + + // Edit or create database object + if ([tableColumnWidths objectForKey:host_db] == nil) { + [tableColumnWidths setObject:[NSMutableDictionary dictionary] forKey:host_db]; + } else { + [tableColumnWidths setObject:[NSMutableDictionary dictionaryWithDictionary:[tableColumnWidths objectForKey:host_db]] forKey:host_db]; + } + + // Edit or create table object + if ([[tableColumnWidths objectForKey:host_db] objectForKey:table] == nil) { + [[tableColumnWidths objectForKey:host_db] setObject:[NSMutableDictionary dictionary] forKey:table]; + } else { + [[tableColumnWidths objectForKey:host_db] setObject:[NSMutableDictionary dictionaryWithDictionary:[[tableColumnWidths objectForKey:host_db] objectForKey:table]] forKey:table]; + } + + // Save the column size + [[[tableColumnWidths objectForKey:host_db] objectForKey:table] setObject:[NSNumber numberWithDouble:[(NSTableColumn *)[[aNotification userInfo] objectForKey:@"NSTableColumn"] width]] forKey:col]; + [prefs setObject:tableColumnWidths forKey:SPTableColumnWidths]; +} + + +#pragma mark - +#pragma mark TextView delegate methods + +/* + * Traps enter key and performs query instead of inserting a line break if aTextView == textView + * closes valueSheet if aTextView == valueTextField + */ +- (BOOL)textView:(NSTextView *)aTextView doCommandBySelector:(SEL)aSelector +{ + if ( aTextView == textView ) { + if ( [aTextView methodForSelector:aSelector] == [aTextView methodForSelector:@selector(insertNewline:)] && + [[[NSApp currentEvent] characters] isEqualToString:@"\003"] ) + { + [self runAllQueries:self]; + return YES; + } else { + return NO; + } + + } else if ( aTextView == valueTextField ) { + if ( [aTextView methodForSelector:aSelector] == [aTextView methodForSelector:@selector(insertNewline:)] ) + { + [self closeSheet:self]; + return YES; + } else { + return NO; + } + } + return NO; +} + +#pragma mark - +#pragma mark TextView notifications + +- (NSRange)textView:(NSTextView *)aTextView willChangeSelectionFromCharacterRange:(NSRange)oldSelectedCharRange toCharacterRange:(NSRange)newSelectedCharRange +{ + // Check if snippet session is still valid + if(!newSelectedCharRange.length && [textView isSnippetMode]) [textView checkForCaretInsideSnippet]; + + return newSelectedCharRange; +} + +/* + * A notification posted when the selection changes within the text view; + * used to control the run-currentrun-selection button state and action. + */ +- (void)textViewDidChangeSelection:(NSNotification *)aNotification +{ + + // Ensure that the notification is from the custom query text view + if ( [aNotification object] != textView ) return; + + BOOL isLookBehind = YES; + NSRange currentSelection = [textView selectedRange]; + NSUInteger caretPosition = currentSelection.location; + NSRange qRange = [self queryRangeAtPosition:caretPosition lookBehind:&isLookBehind]; + + if(qRange.length) + currentQueryRange = qRange; + else + currentQueryRange = NSMakeRange(0, 0); + + [textView setQueryRange:qRange]; + [textView setNeedsDisplay:YES]; + + // disable "Comment Current Query" menu item if no current query is selectable + [commentCurrentQueryMenuItem setEnabled:(currentQueryRange.length) ? YES : NO]; + + // If no text is selected, disable the button and action menu. + if ( caretPosition == NSNotFound ) { + selectionButtonCanBeEnabled = NO; + [runSelectionButton setEnabled:NO]; + [runSelectionMenuItem setEnabled:NO]; + return; + } + + // If the current selection is a single caret position, update the button based on + // whether the caret is inside a valid query. + if (!currentSelection.length) { + [runSelectionButton setTitle:NSLocalizedString(@"Run Current", @"Title of button to run current query in custom query view")]; + [runSelectionMenuItem setTitle:NSLocalizedString(@"Run Current Query", @"Title of action menu item to run current query in custom query view")]; + + // If a valid query is present at the cursor position, enable the button + if (qRange.length) { + if (isLookBehind) { + [runSelectionButton setTitle:NSLocalizedString(@"Run Previous", @"Title of button to run query just before text caret in custom query view")]; + [runSelectionMenuItem setTitle:NSLocalizedString(@"Run Previous Query", @"Title of action menu item to run query just before text caret in custom query view")]; + } + selectionButtonCanBeEnabled = YES; + if (![tableDocumentInstance isWorking]) { + [runSelectionButton setEnabled:YES]; + [runSelectionMenuItem setEnabled:YES]; + } + } else { + selectionButtonCanBeEnabled = NO; + [runSelectionButton setEnabled:NO]; + [runSelectionMenuItem setEnabled:NO]; + } + [commentLineOrSelectionMenuItem setTitle:NSLocalizedString(@"Comment Line", @"Title of action menu item to comment line")]; + + // For selection ranges, enable the button. + } else { + selectionButtonCanBeEnabled = YES; + [runSelectionButton setTitle:NSLocalizedString(@"Run Selection", @"Title of button to run selected text in custom query view")]; + [runSelectionMenuItem setTitle:NSLocalizedString(@"Run Selected Text", @"Title of action menu item to run selected text in custom query view")]; + [commentLineOrSelectionMenuItem setTitle:NSLocalizedString(@"Comment Selection", @"Title of action menu item to comment selection")]; + if (![tableDocumentInstance isWorking]) { + [runSelectionButton setEnabled:YES]; + [runSelectionMenuItem setEnabled:YES]; + } + } + + if(!historyItemWasJustInserted) + currentHistoryOffsetIndex = -1; +} + +#pragma mark - +#pragma mark TextField delegate methods + +/** + * Called whenever the user changes the name of the new query favorite or + * the user changed the query favorite search string. + */ +- (void)controlTextDidChange:(NSNotification *)notification +{ + if ([notification object] == queryFavoriteNameTextField) + [saveQueryFavoriteButton setEnabled:[[queryFavoriteNameTextField stringValue] length]]; + else if ([notification object] == queryFavoritesSearchField){ + [self filterQueryFavorites:nil]; + } + else if ([notification object] == queryHistorySearchField) { + [self filterQueryHistory:nil]; + } + +} + +#pragma mark - +#pragma mark SplitView delegate methods + +/* + * Tells the splitView that it can collapse views + */ +- (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview +{ + return YES; +} + +/* + * Defines max position of splitView + */ +- (CGFloat)splitView:(NSSplitView *)sender constrainMaxCoordinate:(CGFloat)proposedMax ofSubviewAt:(NSInteger)offset +{ + if (sender != queryInfoPaneSplitView) return (offset == 0) ? (proposedMax - 100) : (proposedMax - 73); +} + +/* + * Defines min position of splitView + */ +- (CGFloat)splitView:(NSSplitView *)sender constrainMinCoordinate:(CGFloat)proposedMin ofSubviewAt:(NSInteger)offset +{ + if (sender != queryInfoPaneSplitView) return proposedMin + 100; +} + +/** + * The query information pane cannot be resized. + */ +- (NSRect)splitView:(NSSplitView *)splitView effectiveRect:(NSRect)proposedEffectiveRect forDrawnRect:(NSRect)drawnRect ofDividerAtIndex:(NSInteger)dividerIndex +{ + return (splitView == queryInfoPaneSplitView ? NSZeroRect : proposedEffectiveRect); +} + +#pragma mark - +#pragma mark MySQL Help + +/* + * Set the MySQL version as X.Y for Help window title and online search + */ +- (void)setMySQLversion:(NSString *)theVersion +{ + mySQLversion = [[theVersion substringToIndex:3] retain]; + [textView setConnection:mySQLConnection withVersion:[[[mySQLversion componentsSeparatedByString:@"."] objectAtIndex:0] integerValue]]; + +} + +/* + * Return the Help window. + */ +- (NSWindow *)helpWebViewWindow +{ + return helpWebViewWindow; +} + +/* + * Show the data for "HELP 'searchString'". + */ +- (void)showHelpFor:(NSString *)searchString addToHistory:(BOOL)addToHistory calledByAutoHelp:(BOOL)autoHelp +{ + + if(![searchString length]) return; + + NSString *helpString = [self getHTMLformattedMySQLHelpFor:searchString calledByAutoHelp:autoHelp]; + + if(autoHelp && [helpString isEqualToString:SP_HELP_NOT_AVAILABLE]) { + [helpWebViewWindow orderOut:nil]; + return; + } + + // Order out resp. init the Help window if not visible + if(![helpWebViewWindow isVisible]) + { + // set title of the Help window + [helpWebViewWindow setTitle:[NSString stringWithFormat:@"%@ (%@ %@)", NSLocalizedString(@"MySQL Help", @"mysql help"), NSLocalizedString(@"version", @"version"), mySQLversion]]; + + // init goback/forward buttons + if([[helpWebView backForwardList] backListCount] < 1) + { + [helpNavigator setEnabled:NO forSegment:SP_HELP_GOBACK_BUTTON]; + [helpNavigator setEnabled:NO forSegment:SP_HELP_GOFORWARD_BUTTON]; + } else { + [helpNavigator setEnabled:[[helpWebView backForwardList] backListCount] forSegment:SP_HELP_GOBACK_BUTTON]; + [helpNavigator setEnabled:[[helpWebView backForwardList] forwardListCount] forSegment:SP_HELP_GOFORWARD_BUTTON]; + } + + // set default to search in MySQL help + helpTarget = SP_HELP_SEARCH_IN_MYSQL; + [helpTargetSelector setSelectedSegment:SP_HELP_SEARCH_IN_MYSQL]; + [self helpTargetValidation]; + + // order out Help window if Help is available + if(![helpString isEqualToString:SP_HELP_NOT_AVAILABLE]) + [helpWebViewWindow orderFront:helpWebView]; + + } + + // close Help window if no Help available + if([helpString isEqualToString:SP_HELP_NOT_AVAILABLE]) + [helpWebViewWindow close]; + + if(![helpString length]) return; + + // add searchString to history list + if(addToHistory) + { + WebHistoryItem *aWebHistoryItem = [[WebHistoryItem alloc] initWithURLString:[NSString stringWithFormat:@"applewebdata://%@", searchString] title:searchString lastVisitedTimeInterval:[[NSDate date] timeIntervalSinceDate:[NSDate distantFuture]]]; + [[helpWebView backForwardList] addItem:aWebHistoryItem]; + [aWebHistoryItem release]; + } + + // validate goback/forward buttons + [helpNavigator setEnabled:[[helpWebView backForwardList] backListCount] forSegment:SP_HELP_GOBACK_BUTTON]; + [helpNavigator setEnabled:[[helpWebView backForwardList] forwardListCount] forSegment:SP_HELP_GOFORWARD_BUTTON]; + + // load HTML formatted help into the webview + [[helpWebView mainFrame] loadHTMLString:helpString baseURL:nil]; + +} + + +/* + * Show the data for "HELP 'search word'" according to helpTarget + */ +- (IBAction)showHelpForSearchString:(id)sender +{ + NSString *searchString = [[helpSearchField stringValue] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + switch(helpTarget) + { + case SP_HELP_SEARCH_IN_PAGE: + if(![helpWebView searchFor:searchString direction:YES caseSensitive:NO wrap:YES]) + if([searchString length]) NSBeep(); + break; + case SP_HELP_SEARCH_IN_WEB: + if(![searchString length]) + break; + [self openMySQLonlineDocumentationWithString:searchString]; + break; + case SP_HELP_SEARCH_IN_MYSQL: + [self showHelpFor:searchString addToHistory:YES calledByAutoHelp:NO]; + break; + } +} + +/* + * Show the Help for the selected text in the webview + */ +- (IBAction)showHelpForWebViewSelection:(id)sender +{ + [self showHelpFor:[[helpWebView selectedDOMRange] text] addToHistory:YES calledByAutoHelp:NO]; +} + +/* + * Show MySQL's online documentation for the selected text in the webview + */ +- (IBAction)searchInDocForWebViewSelection:(id)sender +{ + NSString *searchString = [[[helpWebView selectedDOMRange] text] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if(![searchString length]) + { + NSBeep(); + return; + } + [self openMySQLonlineDocumentationWithString:searchString]; +} + + +/* + * Show the data for "HELP 'currentWord'" + */ +- (IBAction)showHelpForCurrentWord:(id)sender +{ + NSString *searchString = [[sender string] substringWithRange:[sender getRangeForCurrentWord]]; + [self showHelpFor:searchString addToHistory:YES calledByAutoHelp:NO]; +} + +/* + * Find Next/Previous in current page + */ +- (IBAction)helpSearchFindNextInPage:(id)sender +{ + if(helpTarget == SP_HELP_SEARCH_IN_PAGE) + if(![helpWebView searchFor:[[helpSearchField stringValue] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] direction:YES caseSensitive:NO wrap:YES]) + NSBeep(); +} +- (IBAction)helpSearchFindPreviousInPage:(id)sender +{ + if(helpTarget == SP_HELP_SEARCH_IN_PAGE) + if(![helpWebView searchFor:[[helpSearchField stringValue] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] direction:NO caseSensitive:NO wrap:YES]) + NSBeep(); +} + +/* + * Navigation for back/TOC/forward + */ +- (IBAction)helpSegmentDispatcher:(id)sender +{ + switch([helpNavigator selectedSegment]) + { + case SP_HELP_GOBACK_BUTTON: + [helpWebView goBack]; + break; + case SP_HELP_SHOW_TOC_BUTTON: + [self showHelpFor:SP_HELP_TOC_SEARCH_STRING addToHistory:YES calledByAutoHelp:NO]; + break; + case SP_HELP_GOFORWARD_BUTTON: + [helpWebView goForward]; + break; + } + // validate goback and goforward buttons according history + [helpNavigator setEnabled:[[helpWebView backForwardList] backListCount] forSegment:SP_HELP_GOBACK_BUTTON]; + [helpNavigator setEnabled:[[helpWebView backForwardList] forwardListCount] forSegment:SP_HELP_GOFORWARD_BUTTON]; + +} + +/* + * Set helpTarget according user choice via mouse and keyboard short-cuts. + */ +- (IBAction)helpSelectHelpTargetMySQL:(id)sender +{ + helpTarget = SP_HELP_SEARCH_IN_MYSQL; + [helpTargetSelector setSelectedSegment:SP_HELP_SEARCH_IN_MYSQL]; + [self helpTargetValidation]; +} +- (IBAction)helpSelectHelpTargetPage:(id)sender +{ + helpTarget = SP_HELP_SEARCH_IN_PAGE; + [helpTargetSelector setSelectedSegment:SP_HELP_SEARCH_IN_PAGE]; + [self helpTargetValidation]; +} +- (IBAction)helpSelectHelpTargetWeb:(id)sender +{ + helpTarget = SP_HELP_SEARCH_IN_WEB; + [helpTargetSelector setSelectedSegment:SP_HELP_SEARCH_IN_WEB]; + [self helpTargetValidation]; +} +- (IBAction)helpTargetDispatcher:(id)sender +{ + helpTarget = [helpTargetSelector selectedSegment]; + [self helpTargetValidation]; +} + +- (IBAction)showCompletionList:(id)sender +{ + NSRange insertRange = NSMakeRange([textView selectedRange].location, 0); + switch([sender tag]) { + case 8000: + [textView showCompletionListFor:@"$SP_ASLIST_ALL_DATABASES" atRange:insertRange fuzzySearch:NO]; + break; + case 8001: + [textView showCompletionListFor:@"$SP_ASLIST_ALL_TABLES" atRange:insertRange fuzzySearch:NO]; + break; + case 8002: + [textView showCompletionListFor:@"$SP_ASLIST_ALL_FIELDS" atRange:insertRange fuzzySearch:NO]; + break; + } +} +/* + * Show the data for "HELP 'currentWord' invoked by autohelp" + */ +- (void)showAutoHelpForCurrentWord:(id)sender +{ + NSString *searchString = [[sender string] substringWithRange:[sender getRangeForCurrentWord]]; + [self showHelpFor:searchString addToHistory:YES calledByAutoHelp:YES]; +} + +/* + * Control the help search field behaviour. + */ +- (void)helpTargetValidation +{ + switch(helpTarget) + { + case SP_HELP_SEARCH_IN_PAGE: + case SP_HELP_SEARCH_IN_WEB: + [helpSearchFieldCell setSendsWholeSearchString:YES]; + break; + case SP_HELP_SEARCH_IN_MYSQL: + [helpSearchFieldCell setSendsWholeSearchString:NO]; + break; + } +} + +- (void)openMySQLonlineDocumentationWithString:(NSString *)searchString +{ + NSString *version = nil; + if([[mySQLversion stringByReplacingOccurrencesOfString:@"." withString:@""] integerValue] < 42) + version = @"41"; + else + version = [mySQLversion stringByReplacingOccurrencesOfString:@"." withString:@""]; + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString: + [[NSString stringWithFormat: + SPMySQLSearchURL, + searchString, + version] + stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding]]]; +} + +/* + * Return the help string HTML formatted from executing "HELP 'searchString'". + * If more than one help topic was found return a link list. + */ +- (NSString *)getHTMLformattedMySQLHelpFor:(NSString *)searchString calledByAutoHelp:(BOOL)autoHelp +{ + + if(![searchString length]) return @""; + + NSRange aRange; + MCPResult *theResult = nil; + NSDictionary *tableDetails; + NSMutableString *theHelp = [NSMutableString string]; + + [theHelp setString:@""]; + + // search via: HELP 'searchString' + theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"HELP '%@'", [searchString stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"]]]; + if ([mySQLConnection queryErrored]) + { + // if an error or HELP is not supported fall back to online search but + // don't open it if autoHelp is enabled + if(!autoHelp) + [self openMySQLonlineDocumentationWithString:searchString]; + + [helpWebViewWindow close]; + return SP_HELP_NOT_AVAILABLE; + } + // nothing found? + if(![theResult numOfRows]) { + // try to search via: HELP 'searchString%' + theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"HELP '%@%%'", [searchString stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"]]]; + // really nothing found? + if(![theResult numOfRows]) + return @""; + } + tableDetails = [[NSDictionary alloc] initWithDictionary:[theResult fetchRowAsDictionary]]; + + if ([tableDetails objectForKey:@"description"]) { // one single help topic found + if ([tableDetails objectForKey:@"name"]) { + [theHelp appendString:@"<h2 class='header'>"]; + [theHelp appendString:[[[tableDetails objectForKey:@"name"] copy] autorelease]]; + [theHelp appendString:@"</h2>"]; + + } + if ([tableDetails objectForKey:@"description"]) { + NSMutableString *desc = [NSMutableString string]; + NSError *err1 = NULL; + NSString *aUrl; + + [desc setString:[[[tableDetails objectForKey:@"description"] copy] autorelease]]; + + //[desc replaceOccurrencesOfString:[searchString uppercaseString] withString:[NSString stringWithFormat:@"<span class='searchstring'>%@</span>", [searchString uppercaseString]] options:NSLiteralSearch range:NSMakeRange(0,[desc length])]; + + // detect and generate http links + aRange = NSMakeRange(0,0); + NSInteger safeCnt = 0; // safety counter - not more than 200 loops allowed + while(1){ + aRange = [desc rangeOfRegex:@"\\s((https?|ftp|file)://.*?html)" options:RKLNoOptions inRange:NSMakeRange(aRange.location+aRange.length, [desc length]-aRange.location-aRange.length) capture:1 error:&err1]; + if(aRange.location != NSNotFound) { + aUrl = [desc substringWithRange:aRange]; + [desc replaceCharactersInRange:aRange withString:[NSString stringWithFormat:@"<a href='%@'>%@</a>", aUrl, aUrl]]; + } + else + break; + safeCnt++; + if(safeCnt > 200) + break; + } + // detect and generate mysql links for "[HELP keyword]" + aRange = NSMakeRange(0,0); + safeCnt = 0; + while(1){ + // TODO how to catch in HELP 'grant' last see [HELP SHOW GRANTS] ?? it's ridiculous + aRange = [desc rangeOfRegex:@"\\[HELP ([^ ]*?)\\]" options:RKLNoOptions inRange:NSMakeRange(aRange.location+aRange.length+53, [desc length]-53-aRange.location-aRange.length) capture:1 error:&err1]; + if(aRange.location != NSNotFound) { + aUrl = [[desc substringWithRange:aRange] stringByReplacingOccurrencesOfString:@"\n" withString:@" "]; + [desc replaceCharactersInRange:aRange withString:[NSString stringWithFormat:@"<a title='%@ “%@”' href='%@' class='internallink'>%@</a>", NSLocalizedString(@"Show MySQL help for", @"show mysql help for"), aUrl, aUrl, aUrl]]; + } + else + break; + safeCnt++; + if(safeCnt > 200) + break; + } + // detect and generate mysql links for capitalzed letters + // aRange = NSMakeRange(0,0); + // safeCnt = 0; + // while(1){ + // aRange = [desc rangeOfRegex:@"(?<!\\w)([A-Z_]{2,}( [A-Z_]{2,})?)" options:RKLNoOptions inRange:NSMakeRange(aRange.location+aRange.length, [desc length]-aRange.location-aRange.length) capture:1 error:&err1]; + // if(aRange.location != NSNotFound) { + // aUrl = [desc substringWithRange:aRange]; + // [desc replaceCharactersInRange:aRange withString:[NSString stringWithFormat:@"<a title='%@ “%@”' href='%@' class='internallink'>%@</a>", NSLocalizedString(@"Show MySQL help for", @"show mysql help for"), aUrl, aUrl, aUrl]]; + // } + // else + // break; + // safeCnt++; + // if(safeCnt > 200) + // break; + // } + + [theHelp appendString:@"<pre class='description'>"]; + [theHelp appendString:desc]; + [theHelp appendString:@"</pre>"]; + } + // are examples available? + if([tableDetails objectForKey:@"example"]){ + NSString *examples = [[[tableDetails objectForKey:@"example"] copy] autorelease]; + if([examples length]){ + [theHelp appendString:@"<br><i><b>Example:</b></i><br><pre class='example'>"]; + [theHelp appendString:examples]; + [theHelp appendString:@"</pre>"]; + } + } + } else { // list all found topics + NSInteger i; + NSInteger r = [theResult numOfRows]; + if (r) [theResult dataSeek:0]; + // check if HELP 'contents' is called + if(![searchString isEqualToString:SP_HELP_TOC_SEARCH_STRING]) + [theHelp appendString:[NSString stringWithFormat:@"<br><i>%@ “%@”</i><br>", NSLocalizedString(@"Help topics for", @"help topics for"), searchString]]; + else + [theHelp appendString:[NSString stringWithFormat:@"<br><b>%@:</b><br>", NSLocalizedString(@"MySQL Help – Categories", @"mysql help categories"), searchString]]; + + // iterate through all found rows and print them as HTML ul/li list + [theHelp appendString:@"<ul>"]; + for ( i = 0 ; i < r ; i++ ) { + NSArray *anArray = [theResult fetchRowAsArray]; + NSString *topic = [anArray objectAtIndex:[anArray count]-2]; + [theHelp appendString: + [NSString stringWithFormat:@"<li><a title='%@ “%@”' href='%@' class='internallink'>%@</a></li>", NSLocalizedString(@"Show MySQL help for", @"show mysql help for"), topic, topic, topic]]; + } + [theHelp appendString:@"</ul>"]; + } + + [tableDetails release]; + + return [NSString stringWithFormat:helpHTMLTemplate, theHelp]; + +} + +////////////////////////////// +// WebView delegate methods // +////////////////////////////// + +/* + * Link detector: If user clicked at an http link open it in the default browser, + * otherwise search for it in the MySQL help. Additionally handle back/forward events from + * keyboard and context menu. + */ +- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener +{ + NSInteger navigationType = [[actionInformation objectForKey:WebActionNavigationTypeKey] integerValue]; + + if([[[request URL] scheme] isEqualToString:@"applewebdata"] && navigationType == WebNavigationTypeLinkClicked){ + [self showHelpFor:[[[request URL] path] lastPathComponent] addToHistory:YES calledByAutoHelp:NO]; + [listener ignore]; + } else { + if (navigationType == WebNavigationTypeOther) { + // catch reload event + // if([[[actionInformation objectForKey:WebActionOriginalURLKey] absoluteString] isEqualToString:@"about:blank"]) + // [listener use]; + // else + [listener use]; + } else if (navigationType == WebNavigationTypeLinkClicked) { + // show http in browser + [[NSWorkspace sharedWorkspace] openURL:[actionInformation objectForKey:WebActionOriginalURLKey]]; + [listener ignore]; + } else if (navigationType == WebNavigationTypeBackForward) { + // catch back/forward events from contextual menu + [self showHelpFor:[[[[actionInformation objectForKey:WebActionOriginalURLKey] absoluteString] lastPathComponent] stringByReplacingPercentEscapesUsingEncoding:NSASCIIStringEncoding] addToHistory:NO calledByAutoHelp:NO]; + [listener ignore]; + } else if (navigationType == WebNavigationTypeReload) { + // just in case + [listener ignore]; + } else { + // Ignore WebNavigationTypeFormSubmitted, WebNavigationTypeFormResubmitted. + [listener ignore]; + } + } +} + +/* + * Manage contextual menu in helpWebView + * Ignore "Reload", "Open Link", "Open Link in new Window", "Download link" etc. + */ +- (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems +{ + + NSMutableArray *webViewMenuItems = [[defaultMenuItems mutableCopy] autorelease]; + + if (webViewMenuItems) + { + // Remove all needless default menu items + NSEnumerator *itemEnumerator = [defaultMenuItems objectEnumerator]; + NSMenuItem *menuItem = nil; + while (menuItem = [itemEnumerator nextObject]) + { + NSInteger tag = [menuItem tag]; + switch (tag) + { + case 2000: // WebMenuItemTagOpenLink + case WebMenuItemTagOpenLinkInNewWindow: + case WebMenuItemTagDownloadLinkToDisk: + case WebMenuItemTagOpenImageInNewWindow: + case WebMenuItemTagDownloadImageToDisk: + case WebMenuItemTagCopyImageToClipboard: + case WebMenuItemTagOpenFrameInNewWindow: + case WebMenuItemTagStop: + case WebMenuItemTagReload: + case WebMenuItemTagCut: + case WebMenuItemTagPaste: + case WebMenuItemTagSpellingGuess: + case WebMenuItemTagNoGuessesFound: + case WebMenuItemTagIgnoreSpelling: + case WebMenuItemTagLearnSpelling: + case WebMenuItemTagOther: + case WebMenuItemTagOpenWithDefaultApplication: + [webViewMenuItems removeObjectIdenticalTo: menuItem]; + break; + } + } + } + + // Add two menu items for a selection if no link is given + if(webViewMenuItems + && [[element objectForKey:@"WebElementIsSelected"] boolValue] + && ![[element objectForKey:@"WebElementLinkIsLive"] boolValue]) + { + + NSMenuItem *searchInMySQL; + NSMenuItem *searchInMySQLonline; + + [webViewMenuItems insertObject:[NSMenuItem separatorItem] atIndex:0]; + + searchInMySQLonline = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Search in MySQL Documentation", @"Search in MySQL Documentation") action:@selector(searchInDocForWebViewSelection:) keyEquivalent:@""]; + [searchInMySQLonline setEnabled:YES]; + [searchInMySQLonline setTarget:self]; + [webViewMenuItems insertObject:searchInMySQLonline atIndex:0]; + [searchInMySQLonline release]; + + searchInMySQL = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Search in MySQL Help", @"Search in MySQL Help") action:@selector(showHelpForWebViewSelection:) keyEquivalent:@""]; + [searchInMySQL setEnabled:YES]; + [searchInMySQL setTarget:self]; + [webViewMenuItems insertObject:searchInMySQL atIndex:0]; + [searchInMySQL release]; + + } + + return webViewMenuItems; +} + +#pragma mark - +#pragma mark Query favorites manager delegate methods + +/** + * Rebuild history popup menu. + */ +- (void)historyItemsHaveBeenUpdated:(id)manager +{ + + // Abort if the connection has been closed already - sign of a closed window + if (![mySQLConnection isConnected]) return; + + // Refresh history popup menu + NSMenu* historyMenu = [queryHistoryButton menu]; + while([queryHistoryButton numberOfItems] > 7) + [queryHistoryButton removeItemAtIndex:[queryHistoryButton numberOfItems]-1]; + + NSUInteger numberOfHistoryItems = [[SPQueryController sharedQueryController] numberOfHistoryItemsForFileURL:[tableDocumentInstance fileURL]]; + if(numberOfHistoryItems>0) + for(id historyMenuItem in [[SPQueryController sharedQueryController] historyMenuItemsForFileURL:[tableDocumentInstance fileURL]]) + [historyMenu addItem:historyMenuItem]; +} +/** + * Called by the query favorites manager whenever the query favorites have been updated. + */ +- (void)queryFavoritesHaveBeenUpdated:(id)manager +{ + NSMenuItem *headerMenuItem; + NSMenu *menu = [queryFavoritesButton menu]; + + // Remove all favorites beginning from the end + while([queryFavoritesButton numberOfItems] > 7) + [queryFavoritesButton removeItemAtIndex:[queryFavoritesButton numberOfItems]-1]; + + // Build document-based list + headerMenuItem = [[NSMenuItem alloc] initWithTitle: + [[[[tableDocumentInstance fileURL] absoluteString] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding] lastPathComponent] + action:NULL keyEquivalent:@""]; + [headerMenuItem setTag:SP_FAVORITE_HEADER_MENUITEM_TAG]; + [headerMenuItem setToolTip:[NSString stringWithFormat:@"‘%@’ based favorites", + [[[[tableDocumentInstance fileURL] absoluteString] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding] lastPathComponent]]]; + [headerMenuItem setIndentationLevel:0]; + [menu addItem:headerMenuItem]; + [headerMenuItem release]; + for (NSDictionary *favorite in [[SPQueryController sharedQueryController] favoritesForFileURL:[tableDocumentInstance fileURL]]) { + if (![favorite isKindOfClass:[NSDictionary class]] || ![favorite objectForKey:@"name"]) continue; + NSMutableParagraphStyle *paraStyle = [[[NSMutableParagraphStyle alloc] init] autorelease]; + [paraStyle setTabStops:[NSArray array]]; + [paraStyle addTabStop:[[[NSTextTab alloc] initWithType:NSRightTabStopType location:190.0] autorelease]]; + NSDictionary *attributes = [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:paraStyle, [NSFont systemFontOfSize:11], nil] forKeys:[NSArray arrayWithObjects:NSParagraphStyleAttributeName, NSFontAttributeName, nil]]; + NSAttributedString *titleString = [[[NSAttributedString alloc] + initWithString:([favorite objectForKey:@"tabtrigger"] && [(NSString*)[favorite objectForKey:@"tabtrigger"] length]) ? [NSString stringWithFormat:@"%@\t%@⇥", [favorite objectForKey:@"name"], [favorite objectForKey:@"tabtrigger"]] : [favorite objectForKey:@"name"] + attributes:attributes] autorelease]; + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"" action:NULL keyEquivalent:@""]; + [item setToolTip:[NSString stringWithString:[favorite objectForKey:@"query"]]]; + [item setAttributedTitle:titleString]; + [item setIndentationLevel:1]; + [menu addItem:item]; + [item release]; + } + + // Build global list + headerMenuItem = [[NSMenuItem alloc] initWithTitle:@"Global" action:NULL keyEquivalent:@""]; + [headerMenuItem setTag:SP_FAVORITE_HEADER_MENUITEM_TAG]; + [headerMenuItem setToolTip:@"Globally stored favorites"]; + [headerMenuItem setIndentationLevel:0]; + [menu addItem:headerMenuItem]; + [headerMenuItem release]; + for (NSDictionary *favorite in [prefs objectForKey:SPQueryFavorites]) { + if (![favorite isKindOfClass:[NSDictionary class]] || ![favorite objectForKey:@"name"]) continue; + NSMutableParagraphStyle *paraStyle = [[[NSMutableParagraphStyle alloc] init] autorelease]; + [paraStyle setTabStops:[NSArray array]]; + [paraStyle addTabStop:[[[NSTextTab alloc] initWithType:NSRightTabStopType location:190.0] autorelease]]; + NSDictionary *attributes = [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:paraStyle, [NSFont systemFontOfSize:11], nil] forKeys:[NSArray arrayWithObjects:NSParagraphStyleAttributeName, NSFontAttributeName, nil]]; + NSAttributedString *titleString = [[[NSAttributedString alloc] + initWithString:([favorite objectForKey:@"tabtrigger"] && [(NSString*)[favorite objectForKey:@"tabtrigger"] length]) ? [NSString stringWithFormat:@"%@\t%@⇥", [favorite objectForKey:@"name"], [favorite objectForKey:@"tabtrigger"]] : [favorite objectForKey:@"name"] + attributes:attributes] autorelease]; + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"" action:NULL keyEquivalent:@""]; + [item setToolTip:[NSString stringWithString:[favorite objectForKey:@"query"]]]; + [item setAttributedTitle:titleString]; + [item setIndentationLevel:1]; + [menu addItem:item]; + [item release]; + } +} + +#pragma mark - +#pragma mark Task interaction + +/** + * Disable all content interactive elements during an ongoing task. + */ +- (void) startDocumentTaskForTab:(NSNotification *)aNotification +{ + isWorking = YES; + + // Only proceed if this view is selected. + if (![[tableDocumentInstance selectedToolbarItemIdentifier] isEqualToString:SPMainToolbarCustomQuery]) + return; + + tableRowsSelectable = NO; + [runSelectionButton setEnabled:NO]; + [runSelectionMenuItem setEnabled:NO]; + [runAllButton setEnabled:NO]; + [runAllMenuItem setEnabled:NO]; +} + +/** + * Enable all content interactive elements after an ongoing task. + */ +- (void) endDocumentTaskForTab:(NSNotification *)aNotification +{ + isWorking = NO; + + // Only proceed if this view is selected. + if (![[tableDocumentInstance selectedToolbarItemIdentifier] isEqualToString:SPMainToolbarCustomQuery]) + return; + + if (selectionButtonCanBeEnabled) { + [runSelectionButton setEnabled:YES]; + [runSelectionMenuItem setEnabled:YES]; + } + tableRowsSelectable = YES; + [runAllButton setEnabled:YES]; + [runAllMenuItem setEnabled:YES]; +} + +#pragma mark - +#pragma mark Other + +/** + * Returns the number of queries. + */ +- (NSUInteger)numberOfQueries +{ + return numberOfQueries; +} + +- (NSString *)buildHistoryString +{ + return [[[SPQueryController sharedQueryController] historyForFileURL:[tableDocumentInstance fileURL]] componentsJoinedByString:@";\n"]; +} + +/** + * Add a query string to the file/global history, via the query controller. + * Single argument allows calls on the main thread. + */ +- (void)addHistoryEntry:(NSString *)entryString +{ + [[SPQueryController sharedQueryController] addHistory:entryString forFileURL:[tableDocumentInstance fileURL]]; +} + +/* + * 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 +{ + // Display table veiew vertical gridlines preference changed + if ([keyPath isEqualToString:SPDisplayTableViewVerticalGridlines]) { + [customQueryView setGridStyleMask:([[change objectForKey:NSKeyValueChangeNewKey] boolValue]) ? NSTableViewSolidVerticalGridLineMask : NSTableViewGridNone]; + } + // Result Table Font preference changed + else if ([keyPath isEqualToString:SPGlobalResultTableFont]) { + NSFont *tableFont = [NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]; + [customQueryView setRowHeight:2.0f+NSSizeToCGSize([[NSString stringWithString:@"{ǞṶḹÜ∑zgyf"] sizeWithAttributes:[NSDictionary dictionaryWithObject:tableFont forKey:NSFontAttributeName]]).height]; + [customQueryView setFont:tableFont]; + [customQueryView reloadData]; + } +} + +/** + * Called when the save query favorite/clear history sheet is dismissed. + */ +- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo +{ + + if ([contextInfo isEqualToString:@"clearHistory"]) { + if (returnCode == NSOKButton) { + // Remove items in the query controller + [[SPQueryController sharedQueryController] replaceHistoryByArray:[NSMutableArray array] forFileURL:[tableDocumentInstance fileURL]]; + } + return; + } + + if ([contextInfo isEqualToString:@"addAllToNewQueryFavorite"] || [contextInfo isEqualToString:@"addSelectionToNewQueryFavorite"]) { + if (returnCode == NSOKButton) { + + // Add the new query favorite directly the user's preferences here instead of asking the manager to do it + // as it may not have been fully initialized yet. + NSMutableArray *favorites = [NSMutableArray arrayWithArray:[prefs objectForKey:SPQueryFavorites]]; + + // What should be saved + NSString *queryToBeAddded; + + if([contextInfo isEqualToString:@"addSelectionToNewQueryFavorite"]) { + // First check for a selection + if([textView selectedRange].length) + queryToBeAddded = [[textView string] substringWithRange:[textView selectedRange]]; + // then for a current query + else if(currentQueryRange.length) + queryToBeAddded = [[textView string] substringWithRange:currentQueryRange]; + // otherwise take the entire string + else + queryToBeAddded = [textView string]; + } else { + queryToBeAddded = [textView string]; + } + + if([saveQueryFavoriteGlobal state] == NSOnState) { + [favorites addObject:[NSMutableDictionary dictionaryWithObjects: + [NSArray arrayWithObjects:[queryFavoriteNameTextField stringValue], queryToBeAddded, nil] + forKeys:[NSArray arrayWithObjects:@"name", @"query", nil]]]; + + [prefs setObject:favorites forKey:SPQueryFavorites]; + } else { + [[SPQueryController sharedQueryController] addFavorite:[NSMutableDictionary dictionaryWithObjects: + [NSArray arrayWithObjects:[queryFavoriteNameTextField stringValue], [[queryToBeAddded mutableCopy] autorelease], nil] + forKeys:[NSArray arrayWithObjects:@"name", @"query", nil]] forFileURL:[tableDocumentInstance fileURL]]; + } + + [saveQueryFavoriteGlobal setState:NSOffState]; + + [self queryFavoritesHaveBeenUpdated:nil]; + + } + } + + [queryFavoriteNameTextField setStringValue:@""]; +} + +- (void)savePanelDidEnd:(NSSavePanel *)panel returnCode:(NSInteger)returnCode contextInfo:(id)contextInfo +{ + if([contextInfo isEqualToString:@"saveHistory"]) { + if (returnCode == NSOKButton) { + NSError *error = nil; + + [prefs setInteger:[[encodingPopUp selectedItem] tag] forKey:SPLastSQLFileEncoding]; + [prefs synchronize]; + + [[self buildHistoryString] writeToFile:[panel filename] + atomically:YES + encoding:[[encodingPopUp selectedItem] tag] + error:&error]; + + if (error) [[NSAlert alertWithError:error] runModal]; + } + } +} + +/** + * Menu item validation. + */ +- (BOOL)validateMenuItem:(NSMenuItem *)menuItem +{ + // Control "Save ... to Favorites" + if ( [menuItem tag] == SP_SAVE_SELECTION_FAVORTITE_MENUITEM_TAG ) { + if ([[textView string] length] < 1) return NO; + if([textView selectedRange].length) + [menuItem setTitle:NSLocalizedString(@"Save Selection to Favorites",@"Save Selection to Favorites")]; + else if(currentQueryRange.length) + [menuItem setTitle:NSLocalizedString(@"Save Current Query to Favorites",@"Save Current Query to Favorites")]; + else + [menuItem setTitle:NSLocalizedString(@"Save All to Favorites",@"Save All to Favorites")]; + } + + // Control "Save All to Favorites" + if ( [menuItem tag] == SP_SAVE_ALL_FAVORTITE_MENUITEM_TAG ) { + if ([[textView string] length] < 1) return NO; + } + // Avoid selecting button list headers + else if ( [menuItem tag] == SP_FAVORITE_HEADER_MENUITEM_TAG ) { + return NO; + } + // Control Clear History menu item title according to isUntitled + else if ( [menuItem tag] == SP_HISTORY_CLEAR_MENUITEM_TAG ) { + if ( [tableDocumentInstance isUntitled] ) { + [menuItem setTitle:NSLocalizedString(@"Clear Global History", @"clear global history menu item title")]; + [menuItem setToolTip:NSLocalizedString(@"Clear the global history list", @"clear the global history list tooltip message")]; + } else { + [menuItem setTitle:[NSString stringWithFormat:NSLocalizedString(@"Clear History for “%@”", @"clear history for “%@” menu title"), [tableDocumentInstance displayName]]]; + [menuItem setToolTip:NSLocalizedString(@"Clear the document-based history list", @"clear the document-based history list tooltip message")]; + } + } + // Check for History items + else if ( [menuItem tag] >= SP_HISTORY_COPY_MENUITEM_TAG && [menuItem tag] <= SP_HISTORY_CLEAR_MENUITEM_TAG ) { + return ([queryHistoryButton numberOfItems]-7); + } + + return YES; +} + +#pragma mark - + +- (id)init +{ + if ((self = [super init])) { + + usedQuery = [[NSString stringWithString:@""] retain]; + + sortField = nil; + isDesc = NO; + sortColumn = nil; + selectionButtonCanBeEnabled = NO; + cqColumnDefinition = nil; + favoritesManager = nil; + + tableRowsSelectable = YES; + selectionIndexToRestore = nil; + selectionViewportToRestore = NSZeroRect; + + // init helpHTMLTemplate + NSError *error; + + helpHTMLTemplate = [[NSString alloc] + initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:SPHTMLHelpTemplate ofType:@"html"] + encoding:NSUTF8StringEncoding + error:&error]; + + // an error occurred while reading + if (helpHTMLTemplate == nil) { + NSLog(@"%@", [NSString stringWithFormat:@"Error reading “%@.html”!<br>%@", SPHTMLHelpTemplate, [error localizedFailureReason]]); + NSBeep(); + } + + // init search history + [helpWebView setMaintainsBackForwardList:YES]; + [[helpWebView backForwardList] setCapacity:20]; + + // init tableView's data source + resultDataCount = 0; + resultData = [[SPDataStorage alloc] init]; + editedRow = -1; + + currentHistoryOffsetIndex = -1; + historyItemWasJustInserted = NO; + + prefs = [NSUserDefaults standardUserDefaults]; + + } + + return self; +} + +/** + * Filters the query favorites menu. + */ +- (IBAction)filterQueryFavorites:(id)sender +{ + NSUInteger i; + NSMenu *menu = [queryFavoritesButton menu]; + NSString *searchPattern = [queryFavoritesSearchField stringValue]; + + for (i = 7; i < [menu numberOfItems]; i++) + { + [[menu itemAtIndex:i] setHidden:([[menu itemAtIndex:i] tag] != SP_FAVORITE_HEADER_MENUITEM_TAG + && ![[[menu itemAtIndex:i] title] isMatchedByRegex:[NSString stringWithFormat:@"(?i).*%@.*", searchPattern]])]; + } +} + +/** + * Filters the query history menu. + */ +- (IBAction)filterQueryHistory:(id)sender +{ + NSMenu *menu = [queryHistoryButton menu]; + NSUInteger numberOfItems = [menu numberOfItems]; + NSUInteger i; + NSString *searchPattern = [queryHistorySearchField stringValue]; + NSArray *history = [[SPQueryController sharedQueryController] historyForFileURL:[tableDocumentInstance fileURL]]; + for (i = 7; i < numberOfItems; i++) + { + [[menu itemAtIndex:i] setHidden:(![[history objectAtIndex:i-7] isMatchedByRegex:[NSString stringWithFormat:@"(?i).*%@.*", searchPattern]])]; + } +} + +/** + * Abort editing of the Favorite and History search field editors if user presses ARROW UP or DOWN + * to allow to navigate through the menu item list. + */ +- (BOOL)control:(NSControl*)control textView:(NSTextView*)textView doCommandBySelector:(SEL)commandSelector +{ + if(control == queryHistorySearchField || control == queryFavoritesSearchField) { + if(commandSelector == @selector(moveDown:) || commandSelector == @selector(moveUp:)) { + [queryHistorySearchField abortEditing]; + [queryFavoritesSearchField abortEditing]; + + // Send moveDown/Up to the popup menu + NSEvent *arrowEvent; + if(commandSelector == @selector(moveDown:)) + arrowEvent = [NSEvent keyEventWithType:NSKeyDown location:NSMakePoint(0,0) modifierFlags:0 timestamp:0 windowNumber:[[tableDocumentInstance parentWindow] windowNumber] context:[NSGraphicsContext currentContext] characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:0x7D]; + else + arrowEvent = [NSEvent keyEventWithType:NSKeyDown location:NSMakePoint(0,0) modifierFlags:0 timestamp:0 windowNumber:[[tableDocumentInstance parentWindow] windowNumber] context:[NSGraphicsContext currentContext] characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:0x7E]; + [[NSApplication sharedApplication] postEvent:arrowEvent atStart:NO]; + return YES; + + } + } + return NO; +} +// - (void)menu:(NSMenu *)menu willHighlightItem:(NSMenuItem *)item +// { +// // Set the focus at the search field +// // TODO : but no way out; always selecting first/last menu item +// // because after setting focus to search field NSMenu selectedItemIndex is -1 +// if(item == queryHistorySearchMenuItem) { +// [queryHistorySearchField selectText:nil]; +// } +// +// } + +/** + * Setup various interface controls. + */ +- (void)awakeFromNib +{ + // Set pre-defined menu tags + [queryFavoritesSaveAsMenuItem setTag:SP_SAVE_SELECTION_FAVORTITE_MENUITEM_TAG]; + [queryFavoritesSaveAllMenuItem setTag:SP_SAVE_ALL_FAVORTITE_MENUITEM_TAG]; + + // Set the structure and index view's vertical gridlines if required + [customQueryView setGridStyleMask:([prefs boolForKey:SPDisplayTableViewVerticalGridlines]) ? NSTableViewSolidVerticalGridLineMask : NSTableViewGridNone]; + + // Add observers for document task activity + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(startDocumentTaskForTab:) + name:SPDocumentTaskStartNotification + object:tableDocumentInstance]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(endDocumentTaskForTab:) + name:SPDocumentTaskEndNotification + object:tableDocumentInstance]; + + [prefs addObserver:self forKeyPath:SPGlobalResultTableFont options:NSKeyValueObservingOptionNew context:NULL]; + + // Collapse the query information pane + /*if ([queryInfoPaneSplitView collapsibleSubview]) { + [queryInfoButton setNextState]; + [queryInfoButton setToolTip:NSLocalizedString(@"Show Query Information", @"Show Query Information")]; + + [queryInfoPaneSplitView setValue:[NSNumber numberWithFloat:[queryInfoPaneSplitView collapsibleSubview].frame.size.height] forKey:@"uncollapsedSize"]; + [[queryInfoPaneSplitView collapsibleSubview] setFrameSize:NSMakeSize([queryInfoPaneSplitView collapsibleSubview].frame.size.width, 0)]; + [queryInfoPaneSplitView setCollapsibleSubviewCollapsed:YES]; + }*/ +} + +/** + * Dealloc. + */ +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [prefs removeObserver:self forKeyPath:SPGlobalResultTableFont]; + + [usedQuery release]; + [resultData release]; + [favoritesManager release]; + + if (helpHTMLTemplate) [helpHTMLTemplate release]; + if (mySQLversion) [mySQLversion release]; + if (sortField) [sortField release]; + if (cqColumnDefinition) [cqColumnDefinition release]; + if (selectionIndexToRestore) [selectionIndexToRestore release]; + if (currentQueryRanges) [currentQueryRanges release]; + + [super dealloc]; +} + +@end |