diff options
author | stuconnolly <stuart02@gmail.com> | 2010-05-28 16:07:13 +0000 |
---|---|---|
committer | stuconnolly <stuart02@gmail.com> | 2010-05-28 16:07:13 +0000 |
commit | a3cdae0d22d41758152fd864f49bb894c1bd464e (patch) | |
tree | 0ca7c4b8d1883339bf5e0685cd8332e245ab6afc /Source/SPTableContent.m | |
parent | 9eb3012a29eb9adb658159c984716971f0141446 (diff) | |
download | sequelpro-a3cdae0d22d41758152fd864f49bb894c1bd464e.tar.gz sequelpro-a3cdae0d22d41758152fd864f49bb894c1bd464e.tar.bz2 sequelpro-a3cdae0d22d41758152fd864f49bb894c1bd464e.zip |
Rename TableContent to SPTableContent.
Diffstat (limited to 'Source/SPTableContent.m')
-rw-r--r-- | Source/SPTableContent.m | 3413 |
1 files changed, 3413 insertions, 0 deletions
diff --git a/Source/SPTableContent.m b/Source/SPTableContent.m new file mode 100644 index 00000000..a040fb1c --- /dev/null +++ b/Source/SPTableContent.m @@ -0,0 +1,3413 @@ +// +// $Id$ +// +// TableDocument.h +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Wed May 01 2002. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// Forked by Abhi Beckert (abhibeckert.com) 2008-04-04 +// +// 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 "SPTableContent.h" +#import "TableDocument.h" +#import "SPTableStructure.h" +#import "SPTableInfo.h" +#import "SPTablesList.h" +#import "SPImageView.h" +#import "CMCopyTable.h" +#import "SPDataCellFormatter.h" +#import "SPTableData.h" +#import "SPQueryController.h" +#import "SPStringAdditions.h" +#import "SPArrayAdditions.h" +#import "SPTextViewAdditions.h" +#import "SPDataAdditions.h" +#import "SPTextAndLinkCell.h" +#import "QLPreviewPanel.h" +#import "SPFieldEditorController.h" +#import "SPTooltip.h" +#import "RegexKitLite.h" +#import "SPContentFilterManager.h" +#import "SPNotLoaded.h" +#import "SPConstants.h" +#import "SPDataStorage.h" +#import "SPAlertSheets.h" +#import "SPMainThreadTrampoline.h" +#import "SPHistoryController.h" + +@implementation SPTableContent + +/** + * Standard init method. Initialize various ivars. + */ +- (id)init +{ + if ((self == [super init])) { + _mainNibLoaded = NO; + isWorking = NO; + pthread_mutex_init(&tableValuesLock, NULL); + nibObjectsToRelease = [[NSMutableArray alloc] init]; + + tableValues = [[SPDataStorage alloc] init]; + tableRowsCount = 0; + previousTableRowsCount = 0; + dataColumns = [[NSMutableArray alloc] init]; + oldRow = [[NSMutableArray alloc] init]; + + selectedTable = nil; + sortCol = nil; + isDesc = NO; + keys = nil; + + currentlyEditingRow = -1; + contentPage = 1; + + sortColumnToRestore = nil; + sortColumnToRestoreIsAsc = YES; + pageToRestore = 1; + selectionIndexToRestore = nil; + selectionViewportToRestore = NSZeroRect; + filterFieldToRestore = nil; + filterComparisonToRestore = nil; + filterValueToRestore = nil; + firstBetweenValueToRestore = nil; + secondBetweenValueToRestore = nil; + tableRowsSelectable = YES; + contentFilterManager = nil; + + isFiltered = NO; + isLimited = NO; + isInterruptedLoad = NO; + + prefs = [NSUserDefaults standardUserDefaults]; + + usedQuery = [[NSString alloc] initWithString:@""]; + + // Init default filters for Content Browser + contentFilters = nil; + contentFilters = [[NSMutableDictionary alloc] init]; + numberOfDefaultFilters = [[NSMutableDictionary alloc] init]; + + NSError *readError = nil; + NSString *convError = nil; + NSPropertyListFormat format; + NSData *defaultFilterData = [NSData dataWithContentsOfFile:[NSBundle pathForResource:@"ContentFilters.plist" ofType:nil inDirectory:[[NSBundle mainBundle] bundlePath]] + options:NSMappedRead error:&readError]; + + [contentFilters setDictionary:[NSPropertyListSerialization propertyListFromData:defaultFilterData + mutabilityOption:NSPropertyListMutableContainersAndLeaves format:&format errorDescription:&convError]]; + if(contentFilters == nil || readError != nil || convError != nil) { + NSLog(@"Error while reading 'ContentFilters.plist':\n%@\n%@", [readError localizedDescription], convError); + NSBeep(); + } else { + [numberOfDefaultFilters setObject:[NSNumber numberWithInteger:[[contentFilters objectForKey:@"number"] count]] forKey:@"number"]; + [numberOfDefaultFilters setObject:[NSNumber numberWithInteger:[[contentFilters objectForKey:@"date"] count]] forKey:@"date"]; + [numberOfDefaultFilters setObject:[NSNumber numberWithInteger:[[contentFilters objectForKey:@"string"] count]] forKey:@"string"]; + } + + + } + + return self; +} + +/** + * Initialise various interface controls + */ +- (void)awakeFromNib +{ + if (_mainNibLoaded) return; + _mainNibLoaded = YES; + + // Set the table content view's vertical gridlines if required + [tableContentView setGridStyleMask:([prefs boolForKey:SPDisplayTableViewVerticalGridlines]) ? NSTableViewSolidVerticalGridLineMask : NSTableViewGridNone]; + + // Load the pagination view, keeping references to the top-level objects for later release + NSArray *paginationViewTopLevelObjects = nil; + NSNib *nibLoader = [[NSNib alloc] initWithNibNamed:@"ContentPaginationView" bundle:[NSBundle mainBundle]]; + if (![nibLoader instantiateNibWithOwner:self topLevelObjects:&paginationViewTopLevelObjects]) { + NSLog(@"Content pagination nib could not be loaded; pagination will not function correctly."); + } else { + [nibObjectsToRelease addObjectsFromArray:paginationViewTopLevelObjects]; + } + [nibLoader release]; + + // Add the pagination view to the content area + NSRect paginationViewFrame = [paginationView frame]; + NSRect paginationButtonFrame = [paginationButton frame]; + paginationViewHeight = paginationViewFrame.size.height; + paginationViewFrame.origin.x = paginationButtonFrame.origin.x + paginationButtonFrame.size.width - paginationViewFrame.size.width; + paginationViewFrame.origin.y = paginationButtonFrame.origin.y + paginationButtonFrame.size.height - 2; + paginationViewFrame.size.height = 0; + [paginationView setFrame:paginationViewFrame]; + [contentViewPane addSubview:paginationView]; + + // 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]; +} + +#pragma mark - +#pragma mark Table loading methods and information + +/* + * Loads aTable, retrieving column information and updating the tableViewColumns before + * reloading table data into the data array and redrawing the table. + */ +- (void)loadTable:(NSString *)aTable +{ + + // Abort the reload if the user is still editing a row + if ( isEditingRow ) + return; + + // If no table has been supplied, clear the table interface and return + if (!aTable || [aTable isEqualToString:@""]) { + [self performSelectorOnMainThread:@selector(setTableDetails:) withObject:nil waitUntilDone:YES]; + return; + } + + // Attempt to retrieve the table encoding; if that fails (indicating an error occurred + // while retrieving table data), or if the Rows variable is null, clear and return + if (![tableDataInstance tableEncoding] || [[[tableDataInstance statusValues] objectForKey:@"Rows"] isNSNull]) { + [self performSelectorOnMainThread:@selector(setTableDetails:) withObject:nil waitUntilDone:YES]; + return; + } + + // Post a notification that a query will be performed + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:tableDocumentInstance]; + + // Set up the table details for the new table, and trigger an interface update + NSDictionary *tableDetails = [NSDictionary dictionaryWithObjectsAndKeys: + aTable, @"name", + [tableDataInstance columns], @"columns", + [tableDataInstance columnNames], @"columnNames", + [tableDataInstance getConstraints], @"constraints", + nil]; + [self performSelectorOnMainThread:@selector(setTableDetails:) withObject:tableDetails waitUntilDone:YES]; + + // Trigger a data refresh + [self loadTableValues]; + + // Restore the view origin if appropriate + if (!NSEqualRects(selectionViewportToRestore, NSZeroRect)) { + + // Scroll the viewport to the saved location + selectionViewportToRestore.size = [tableContentView visibleRect].size; + [tableContentView scrollRectToVisible:selectionViewportToRestore]; + } + + // Restore selection indexes if appropriate + if (selectionIndexToRestore) { + BOOL previousTableRowsSelectable = tableRowsSelectable; + tableRowsSelectable = YES; + [tableContentView selectRowIndexes:selectionIndexToRestore byExtendingSelection:NO]; + tableRowsSelectable = previousTableRowsSelectable; + } + + // Update display if necessary + [[tableContentView onMainThread] setNeedsDisplay:YES]; + + // Init copyTable with necessary information for copying selected rows as SQL INSERT + [tableContentView setTableInstance:self withTableData:tableValues withColumns:dataColumns withTableName:selectedTable withConnection:mySQLConnection]; + // Post the notification that the query is finished + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:tableDocumentInstance]; + + // Clear any details to restore now that they have been restored + [self clearDetailsToRestore]; +} + +/** + * Update stored table details and update the interface to match the supplied + * table details. + * Should be called on the main thread. + */ +- (void) setTableDetails:(NSDictionary *)tableDetails +{ + NSString *newTableName; + NSInteger i; + NSNumber *colWidth, *sortColumnNumberToRestore = nil; + NSArray *columnNames; + NSDictionary *columnDefinition; + NSTableColumn *theCol; + BOOL enableInteraction = ![[tableDocumentInstance selectedToolbarItemIdentifier] isEqualToString:SPMainToolbarTableContent] || ![tableDocumentInstance isWorking]; + + if (!tableDetails) { + newTableName = nil; + } else { + newTableName = [tableDetails objectForKey:@"name"]; + } + + // Ensure the pagination view hides itself if visible, after a tiny delay for smoothness + [self performSelector:@selector(setPaginationViewVisibility:) withObject:nil afterDelay:0.1]; + + // Reset table key store for use in argumentForRow: + if (keys) [keys release], keys = nil; + + // Reset data column store + [dataColumns removeAllObjects]; + + // Check the supplied table name. If it matches the old one, a reload is being performed; + // reload the data in-place to maintain table state if possible. + if ([selectedTable isEqualToString:newTableName]) { + previousTableRowsCount = tableRowsCount; + + // Otherwise store the newly selected table name and reset the data + } else { + if (selectedTable) [selectedTable release], selectedTable = nil; + if (newTableName) selectedTable = [[NSString alloc] initWithString:newTableName]; + previousTableRowsCount = 0; + contentPage = 1; + [paginationPageField setStringValue:@"1"]; + + // Clear the selection + [tableContentView deselectAll:self]; + + // Restore the table content view to the top left + [tableContentView scrollRowToVisible:0]; + [tableContentView scrollColumnToVisible:0]; + } + + // If no table has been supplied, reset the view to a blank table and disabled elements. + if (!newTableName) { + // Remove existing columns from the table + while ([[tableContentView tableColumns] count]) { + [tableContentView removeTableColumn:NSArrayObjectAtIndex([tableContentView tableColumns], 0)]; + } + + // Empty the stored data arrays, including emptying the tableValues array + // by ressignment for thread safety. + previousTableRowsCount = 0; + [self clearTableValues]; + [tableContentView reloadData]; + isFiltered = NO; + isLimited = NO; + [countText setStringValue:@""]; + + // Reset sort column + if (sortCol) [sortCol release]; sortCol = nil; + isDesc = NO; + + // Empty and disable filter options + [fieldField setEnabled:NO]; + [fieldField removeAllItems]; + [fieldField addItemWithTitle:NSLocalizedString(@"field", @"popup menuitem for field (showing only if disabled)")]; + [compareField setEnabled:NO]; + [compareField removeAllItems]; + [compareField addItemWithTitle:NSLocalizedString(@"is", @"popup menuitem for field IS value")]; + [argumentField setHidden:NO]; + [argumentField setEnabled:NO]; + [firstBetweenField setEnabled:NO]; + [secondBetweenField setEnabled:NO]; + [firstBetweenField setStringValue:@""]; + [secondBetweenField setStringValue:@""]; + [argumentField setStringValue:@""]; + [filterButton setEnabled:NO]; + + // Hide BETWEEN operator controls + [firstBetweenField setHidden:YES]; + [secondBetweenField setHidden:YES]; + [betweenTextField setHidden:YES]; + + // Disable pagination + [paginationPreviousButton setEnabled:NO]; + [paginationButton setEnabled:NO]; + [paginationButton setTitle:@""]; + [paginationNextButton setEnabled:NO]; + + // Disable table action buttons + [addButton setEnabled:NO]; + [copyButton setEnabled:NO]; + [removeButton setEnabled:NO]; + + // Clear restoration settings + [self clearDetailsToRestore]; + + return; + } + + // Otherwise, prepare to set up the new table - the table data instance already has table details set. + + // Remove existing columns from the table + while ([[tableContentView tableColumns] count]) { + [tableContentView removeTableColumn:NSArrayObjectAtIndex([tableContentView tableColumns], 0)]; + } + + // Retrieve the field names and types for this table from the data cache. This is used when requesting all data as part + // of the fieldListForQuery method, and also to decide whether or not to preserve the current filter/sort settings. + [dataColumns addObjectsFromArray:[tableDetails objectForKey:@"columns"]]; + columnNames = [tableDetails objectForKey:@"columnNames"]; + + // Retrieve the constraints, and loop through them to add up to one foreign key to each column + NSArray *constraints = [tableDetails objectForKey:@"constraints"]; + + for (NSDictionary *constraint in constraints) + { + NSString *firstColumn = [[constraint objectForKey:@"columns"] objectAtIndex:0]; + NSString *firstRefColumn = [[[constraint objectForKey:@"ref_columns"] componentsSeparatedByString:@","] objectAtIndex:0]; + NSUInteger columnIndex = [columnNames indexOfObject:firstColumn]; + + if (columnIndex != NSNotFound && ![[dataColumns objectAtIndex:columnIndex] objectForKey:@"foreignkeyreference"]) { + NSDictionary *refDictionary = [NSDictionary dictionaryWithObjectsAndKeys: + [constraint objectForKey:@"ref_table"], @"table", + firstRefColumn, @"column", + nil]; + NSMutableDictionary *rowDictionary = [NSMutableDictionary dictionaryWithDictionary:[dataColumns objectAtIndex:columnIndex]]; + [rowDictionary setObject:refDictionary forKey:@"foreignkeyreference"]; + [dataColumns replaceObjectAtIndex:columnIndex withObject:rowDictionary]; + } + } + + NSString *nullValue = [prefs objectForKey:SPNullValue]; + NSFont *tableFont = [NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPGlobalResultTableFont]]; + [tableContentView setRowHeight:2.0f+NSSizeToCGSize([[NSString stringWithString:@"{ǞṶḹÜ∑zgyf"] sizeWithAttributes:[NSDictionary dictionaryWithObject:tableFont forKey:NSFontAttributeName]]).height]; + + // Add the new columns to the table + for ( i = 0 ; i < [dataColumns count] ; i++ ) { + columnDefinition = NSArrayObjectAtIndex(dataColumns, i); + + // Set up the column + theCol = [[NSTableColumn alloc] initWithIdentifier:[columnDefinition objectForKey:@"datacolumnindex"]]; + [[theCol headerCell] setStringValue:[columnDefinition objectForKey:@"name"]]; + [theCol setEditable:YES]; + + // Set up the data cell depending on the column type + id dataCell; + if ([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"enum"]) { + dataCell = [[[NSComboBoxCell alloc] initTextCell:@""] autorelease]; + [dataCell setButtonBordered:NO]; + [dataCell setBezeled:NO]; + [dataCell setDrawsBackground:NO]; + [dataCell setCompletes:YES]; + [dataCell setControlSize:NSSmallControlSize]; + // add prefs NULL value representation if NULL value is allowed for that field + if([[columnDefinition objectForKey:@"null"] boolValue]) + [dataCell addItemWithObjectValue:nullValue]; + [dataCell addItemsWithObjectValues:[columnDefinition objectForKey:@"values"]]; + + // Add a foreign key arrow if applicable + } else if ([columnDefinition objectForKey:@"foreignkeyreference"]) { + dataCell = [[[SPTextAndLinkCell alloc] initTextCell:@""] autorelease]; + [dataCell setTarget:self action:@selector(clickLinkArrow:)]; + + // Otherwise instantiate a text-only cell + } else { + dataCell = [[[SPTextAndLinkCell alloc] initTextCell:@""] autorelease]; + } + [dataCell setEditable:YES]; + + // Set the line break mode and an NSFormatter subclass which truncates long strings for display + [dataCell setLineBreakMode:NSLineBreakByTruncatingTail]; + [dataCell setFormatter:[[SPDataCellFormatter new] autorelease]]; + + // Set field length limit if field is a varchar to match varchar length + if ([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"string"]) { + [[dataCell formatter] setTextLimit:[[columnDefinition objectForKey:@"length"] integerValue]]; + } + + // Set the data cell font according to the preferences + [dataCell setFont:tableFont]; + + // Assign the data cell + [theCol setDataCell:dataCell]; + + // Set the width of this column to saved value if exists + colWidth = [[[[prefs objectForKey:SPTableColumnWidths] objectForKey:[NSString stringWithFormat:@"%@@%@", [tableDocumentInstance database], [tableDocumentInstance host]]] objectForKey:[tablesListInstance tableName]] objectForKey:[columnDefinition objectForKey:@"name"]]; + if ( colWidth ) { + [theCol setWidth:[colWidth doubleValue]]; + } + + // Set the column to be reselected for sorting if appropriate + if (sortColumnToRestore && [sortColumnToRestore isEqualToString:[columnDefinition objectForKey:@"name"]]) + sortColumnNumberToRestore = [columnDefinition objectForKey:@"datacolumnindex"]; + + // Add the column to the table + [tableContentView addTableColumn:theCol]; + [theCol release]; + } + + // If the table has been reloaded and the previously selected sort column is still present, reselect it. + if (sortColumnNumberToRestore) { + theCol = [tableContentView tableColumnWithIdentifier:sortColumnNumberToRestore]; + if (sortCol) [sortCol release]; + sortCol = [sortColumnNumberToRestore copy]; + [tableContentView setHighlightedTableColumn:theCol]; + isDesc = !sortColumnToRestoreIsAsc; + if ( isDesc ) { + [tableContentView setIndicatorImage:[NSImage imageNamed:@"NSDescendingSortIndicator"] inTableColumn:theCol]; + } else { + [tableContentView setIndicatorImage:[NSImage imageNamed:@"NSAscendingSortIndicator"] inTableColumn:theCol]; + } + + // Otherwise, clear sorting + } else { + if (sortCol) { + [sortCol release]; + sortCol = nil; + } + isDesc = NO; + } + + // Store the current first responder so filter field doesn't steal focus + id currentFirstResponder = [[tableDocumentInstance parentWindow] firstResponder]; + + // Enable and initialize filter fields (with tags for position of menu item and field position) + [fieldField setEnabled:YES]; + [fieldField removeAllItems]; + [fieldField addItemsWithTitles:columnNames]; + for ( i = 0 ; i < [fieldField numberOfItems] ; i++ ) { + [[fieldField itemAtIndex:i] setTag:i]; + } + [compareField setEnabled:YES]; + [self setCompareTypes:self]; + [argumentField setEnabled:YES]; + [argumentField setStringValue:@""]; + [filterButton setEnabled:enableInteraction]; + + // Restore preserved filter settings if appropriate and valid + if (filterFieldToRestore) { + [fieldField selectItemWithTitle:filterFieldToRestore]; + [self setCompareTypes:self]; + + if ([fieldField itemWithTitle:filterFieldToRestore] + && ((!filterComparisonToRestore && filterValueToRestore) + || [compareField itemWithTitle:filterComparisonToRestore])) + { + if (filterComparisonToRestore) [compareField selectItemWithTitle:filterComparisonToRestore]; + if([filterComparisonToRestore isEqualToString:@"BETWEEN"]) { + [argumentField setHidden:YES]; + if (firstBetweenValueToRestore) [firstBetweenField setStringValue:firstBetweenValueToRestore]; + if (secondBetweenValueToRestore) [secondBetweenField setStringValue:secondBetweenValueToRestore]; + } else { + if (filterValueToRestore) [argumentField setStringValue:filterValueToRestore]; + } + [self toggleFilterField:self]; + + } + } + + // Restore page number if limiting is set + if ([prefs boolForKey:SPLimitResults]) contentPage = pageToRestore; + + // Restore first responder + [[tableDocumentInstance parentWindow] makeFirstResponder:currentFirstResponder]; + + // Set the state of the table buttons + [addButton setEnabled:enableInteraction]; + [copyButton setEnabled:NO]; + [removeButton setEnabled:NO]; + + // Reset the table store if required - basically if the table is being changed, + // reassigning before emptying for thread safety. + if (!previousTableRowsCount) { + [self clearTableValues]; + } + +} + +/** + * Remove all items from the current table value store. Do this by + * reassigning the tableValues store and releasing the old location, + * while setting thread safety flags. + */ +- (void) clearTableValues +{ + SPDataStorage *tableValuesTransition; + + tableValuesTransition = tableValues; + pthread_mutex_lock(&tableValuesLock); + tableRowsCount = 0; + tableValues = [[SPDataStorage alloc] init]; + [tableContentView setTableData:tableValues]; + pthread_mutex_unlock(&tableValuesLock); + [tableValuesTransition release]; +} + +/** + * Reload the table data without reconfiguring the tableView, + * using filters and limits as appropriate. + * Will not refresh the table view itself. + * Note that this does not empty the table array - see use of previousTableRowsCount. + */ +- (void) loadTableValues +{ + // If no table is selected, return + if (!selectedTable) return; + + NSMutableString *queryString; + NSString *queryStringBeforeLimit = nil; + NSString *filterString; + MCPStreamingResult *streamingResult; + NSInteger rowsToLoad = [[tableDataInstance statusValueForKey:@"Rows"] integerValue]; + + [countText setStringValue:NSLocalizedString(@"Loading table data...", @"Loading table data string")]; + + // Notify any listeners that a query has started + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:tableDocumentInstance]; + + // Start construction of the query string + queryString = [NSMutableString stringWithFormat:@"SELECT %@ FROM %@", [self fieldListForQuery], [selectedTable backtickQuotedString]]; + + // Add a filter string if appropriate + filterString = [self tableFilterString]; + + if (filterString) { + [queryString appendFormat:@" WHERE %@", filterString]; + isFiltered = YES; + } else { + isFiltered = NO; + } + + // Add sorting details if appropriate + if (sortCol) { + [queryString appendFormat:@" ORDER BY %@", [[[dataColumns objectAtIndex:[sortCol integerValue]] objectForKey:@"name"] backtickQuotedString]]; + if (isDesc) [queryString appendString:@" DESC"]; + } + + // Check to see if a limit needs to be applied + if ([prefs boolForKey:SPLimitResults]) { + + // Ensure the page supplied is within the appropriate limits + if (contentPage <= 0) + contentPage = 1; + else if (contentPage > 1 && (contentPage - 1) * [prefs integerForKey:SPLimitResultsValue] >= maxNumRows) + contentPage = ceil((CGFloat)maxNumRows / [prefs floatForKey:SPLimitResultsValue]); + + // If the result set is from a late page, take a copy of the string to allow resetting limit + // if no results are found + if (contentPage > 1) { + queryStringBeforeLimit = [NSString stringWithString:queryString]; + } + + // Append the limit settings + [queryString appendFormat:@" LIMIT %ld,%ld", (long)((contentPage-1)*[prefs integerForKey:SPLimitResultsValue]), (long)[prefs integerForKey:SPLimitResultsValue]]; + + // Update the approximate count of the rows to load + rowsToLoad = rowsToLoad - (contentPage-1)*[prefs integerForKey:SPLimitResultsValue]; + if (rowsToLoad > [prefs integerForKey:SPLimitResultsValue]) rowsToLoad = [prefs integerForKey:SPLimitResultsValue]; + } + + // If within a task, allow this query to be cancelled + [tableDocumentInstance enableTaskCancellationWithTitle:NSLocalizedString(@"Stop", @"stop button") callbackObject:nil callbackFunction:NULL]; + + // Perform and process the query + [tableContentView performSelectorOnMainThread:@selector(noteNumberOfRowsChanged) withObject:nil waitUntilDone:YES]; + [self setUsedQuery:queryString]; + streamingResult = [[mySQLConnection streamingQueryString:queryString] retain]; + + // Ensure the number of columns are unchanged; if the column count has changed, abort the load + // and queue a full table reload. + BOOL fullTableReloadRequired = NO; + if (streamingResult && [dataColumns count] != [streamingResult numOfFields]) { + [tableDocumentInstance disableTaskCancellation]; + [mySQLConnection cancelCurrentQuery]; + [streamingResult cancelResultLoad]; + fullTableReloadRequired = YES; + } + + // Process the result into the data store + if (!fullTableReloadRequired && streamingResult) { + [self processResultIntoDataStorage:streamingResult approximateRowCount:rowsToLoad]; + } + if (streamingResult) [streamingResult release]; + + // If the result is empty, and a late page is selected, reset the page + if (!fullTableReloadRequired && [prefs boolForKey:SPLimitResults] && queryStringBeforeLimit && !tableRowsCount && ![mySQLConnection queryCancelled]) { + contentPage = 1; + queryString = [NSMutableString stringWithFormat:@"%@ LIMIT 0,%ld", queryStringBeforeLimit, (long)[prefs integerForKey:SPLimitResultsValue]]; + [self setUsedQuery:queryString]; + streamingResult = [[mySQLConnection streamingQueryString:queryString] retain]; + if (streamingResult) { + [self processResultIntoDataStorage:streamingResult approximateRowCount:[prefs integerForKey:SPLimitResultsValue]]; + [streamingResult release]; + } + } + + if ([mySQLConnection queryCancelled] || [mySQLConnection queryErrored]) + isInterruptedLoad = YES; + else + isInterruptedLoad = NO; + + // End cancellation ability + [tableDocumentInstance disableTaskCancellation]; + + if ([prefs boolForKey:SPLimitResults] + && (contentPage > 1 + || tableRowsCount == [prefs integerForKey:SPLimitResultsValue])) + { + isLimited = YES; + } else { + isLimited = NO; + } + + // Update the rows count as necessary + [self updateNumberOfRows]; + + // Set the filter text + [self updateCountText]; + + // Update pagination + [self updatePaginationState]; + + // Notify listenters that the query has finished + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:tableDocumentInstance]; + + // Trigger a full reload if required + if (fullTableReloadRequired) [self reloadTable:self]; +} + +/* + * Processes a supplied streaming result set, loading it into the data array. + */ +- (void)processResultIntoDataStorage:(MCPStreamingResult *)theResult approximateRowCount:(NSUInteger)targetRowCount +{ + NSArray *tempRow; + NSUInteger i; + NSUInteger dataColumnsCount = [dataColumns count]; + BOOL *columnBlobStatuses = malloc(dataColumnsCount * sizeof(BOOL)); + + // Set the column count on the data store + [tableValues setColumnCount:dataColumnsCount]; + + CGFloat relativeTargetRowCount = 100.0/targetRowCount; + NSUInteger nextTableDisplayBoundary = 50; + BOOL tableViewRedrawn = NO; + + NSUInteger rowsProcessed = 0; + + NSAutoreleasePool *dataLoadingPool; + NSProgressIndicator *dataLoadingIndicator = [tableDocumentInstance valueForKey:@"queryProgressBar"]; + BOOL prefsLoadBlobsAsNeeded = [prefs boolForKey:SPLoadBlobsAsNeeded]; + + // Build up an array of which columns are blobs for faster iteration + for ( i = 0; i < dataColumnsCount ; i++ ) { + columnBlobStatuses[i] = [tableDataInstance columnIsBlobOrText:[NSArrayObjectAtIndex(dataColumns, i) objectForKey:@"name"]]; + } + + // 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(&tableValuesLock); + + if (rowsProcessed < previousTableRowsCount) { + SPDataStorageReplaceRow(tableValues, rowsProcessed, tempRow); + } else { + SPDataStorageAddRow(tableValues, tempRow); + } + + // Alter the values for hidden blob and text fields if appropriate + if ( prefsLoadBlobsAsNeeded ) { + for ( i = 0 ; i < dataColumnsCount ; i++ ) { + if (columnBlobStatuses[i]) { + SPDataStorageReplaceObjectAtRowAndColumn(tableValues, rowsProcessed, i, [SPNotLoaded notLoaded]); + } + } + } + rowsProcessed++; + + pthread_mutex_unlock(&tableValuesLock); + + // Update the task interface as necessary + if (!isFiltered) { + if (rowsProcessed < targetRowCount) { + [tableDocumentInstance setTaskPercentage:(rowsProcessed*relativeTargetRowCount)]; + } else if (rowsProcessed == targetRowCount) { + [tableDocumentInstance setTaskPercentage:100.0]; + [[tableDocumentInstance onMainThread] setTaskProgressToIndeterminateAfterDelay:YES]; + } + } + + // Update the table view with new results every now and then + if (rowsProcessed > nextTableDisplayBoundary) { + if (rowsProcessed > tableRowsCount) tableRowsCount = rowsProcessed; + [[tableContentView onMainThread] noteNumberOfRowsChanged]; + if (!tableViewRedrawn) { + [[tableContentView onMainThread] setNeedsDisplay:YES]; + tableViewRedrawn = YES; + } + nextTableDisplayBoundary *= 2; + } + + // Drain and reset the autorelease pool every ~1024 rows + if (!(rowsProcessed % 1024)) { + [dataLoadingPool drain]; + dataLoadingPool = [[NSAutoreleasePool alloc] init]; + } + } + tableRowsCount = rowsProcessed; + + // If the reloaded table is shorter than the previous table, remove the extra values from the storage + if (tableRowsCount < [tableValues count]) { + pthread_mutex_lock(&tableValuesLock); + [tableValues removeRowsInRange:NSMakeRange(tableRowsCount, [tableValues count] - tableRowsCount)]; + pthread_mutex_unlock(&tableValuesLock); + } + + // Ensure the table is aware of changes + if ([NSThread isMainThread]) { + [tableContentView performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:YES]; + } else { + [tableContentView performSelectorOnMainThread:@selector(noteNumberOfRowsChanged) withObject:nil waitUntilDone:YES]; + [tableContentView performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO]; + } + + // Clean up the autorelease pool and reset the progress indicator + [dataLoadingPool drain]; + [dataLoadingIndicator setIndeterminate:YES]; + + free(columnBlobStatuses); +} + +/** + * Returns the query string for the current filter settings, + * ready to be dropped into a WHERE clause, or nil if no filtering + * is active. + */ +- (NSString *)tableFilterString +{ + + // If the clause has the placeholder $BINARY that placeholder will be replaced + // by BINARY if the user pressed ⇧ while invoking 'Filter' otherwise it will + // replaced by @"". + BOOL caseSensitive = (([[[NSApp onMainThread] currentEvent] modifierFlags] + & (NSShiftKeyMask|NSControlKeyMask|NSAlternateKeyMask|NSCommandKeyMask)) > 0); + + NSString *filterString; + + if(contentFilters == nil) { + NSLog(@"Fatal error while retrieving content filters. No filters found."); + NSBeep(); + return nil; + } + + // Current selected filter type + if(![contentFilters objectForKey:compareType]) { + NSLog(@"Error while retrieving filters. Filter type “%@” unknown.", compareType); + NSBeep(); + return nil; + } + NSDictionary *filter = [[contentFilters objectForKey:compareType] objectAtIndex:[[compareField selectedItem] tag]]; + + if(![filter objectForKey:@"Clause"] || ![filter objectForKey:@"NumberOfArguments"]) { + NSLog(@"Error while retrieving filter clause. No “Clause” or/and “NumberOfArguments” key found."); + NSBeep(); + return nil; + } + + NSUInteger numberOfArguments = [[filter objectForKey:@"NumberOfArguments"] integerValue]; + + BOOL suppressLeadingTablePlaceholder = NO; + if([filter objectForKey:@"SuppressLeadingFieldPlaceholder"]) + suppressLeadingTablePlaceholder = YES; + + // argument if Filter requires only one argument + NSMutableString *argument = [[NSMutableString alloc] initWithString:[argumentField stringValue]]; + + // If the filter field is empty and the selected filter does not require + // only one argument, then no filtering is required - return nil. + if (![argument length] && numberOfArguments == 1) { + [argument release]; + return nil; + } + + // arguments if Filter requires two arguments + NSMutableString *firstBetweenArgument = [[NSMutableString alloc] initWithString:[firstBetweenField stringValue]]; + NSMutableString *secondBetweenArgument = [[NSMutableString alloc] initWithString:[secondBetweenField stringValue]]; + + // If filter requires two arguments and either of the argument fields are empty + // return nil. + if (numberOfArguments == 2) { + if (([firstBetweenArgument length] == 0) || ([secondBetweenArgument length] == 0)) { + [argument release]; + [firstBetweenArgument release]; + [secondBetweenArgument release]; + return nil; + } + } + + // Retrieve actual WHERE clause + NSMutableString *clause = [[NSMutableString alloc] init]; + [clause setString:[filter objectForKey:@"Clause"]]; + + [clause replaceOccurrencesOfRegex:@"(?<!\\\\)\\$BINARY" withString:(caseSensitive) ? @"BINARY" : @""]; + [clause flushCachedRegexData]; + [clause replaceOccurrencesOfRegex:@"(?<!\\\\)\\$CURRENT_FIELD" withString:([fieldField titleOfSelectedItem]) ? [[fieldField titleOfSelectedItem] backtickQuotedString] : @""]; + [clause flushCachedRegexData]; + + // Escape % sign + [clause replaceOccurrencesOfRegex:@"%" withString:@"%%"]; + [clause flushCachedRegexData]; + + // Replace placeholder ${} by %@ + NSRange matchedRange; + NSString *re = @"(?<!\\\\)\\$\\{.*?\\}"; + if([clause isMatchedByRegex:re]) { + while([clause isMatchedByRegex:re]) { + matchedRange = [clause rangeOfRegex:re]; + [clause replaceCharactersInRange:matchedRange withString:@"%@"]; + [clause flushCachedRegexData]; + } + } + + // Check number of placeholders and given 'NumberOfArguments' + if([clause replaceOccurrencesOfString:@"%@" withString:@"%@" options:NSLiteralSearch range:NSMakeRange(0, [clause length])] != numberOfArguments) { + NSLog(@"Error while setting filter string. “NumberOfArguments” differs from the number of arguments specified in “Clause”."); + NSBeep(); + [argument release]; + [firstBetweenArgument release]; + [secondBetweenArgument release]; + [clause release]; + return nil; + } + + // Construct the filter string according the required number of arguments + + if(suppressLeadingTablePlaceholder) { + if (numberOfArguments == 2) { + filterString = [NSString stringWithFormat:clause, + [self escapeFilterArgument:firstBetweenArgument againstClause:clause], + [self escapeFilterArgument:secondBetweenArgument againstClause:clause]]; + } else if (numberOfArguments == 1) { + filterString = [NSString stringWithFormat:clause, [self escapeFilterArgument:argument againstClause:clause]]; + } else { + filterString = [NSString stringWithString:clause]; + if(numberOfArguments > 2) { + NSLog(@"Filter with more than 2 arguments is not yet supported."); + NSBeep(); + } + } + } else { + if (numberOfArguments == 2) { + filterString = [NSString stringWithFormat:@"%@ %@", + [[fieldField titleOfSelectedItem] backtickQuotedString], + [NSString stringWithFormat:clause, + [self escapeFilterArgument:firstBetweenArgument againstClause:clause], + [self escapeFilterArgument:secondBetweenArgument againstClause:clause]]]; + } else if (numberOfArguments == 1) { + filterString = [NSString stringWithFormat:@"%@ %@", + [[fieldField titleOfSelectedItem] backtickQuotedString], + [NSString stringWithFormat:clause, [self escapeFilterArgument:argument againstClause:clause]]]; + } else { + filterString = [NSString stringWithFormat:@"%@ %@", + [[fieldField titleOfSelectedItem] backtickQuotedString], clause]; + if(numberOfArguments > 2) { + NSLog(@"Filter with more than 2 arguments is not yet supported."); + NSBeep(); + } + } + } + + [argument release]; + [firstBetweenArgument release]; + [secondBetweenArgument release]; + [clause release]; + + // Return the filter string + return filterString; +} + +- (NSString *)escapeFilterArgument:(NSString *)argument againstClause:(NSString *)clause +{ + + NSMutableString *arg = [[NSMutableString alloc] init]; + [arg setString:argument]; + + [arg replaceOccurrencesOfRegex:@"(\\\\)(?![nrt_%])" withString:@"\\\\\\\\\\\\\\\\"]; + [arg flushCachedRegexData]; + [arg replaceOccurrencesOfRegex:@"(\\\\)(?=[nrt])" withString:@"\\\\\\"]; + [arg flushCachedRegexData]; + + // Get quote sign for escaping - this should work for 99% of all cases + NSString *quoteSign = [clause stringByMatching:@"([\"'])[^\\1]*?%@[^\\1]*?\\1" capture:1L]; + // Esape argument + if(quoteSign != nil && [quoteSign length] == 1) { + [arg replaceOccurrencesOfRegex:[NSString stringWithFormat:@"(%@)", quoteSign] withString:@"\\\\$1"]; + [arg flushCachedRegexData]; + } + // if([clause isMatchedByRegex:@"(?i)\\blike\\b.*?%(?!@)"]) { + // [arg replaceOccurrencesOfRegex:@"([_%])" withString:@"\\\\$1"]; + // [arg flushCachedRegexData]; + // } + return [arg autorelease]; +} + +/* + * Update the table count/selection text + */ +- (void)updateCountText +{ + NSString *rowString; + NSMutableString *countString = [NSMutableString string]; + NSNumberFormatter *numberFormatter = [[[NSNumberFormatter alloc] init] autorelease]; + [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle]; + + // Set up a couple of common strings + NSString *tableCountString = [numberFormatter stringFromNumber:[NSNumber numberWithUnsignedInteger:tableRowsCount]]; + NSString *maxRowsString = [numberFormatter stringFromNumber:[NSNumber numberWithUnsignedInteger:maxNumRows]]; + + // If the result is partial due to an error or query cancellation, show a very basic count + if (isInterruptedLoad) { + if (tableRowsCount == 1) + [countString appendFormat:NSLocalizedString(@"%@ row in partial load", @"text showing a single row a partially loaded result"), tableCountString]; + else + [countString appendFormat:NSLocalizedString(@"%@ rows in partial load", @"text showing how many rows are in a partially loaded result"), tableCountString]; + + // If no filter or limit is active, show just the count of rows in the table + } else if (!isFiltered && !isLimited) { + if (tableRowsCount == 1) + [countString appendFormat:NSLocalizedString(@"%@ row in table", @"text showing a single row in the result"), tableCountString]; + else + [countString appendFormat:NSLocalizedString(@"%@ rows in table", @"text showing how many rows are in the result"), tableCountString]; + + // If a limit is active, display a string suggesting a limit is active + } else if (!isFiltered && isLimited) { + NSUInteger limitStart = (contentPage-1)*[prefs integerForKey:SPLimitResultsValue] + 1; + [countString appendFormat:NSLocalizedString(@"Rows %@ - %@ of %@%@ from table", @"text showing how many rows are in the limited result"), [numberFormatter stringFromNumber:[NSNumber numberWithUnsignedInteger:limitStart]], [numberFormatter stringFromNumber:[NSNumber numberWithUnsignedInteger:(limitStart+tableRowsCount-1)]], maxNumRowsIsEstimate?@"~":@"", maxRowsString]; + + // If just a filter is active, show a count and an indication a filter is active + } else if (isFiltered && !isLimited) { + if (tableRowsCount == 1) + [countString appendFormat:NSLocalizedString(@"%@ row of %@%@ matches filter", @"text showing how a single rows matched filter"), tableCountString, maxNumRowsIsEstimate?@"~":@"", maxRowsString]; + else + [countString appendFormat:NSLocalizedString(@"%@ rows of %@%@ match filter", @"text showing how many rows matched filter"), tableCountString, maxNumRowsIsEstimate?@"~":@"", maxRowsString]; + + // If both a filter and limit is active, display full string + } else { + NSUInteger limitStart = (contentPage-1)*[prefs integerForKey:SPLimitResultsValue] + 1; + [countString appendFormat:NSLocalizedString(@"Rows %@ - %@ from filtered matches", @"text showing how many rows are in the limited filter match"), [numberFormatter stringFromNumber:[NSNumber numberWithUnsignedInteger:limitStart]], [numberFormatter stringFromNumber:[NSNumber numberWithUnsignedInteger:(limitStart+tableRowsCount-1)]]]; + } + + // If rows are selected, append selection count + if ([tableContentView numberOfSelectedRows] > 0) { + [countString appendString:@"; "]; + if ([tableContentView numberOfSelectedRows] == 1) + rowString = [NSString stringWithString:NSLocalizedString(@"row", @"singular word for row")]; + else + rowString = [NSString stringWithString:NSLocalizedString(@"rows", @"plural word for rows")]; + [countString appendFormat:NSLocalizedString(@"%@ %@ selected", @"text showing how many rows are selected"), [numberFormatter stringFromNumber:[NSNumber numberWithInteger:[tableContentView numberOfSelectedRows]]], rowString]; + } + + [[countText onMainThread] setStringValue:countString]; +} + +#pragma mark - +#pragma mark Table interface actions + +/* + * Reloads the current table data, performing a new SQL query. Now attempts to preserve sort + * order, filters, and viewport. Performs the action in a new thread if a task is not already + * running. + */ +- (IBAction)reloadTable:(id)sender +{ + [tableDocumentInstance startTaskWithDescription:NSLocalizedString(@"Reloading data...", @"Reloading data task description")]; + + if ([NSThread isMainThread]) { + [NSThread detachNewThreadSelector:@selector(reloadTableTask) toTarget:self withObject:nil]; + } else { + [self reloadTableTask]; + } +} + +- (void)reloadTableTask +{ + NSAutoreleasePool *reloadPool = [[NSAutoreleasePool alloc] init]; + + // Check whether a save of the current row is required. + if (![[self onMainThread] saveRowOnDeselect]) return; + + // Save view details to restore safely if possible (except viewport, which will be + // preserved automatically, and can then be scrolled as the table loads) + [self storeCurrentDetailsForRestoration]; + [self setViewportToRestore:NSZeroRect]; + + // Clear the table data column cache + [tableDataInstance resetColumnData]; + + // Load the table's data + [self loadTable:[tablesListInstance tableName]]; + + [tableDocumentInstance endTask]; + + [reloadPool drain]; +} + +/* + * Filter the table with arguments given by the user. + * Performs the action in a new thread if necessary. + */ +- (IBAction)filterTable:(id)sender +{ + NSString *taskString; + + if ([tableDocumentInstance isWorking]) return; + [self setPaginationViewVisibility:FALSE]; + + // Select the correct pagination value + if (![prefs boolForKey:SPLimitResults] || [paginationPageField integerValue] <= 0) + contentPage = 1; + else if (([paginationPageField integerValue] - 1) * [prefs integerForKey:SPLimitResultsValue] >= maxNumRows) + contentPage = ceil((CGFloat)maxNumRows / [prefs floatForKey:SPLimitResultsValue]); + else + contentPage = [paginationPageField integerValue]; + + if ([self tableFilterString]) { + taskString = NSLocalizedString(@"Filtering table...", @"Filtering table task description"); + } else if (contentPage == 1) { + taskString = [NSString stringWithFormat:NSLocalizedString(@"Loading %@...", @"Loading table task string"), selectedTable]; + } else { + taskString = [NSString stringWithFormat:NSLocalizedString(@"Loading page %lu...", @"Loading table page task string"), (unsigned long)contentPage]; + } + + [tableDocumentInstance startTaskWithDescription:taskString]; + + if ([NSThread isMainThread]) { + [NSThread detachNewThreadSelector:@selector(filterTableTask) toTarget:self withObject:nil]; + } else { + [self filterTableTask]; + } +} +- (void)filterTableTask +{ + NSAutoreleasePool *filterPool = [[NSAutoreleasePool alloc] init]; + + // Check whether a save of the current row is required. + if (![[self onMainThread] saveRowOnDeselect]) return; + + // Update history + [spHistoryControllerInstance updateHistoryEntries]; + + // Reset and reload data using the new filter settings + previousTableRowsCount = 0; + [self clearTableValues]; + [self loadTableValues]; + [[tableContentView onMainThread] scrollPoint:NSMakePoint(0.0, 0.0)]; + + [tableDocumentInstance endTask]; + [filterPool drain]; +} + +/** + * Enables or disables the filter input field based on the selected filter type. + */ +- (IBAction)toggleFilterField:(id)sender +{ + + // Check if user called "Edit Filter…" + if([[compareField selectedItem] tag] == [[contentFilters objectForKey:compareType] count]) { + [self openContentFilterManager]; + return; + } + + // Remember last selection for "Edit filter…" + lastSelectedContentFilterIndex = [[compareField selectedItem] tag]; + + NSDictionary *filter = [[contentFilters objectForKey:compareType] objectAtIndex:lastSelectedContentFilterIndex]; + NSUInteger numOfArgs = [[filter objectForKey:@"NumberOfArguments"] integerValue]; + if (numOfArgs == 2) { + [argumentField setHidden:YES]; + + if([filter objectForKey:@"ConjunctionLabels"] && [[filter objectForKey:@"ConjunctionLabels"] count] == 1) + [betweenTextField setStringValue:[[filter objectForKey:@"ConjunctionLabels"] objectAtIndex:0]]; + else + [betweenTextField setStringValue:@""]; + + [betweenTextField setHidden:NO]; + [firstBetweenField setHidden:NO]; + [secondBetweenField setHidden:NO]; + + [firstBetweenField setEnabled:YES]; + [secondBetweenField setEnabled:YES]; + [firstBetweenField selectText:self]; + } + else if (numOfArgs == 1){ + [argumentField setHidden:NO]; + [argumentField setEnabled:YES]; + [argumentField selectText:self]; + + [betweenTextField setHidden:YES]; + [firstBetweenField setHidden:YES]; + [secondBetweenField setHidden:YES]; + } + else { + [argumentField setHidden:NO]; + [argumentField setEnabled:NO]; + + [betweenTextField setHidden:YES]; + [firstBetweenField setHidden:YES]; + [secondBetweenField setHidden:YES]; + + // Start search if no argument is required + if(numOfArgs == 0) + [self filterTable:self]; + } + +} + +- (NSString *)usedQuery +{ + return usedQuery; +} + +- (void)setUsedQuery:(NSString *)query +{ + if (usedQuery) [usedQuery release]; + usedQuery = [[NSString alloc] initWithString:query]; +} + +#pragma mark - +#pragma mark Pagination + +/** + * Move the pagination backwards or forwards one page + */ +- (IBAction) navigatePaginationFromButton:(id)sender +{ + if (sender == paginationPreviousButton) { + if (contentPage <= 1) return; + [paginationPageField setIntegerValue:(contentPage - 1)]; + [self filterTable:sender]; + } else if (sender == paginationNextButton) { + if (contentPage * [prefs integerForKey:SPLimitResultsValue] >= maxNumRows) return; + [paginationPageField setIntegerValue:(contentPage + 1)]; + [self filterTable:sender]; + } +} + +/** + * When the Pagination button is pressed, show or hide the pagination + * layer depending on the current state. + */ +- (IBAction) togglePagination:(id)sender +{ + if ([sender state] == NSOnState) [self setPaginationViewVisibility:YES]; + else [self setPaginationViewVisibility:NO]; +} + +/** + * Show or hide the pagination layer, also changing the first responder as appropriate. + */ +- (void) setPaginationViewVisibility:(BOOL)makeVisible +{ + NSRect paginationViewFrame = [paginationView frame]; + + if (makeVisible) { + if (paginationViewFrame.size.height == paginationViewHeight) return; + paginationViewFrame.size.height = paginationViewHeight; + [paginationButton setState:NSOnState]; + [paginationButton setImage:[NSImage imageNamed:@"button_action"]]; + [[tableDocumentInstance parentWindow] makeFirstResponder:paginationPageField]; + } else { + if (paginationViewFrame.size.height == 0) return; + paginationViewFrame.size.height = 0; + [paginationButton setState:NSOffState]; + [paginationButton setImage:[NSImage imageNamed:@"button_pagination"]]; + if ([[tableDocumentInstance parentWindow] firstResponder] == paginationPageField + || ([[[tableDocumentInstance parentWindow] firstResponder] respondsToSelector:@selector(superview)] + && [(id)[[tableDocumentInstance parentWindow] firstResponder] superview] + && [[(id)[[tableDocumentInstance parentWindow] firstResponder] superview] respondsToSelector:@selector(superview)] + && [[(id)[[tableDocumentInstance parentWindow] firstResponder] superview] superview] == paginationPageField)) + { + [[tableDocumentInstance parentWindow] makeFirstResponder:nil]; + } + } + + [[paginationView animator] setFrame:paginationViewFrame]; +} + +/** + * Update the state of the pagination buttons and text. + */ +- (void) updatePaginationState +{ + NSUInteger maxPage = ceil((CGFloat)maxNumRows / [prefs floatForKey:SPLimitResultsValue]); + if (isFiltered && !isLimited) { + maxPage = contentPage; + } + BOOL enabledMode = ![tableDocumentInstance isWorking]; + + NSNumberFormatter *numberFormatter = [[[NSNumberFormatter alloc] init] autorelease]; + [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle]; + + // Set up the previous page button + if ([prefs boolForKey:SPLimitResults] && contentPage > 1) + [paginationPreviousButton setEnabled:enabledMode]; + else + [paginationPreviousButton setEnabled:NO]; + + // Set up the next page button + if ([prefs boolForKey:SPLimitResults] && contentPage < maxPage) + [paginationNextButton setEnabled:enabledMode]; + else + [paginationNextButton setEnabled:NO]; + + // As long as a table is selected (which it will be if this is called), enable pagination detail button + [paginationButton setEnabled:enabledMode]; + + // Set the values and maximums for the text field and associated pager + [paginationPageField setStringValue:[numberFormatter stringFromNumber:[NSNumber numberWithUnsignedInteger:contentPage]]]; + [[paginationPageField formatter] setMaximum:[NSNumber numberWithUnsignedInteger:maxPage]]; + [paginationPageStepper setIntegerValue:contentPage]; + [paginationPageStepper setMaxValue:maxPage]; +} + +#pragma mark - +#pragma mark Edit methods + +/* + * Adds an empty row to the table-array and goes into edit mode + */ +- (IBAction)addRow:(id)sender +{ + NSMutableDictionary *column; + NSMutableArray *newRow = [NSMutableArray array]; + NSUInteger i; + + // Check whether a save of the current row is required. + if ( ![self saveRowOnDeselect] ) return; + + for ( i = 0 ; i < [dataColumns count] ; i++ ) { + column = NSArrayObjectAtIndex(dataColumns, i); + if ([column objectForKey:@"default"] == nil || [column objectForKey:@"default"] == [NSNull null]) { + [newRow addObject:[NSNull null]]; + } else { + [newRow addObject:[column objectForKey:@"default"]]; + } + } + [tableValues addRowWithContents:newRow]; + tableRowsCount++; + + [tableContentView reloadData]; + [tableContentView selectRowIndexes:[NSIndexSet indexSetWithIndex:[tableContentView numberOfRows]-1] byExtendingSelection:NO]; + [tableContentView scrollRowToVisible:[tableContentView selectedRow]]; + isEditingRow = YES; + isEditingNewRow = YES; + currentlyEditingRow = [tableContentView selectedRow]; + if ( [multipleLineEditingButton state] == NSOffState ) + [tableContentView editColumn:0 row:[tableContentView numberOfRows]-1 withEvent:nil select:YES]; +} + +/** + * Copies a row of the table-array and goes into edit mode + */ +- (IBAction)copyRow:(id)sender +{ + NSMutableArray *tempRow; + MCPResult *queryResult; + NSDictionary *row; + NSArray *dbDataRow = nil; + NSUInteger i; + + // Check whether a save of the current row is required. + if ( ![self saveRowOnDeselect] ) return; + + if ( [tableContentView numberOfSelectedRows] < 1 ) + return; + if ( [tableContentView numberOfSelectedRows] > 1 ) { + SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocumentInstance parentWindow], self, nil, nil, NSLocalizedString(@"You can only copy single rows.", @"message of panel when trying to copy multiple rows")); + return; + } + + //copy row + tempRow = [tableValues rowContentsAtIndex:[tableContentView selectedRow]]; + + //if we don't show blobs, read data for this duplicate column from db + if ([prefs boolForKey:SPLoadBlobsAsNeeded]) { + // Abort if there are no indices on this table - argumentForRow will display an error. + if (![[self argumentForRow:[tableContentView selectedRow]] length]){ + return; + } + //if we have indexes, use argumentForRow + queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SELECT * FROM %@ WHERE %@", [selectedTable backtickQuotedString], [self argumentForRow:[tableContentView selectedRow]]]]; + dbDataRow = [queryResult fetchRowAsArray]; + } + + //set autoincrement fields to NULL + queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM %@", [selectedTable backtickQuotedString]]]; + [queryResult setReturnDataAsStrings:YES]; + if ([queryResult numOfRows]) [queryResult dataSeek:0]; + for ( i = 0 ; i < [queryResult numOfRows] ; i++ ) { + row = [queryResult fetchRowAsDictionary]; + if ( [[row objectForKey:@"Extra"] isEqualToString:@"auto_increment"] ) { + [tempRow replaceObjectAtIndex:i withObject:[NSNull null]]; + } else if ( [tableDataInstance columnIsBlobOrText:[row objectForKey:@"Field"]] && [prefs boolForKey:SPLoadBlobsAsNeeded] && dbDataRow) { + [tempRow replaceObjectAtIndex:i withObject:[dbDataRow objectAtIndex:i]]; + } + } + + //insert the copied row + [tableValues insertRowContents:tempRow atIndex:[tableContentView selectedRow]+1]; + tableRowsCount++; + + //select row and go in edit mode + [tableContentView reloadData]; + [tableContentView selectRowIndexes:[NSIndexSet indexSetWithIndex:[tableContentView selectedRow]+1] byExtendingSelection:NO]; + isEditingRow = YES; + isEditingNewRow = YES; + currentlyEditingRow = [tableContentView selectedRow]; + if ( [multipleLineEditingButton state] == NSOffState ) + [tableContentView editColumn:0 row:[tableContentView selectedRow] withEvent:nil select:YES]; +} + +/** + * Asks the user if they really want to delete the selected rows + */ +- (IBAction)removeRow:(id)sender +{ + // Check whether a save of the current row is required. + // if (![self saveRowOnDeselect]) + // return; + + // cancel editing (maybe this is not the ideal method -- see xcode docs for that method) + [[tableDocumentInstance parentWindow] endEditingFor:nil]; + + + if (![tableContentView numberOfSelectedRows]) + return; + + NSAlert *alert = [NSAlert alertWithMessageText:@"" + defaultButton:NSLocalizedString(@"Delete", @"delete button") + alternateButton:NSLocalizedString(@"Cancel", @"cancel button") + otherButton:nil + informativeTextWithFormat:@""]; + + [alert setAlertStyle:NSCriticalAlertStyle]; + + NSArray *buttons = [alert buttons]; + + // Change the alert's cancel button to have the key equivalent of return + [[buttons objectAtIndex:0] setKeyEquivalent:@"d"]; + [[buttons objectAtIndex:0] setKeyEquivalentModifierMask:NSCommandKeyMask]; + [[buttons objectAtIndex:1] setKeyEquivalent:@"\r"]; + + [alert setShowsSuppressionButton:NO]; + [[alert suppressionButton] setState:NSOffState]; + + NSString *contextInfo = @"removerow"; + + if (([tableContentView numberOfSelectedRows] == [tableContentView numberOfRows]) && !isFiltered && !isLimited && !isInterruptedLoad && !isEditingNewRow) { + + contextInfo = @"removeallrows"; + + // If table has PRIMARY KEY ask for resetting the auto increment after deletion if given + if(![[tableDataInstance statusValueForKey:@"Auto_increment"] isKindOfClass:[NSNull class]]) { + [alert setShowsSuppressionButton:YES]; + [[alert suppressionButton] setState:NSOnState]; + [[[alert suppressionButton] cell] setControlSize:NSSmallControlSize]; + [[[alert suppressionButton] cell] setFont:[NSFont systemFontOfSize:11]]; + [[alert suppressionButton] setTitle:NSLocalizedString(@"Reset AUTO_INCREMENT after deletion?", @"reset auto_increment after deletion of all rows message")]; + } + + [alert setMessageText:NSLocalizedString(@"Delete all rows?", @"delete all rows message")]; + [alert setInformativeText:NSLocalizedString(@"Are you sure you want to delete all the rows from this table? This action cannot be undone.", @"delete all rows informative message")]; + } + else if ([tableContentView numberOfSelectedRows] == 1) { + [alert setMessageText:NSLocalizedString(@"Delete selected row?", @"delete selected row message")]; + [alert setInformativeText:NSLocalizedString(@"Are you sure you want to delete the selected row from this table? This action cannot be undone.", @"delete selected row informative message")]; + } + else { + [alert setMessageText:NSLocalizedString(@"Delete rows?", @"delete rows message")]; + [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to delete the selected %ld rows from this table? This action cannot be undone.", @"delete rows informative message"), (long)[tableContentView numberOfSelectedRows]]]; + } + + [alert beginSheetModalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:@selector(removeRowSheetDidEnd:returnCode:contextInfo:) contextInfo:contextInfo]; +} + +/** + * Perform the requested row deletion action. + */ +- (void)removeRowSheetDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo +{ + + NSMutableIndexSet *selectedRows = [NSMutableIndexSet indexSet]; + NSString *wherePart; + NSInteger i, errors; + BOOL consoleUpdateStatus; + BOOL reloadAfterRemovingRow = [prefs boolForKey:SPReloadAfterRemovingRow]; + + // Order out current sheet to suppress overlapping of sheets + [[alert window] orderOut:nil]; + + if ( [contextInfo isEqualToString:@"removeallrows"] ) { + if ( returnCode == NSAlertDefaultReturn ) { + //check if the user is currently editing a row + if (isEditingRow) { + //cancel the edit + isEditingRow = NO; + // in case the delete fails, make sure we at least stay in a somewhat consistent state + [tableValues replaceRowAtIndex:currentlyEditingRow withRowContents:oldRow]; + currentlyEditingRow = -1; + } + + [mySQLConnection queryString:[NSString stringWithFormat:@"DELETE FROM %@", [selectedTable backtickQuotedString]]]; + if ( ![mySQLConnection queryErrored] ) { + + // Reset auto increment if suppression button was ticked + if([[alert suppressionButton] state] == NSOnState) + [tableSourceInstance setAutoIncrementTo:@"1"]; + + [self reloadTable:self]; + + } else { + [self performSelector:@selector(showErrorSheetWith:) + withObject:[NSArray arrayWithObjects:NSLocalizedString(@"Error", @"error"), + [NSString stringWithFormat:NSLocalizedString(@"Couldn't delete rows.\n\nMySQL said: %@", @"message when deleteing all rows failed"), + [mySQLConnection getLastErrorMessage]], + nil] + afterDelay:0.3]; + } + } + } else if ( [contextInfo isEqualToString:@"removerow"] ) { + if ( returnCode == NSAlertDefaultReturn ) { + [selectedRows addIndexes:[tableContentView selectedRowIndexes]]; + + //check if the user is currently editing a row + if (isEditingRow) { + //make sure that only one row is selected. This should never happen + if ([selectedRows count]!=1) { + NSLog(@"Expected only one selected row, but found %d",[selectedRows count]); + } + // this code is pretty much taken from the escape key handler + if ( isEditingNewRow ) { + // since the user is currently editing a new row, we don't actually have to delete any rows from the database + // we just have to remove the row from the view (and the store) + isEditingRow = NO; + isEditingNewRow = NO; + tableRowsCount--; + [tableValues removeRowAtIndex:currentlyEditingRow]; + currentlyEditingRow = -1; + [self updateCountText]; + [tableContentView reloadData]; + + //deselect the row + [tableContentView selectRowIndexes:[NSIndexSet indexSet] byExtendingSelection:NO]; + + // we also don't have to reload the table, since no query went to the database + return; + } else { + //cancel the edit + isEditingRow = NO; + // in case the delete fails, make sure we at least stay in a somewhat consistent state + [tableValues replaceRowAtIndex:currentlyEditingRow withRowContents:oldRow]; + currentlyEditingRow = -1; + } + + } + [tableContentView selectRowIndexes:[NSIndexSet indexSet] byExtendingSelection:NO]; + + errors = 0; + + // Disable updating of the Console Log window for large number of queries + // to speed the deletion + consoleUpdateStatus = [[SPQueryController sharedQueryController] allowConsoleUpdate]; + if([selectedRows count] > 10) + [[SPQueryController sharedQueryController] setAllowConsoleUpdate:NO]; + + NSUInteger index = [selectedRows firstIndex]; + + NSArray *primaryKeyFieldNames = [tableDataInstance primaryKeyColumnNames]; + + // If no PRIMARY KEY is found and numberOfSelectedRows > 3 then + // check for uniqueness of rows via combining all column values; + // if unique then use the all columns as 'primary keys' + if([selectedRows count] >3 && primaryKeyFieldNames == nil) { + primaryKeyFieldNames = [tableDataInstance columnNames]; + + NSInteger numberOfRows = 0; + // Get the number of rows in the table + MCPResult *r; + r = [mySQLConnection queryString:[NSString stringWithFormat:@"SELECT COUNT(1) FROM %@", [selectedTable backtickQuotedString]]]; + if (![mySQLConnection queryErrored]) { + NSArray *a = [r fetchRowAsArray]; + if([a count]) + numberOfRows = [[a objectAtIndex:0] integerValue]; + } + // Check for uniqueness via LIMIT numberOfRows-1,numberOfRows for speed + if(numberOfRows > 0) { + [mySQLConnection queryString:[NSString stringWithFormat:@"SELECT * FROM %@ GROUP BY %@ LIMIT %ld,%ld", [selectedTable backtickQuotedString], [primaryKeyFieldNames componentsJoinedAndBacktickQuoted], (long)(numberOfRows-1), (long)numberOfRows]]; + if([mySQLConnection affectedRows] == 0) + primaryKeyFieldNames = nil; + } else { + primaryKeyFieldNames = nil; + } + } + + if(primaryKeyFieldNames == nil) { + // delete row by row + while (index != NSNotFound) { + + wherePart = [NSString stringWithString:[self argumentForRow:index]]; + + //argumentForRow might return empty query, in which case we shouldn't execute the partial query + if([wherePart length]) { + [mySQLConnection queryString:[NSString stringWithFormat:@"DELETE FROM %@ WHERE %@", [selectedTable backtickQuotedString], wherePart]]; + + // Check for errors + if ( ![mySQLConnection affectedRows] || [mySQLConnection queryErrored]) { + // If error delete that index from selectedRows for reloading table if + // "ReloadAfterRemovingRow" is disbaled + if(!reloadAfterRemovingRow) + [selectedRows removeIndex:index]; + errors++; + } + } else { + if(!reloadAfterRemovingRow) + [selectedRows removeIndex:index]; + errors++; + } + index = [selectedRows indexGreaterThanIndex:index]; + } + } else if ([primaryKeyFieldNames count] == 1) { + // if table has only one PRIMARY KEY + // delete the fast way by using the PRIMARY KEY in an IN clause + NSMutableString *deleteQuery = [NSMutableString string]; + NSInteger affectedRows = 0; + + [deleteQuery setString:[NSString stringWithFormat:@"DELETE FROM %@ WHERE %@ IN (", [selectedTable backtickQuotedString], [NSArrayObjectAtIndex(primaryKeyFieldNames,0) backtickQuotedString]]]; + + while (index != NSNotFound) { + + id keyValue = [tableValues cellDataAtRow:index column:[[[tableDataInstance columnWithName:NSArrayObjectAtIndex(primaryKeyFieldNames,0)] objectForKey:@"datacolumnindex"] integerValue]]; + + if([keyValue isKindOfClass:[NSData class]]) + [deleteQuery appendString:[NSString stringWithFormat:@"X'%@'", [mySQLConnection prepareBinaryData:keyValue]]]; + else + [deleteQuery appendString:[NSString stringWithFormat:@"'%@'", [keyValue description]]]; + + // Split deletion query into 256k chunks + if([deleteQuery length] > 256000) { + [deleteQuery appendString:@")"]; + [mySQLConnection queryString:deleteQuery]; + // Remember affected rows for error checking + affectedRows += [mySQLConnection affectedRows]; + // Reinit a new deletion query + [deleteQuery setString:[NSString stringWithFormat:@"DELETE FROM %@ WHERE %@ IN (", [selectedTable backtickQuotedString], [NSArrayObjectAtIndex(primaryKeyFieldNames,0) backtickQuotedString]]]; + } else { + [deleteQuery appendString:@","]; + } + + index = [selectedRows indexGreaterThanIndex:index]; + } + + // Check if deleteQuery's maximal length was reached for the last index + // if yes omit the empty query + if(![deleteQuery hasSuffix:@"("]) { + // Replace final , by ) and delete the remaining rows + [deleteQuery setString:[NSString stringWithFormat:@"%@)", [deleteQuery substringToIndex:([deleteQuery length]-1)]]]; + [mySQLConnection queryString:deleteQuery]; + // Remember affected rows for error checking + affectedRows += [mySQLConnection affectedRows]; + } + + errors = (affectedRows > 0) ? [selectedRows count] - affectedRows : [selectedRows count]; + } else { + // if table has more than one PRIMARY KEY + // delete the row by using all PRIMARY KEYs in an OR clause + NSMutableString *deleteQuery = [NSMutableString string]; + NSInteger affectedRows = 0; + + [deleteQuery setString:[NSString stringWithFormat:@"DELETE FROM %@ WHERE ", [selectedTable backtickQuotedString]]]; + + while (index != NSNotFound) { + + // Build the AND clause of PRIMARY KEYS + [deleteQuery appendString:@"("]; + for(NSString *primaryKeyFieldName in primaryKeyFieldNames) { + + id keyValue = [tableValues cellDataAtRow:index column:[[[tableDataInstance columnWithName:primaryKeyFieldName] objectForKey:@"datacolumnindex"] integerValue]]; + + [deleteQuery appendString:[primaryKeyFieldName backtickQuotedString]]; + if ([keyValue isKindOfClass:[NSData class]]) { + [deleteQuery appendString:@"=X'"]; + [deleteQuery appendString:[mySQLConnection prepareBinaryData:keyValue]]; + } else { + [deleteQuery appendString:@"='"]; + [deleteQuery appendString:[mySQLConnection prepareString:[keyValue description]]]; + } + [deleteQuery appendString:@"' AND "]; + } + + // Remove the trailing AND and add the closing bracket + [deleteQuery deleteCharactersInRange:NSMakeRange([deleteQuery length]-5, 5)]; + [deleteQuery appendString:@")"]; + + // Split deletion query into 64k chunks + if([deleteQuery length] > 64000) { + [mySQLConnection queryString:deleteQuery]; + // Remember affected rows for error checking + affectedRows += [mySQLConnection affectedRows]; + // Reinit a new deletion query + [deleteQuery setString:[NSString stringWithFormat:@"DELETE FROM %@ WHERE ", [selectedTable backtickQuotedString]]]; + } else { + [deleteQuery appendString:@" OR "]; + } + + index = [selectedRows indexGreaterThanIndex:index]; + } + + // Check if deleteQuery's maximal length was reached for the last index + // if yes omit the empty query + if(![deleteQuery hasSuffix:@"WHERE "]) { + // Remove final ' OR ' and delete the remaining rows + [deleteQuery setString:[deleteQuery substringToIndex:([deleteQuery length]-4)]]; + [mySQLConnection queryString:deleteQuery]; + // Remember affected rows for error checking + affectedRows += [mySQLConnection affectedRows]; + } + + errors = (affectedRows > 0) ? [selectedRows count] - affectedRows : [selectedRows count]; + } + + // Restore Console Log window's updating bahaviour + [[SPQueryController sharedQueryController] setAllowConsoleUpdate:consoleUpdateStatus]; + + if (errors) { + NSArray *message; + //TODO: The following three messages are NOT localisable! + if (errors < 0) { + message = [NSArray arrayWithObjects:NSLocalizedString(@"Warning", @"warning"), + [NSString stringWithFormat:NSLocalizedString(@"%ld row%@ more %@ deleted! Please check the Console and inform the Sequel Pro team!", @"message of panel when more rows were deleted"), (long)(errors*-1), ((errors*-1)>1)?@"s":@"", (errors>1)?@"were":@"was"], + nil]; + } + else { + if (primaryKeyFieldNames == nil) + message = [NSArray arrayWithObjects:NSLocalizedString(@"Warning", @"warning"), + [NSString stringWithFormat:NSLocalizedString(@"%ld row%@ ha%@ not been deleted. Reload the table to be sure that the rows exist and use a primary key for your table.", @"message of panel when not all selected fields have been deleted"), (long)errors, (errors>1)?@"s":@"", (errors>1)?@"ve":@"s"], + nil]; + else + message = [NSArray arrayWithObjects:NSLocalizedString(@"Warning", @"warning"), + [NSString stringWithFormat:NSLocalizedString(@"%ld row%@ ha%@ not been deleted. Reload the table to be sure that the rows exist and check the Console for possible errors inside the primary key%@ for your table.", @"message of panel when not all selected fields have been deleted by using primary keys"), (long)errors, (errors>1)?@"s":@"", (errors>1)?@"ve":@"s", (errors>1)?@"s":@""], + nil]; + } + + [self performSelector:@selector(showErrorSheetWith:) + withObject:message + afterDelay:0.3]; + } + + // Refresh table content + if ( errors || reloadAfterRemovingRow ) { + previousTableRowsCount = tableRowsCount; + [self loadTableValues]; + } else { + for ( i = tableRowsCount - 1 ; i >= 0 ; i-- ) { + if ([selectedRows containsIndex:i]) [tableValues removeRowAtIndex:i]; + } + tableRowsCount = [tableValues count]; + [tableContentView reloadData]; + } + [tableContentView deselectAll:self]; + } else { + // The user clicked cancel in the "sure you wanna delete" message + // restore editing or whatever + } + + } +} + + +// Accessors + +/** + * Returns the current result (as shown in table content view) as array, the first object containing the field + * names as array, the following objects containing the rows as array. + */ +- (NSArray *)currentDataResult +{ + NSArray *tableColumns; + NSEnumerator *enumerator; + id tableColumn; + NSMutableArray *currentResult = [NSMutableArray array]; + NSMutableArray *tempRow = [NSMutableArray array]; + NSUInteger i; + + //load table if not already done + if ( ![tablesListInstance contentLoaded] ) { + [self loadTable:[tablesListInstance tableName]]; + } + + tableColumns = [tableContentView tableColumns]; + enumerator = [tableColumns objectEnumerator]; + + //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:nil] ; i++) { + [tempRow removeAllObjects]; + enumerator = [tableColumns objectEnumerator]; + while ( (tableColumn = [enumerator nextObject]) ) { + id o = SPDataStorageObjectAtRowAndColumn(tableValues, i, [[tableColumn identifier] integerValue]); + if([o isNSNull]) + [tempRow addObject:@"NULL"]; + else if ([o isSPNotLoaded]) + [tempRow addObject:NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields")]; + else if([o isKindOfClass:[NSString class]]) + [tempRow addObject:[o description]]; + else { + NSImage *image = [[NSImage alloc] initWithData:o]; + if(image) { + NSInteger imageWidth = [image size].width; + if (imageWidth > 100) imageWidth = 100; + [tempRow addObject:[NSString stringWithFormat: + @"<IMG WIDTH='%ld' SRC=\"data:image/auto;base64,%@\">", + (long)imageWidth, + [[image TIFFRepresentationUsingCompression:NSTIFFCompressionJPEG factor:0.01] base64EncodingWithLineLength:0]]]; + } else { + [tempRow addObject:@"<BLOB>"]; + } + if(image) [image release]; + } + } + [currentResult addObject:[NSArray arrayWithArray:tempRow]]; + } + return currentResult; +} + +/** + * Returns the current result (as shown in table content view) as array, the first object containing the field + * names as array, the following objects containing the rows as array. + */ +- (NSArray *)currentResult +{ + NSArray *tableColumns; + NSEnumerator *enumerator; + id tableColumn; + NSMutableArray *currentResult = [NSMutableArray array]; + NSMutableArray *tempRow = [NSMutableArray array]; + NSUInteger i; + + //load table if not already done + if ( ![tablesListInstance contentLoaded] ) { + [self loadTable:[tablesListInstance tableName]]; + } + + tableColumns = [tableContentView tableColumns]; + enumerator = [tableColumns objectEnumerator]; + + //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:nil] ; i++) { + [tempRow removeAllObjects]; + enumerator = [tableColumns objectEnumerator]; + while ( (tableColumn = [enumerator nextObject]) ) { + [tempRow addObject:[self tableView:nil objectValueForTableColumn:tableColumn row:i]]; + } + [currentResult addObject:[NSArray arrayWithArray:tempRow]]; + } + return currentResult; +} + +// Additional methods + +/** + * Sets the connection (received from TableDocument) and makes things that have to be done only once + */ +- (void)setConnection:(MCPConnection *)theConnection +{ + mySQLConnection = theConnection; + + [tableContentView setVerticalMotionCanBeginDrag:NO]; +} + +/** + * Performs the requested action - switching to another table + * with the appropriate filter settings - when a link arrow is + * selected. + */ +- (void)clickLinkArrow:(SPTextAndLinkCell *)theArrowCell +{ + if ([tableDocumentInstance isWorking]) return; + + if ([theArrowCell getClickedColumn] == NSNotFound || [theArrowCell getClickedRow] == NSNotFound) return; + + // Check whether a save of the current row is required. + if ( ![self saveRowOnDeselect] ) return; + + // If on the main thread, fire up a thread to perform the load while keeping the modification flag + [tableDocumentInstance startTaskWithDescription:NSLocalizedString(@"Loading reference...", @"Loading referece task string")]; + if ([NSThread isMainThread]) { + [NSThread detachNewThreadSelector:@selector(clickLinkArrowTask:) toTarget:self withObject:theArrowCell]; + } else { + [self clickLinkArrowTask:theArrowCell]; + } +} +- (void)clickLinkArrowTask:(SPTextAndLinkCell *)theArrowCell +{ + NSAutoreleasePool *linkPool = [[NSAutoreleasePool alloc] init]; + NSUInteger dataColumnIndex = [[[[tableContentView tableColumns] objectAtIndex:[theArrowCell getClickedColumn]] identifier] integerValue]; + BOOL tableFilterRequired = NO; + + // Ensure the clicked cell has foreign key details available + NSDictionary *refDictionary = [[dataColumns objectAtIndex:dataColumnIndex] objectForKey:@"foreignkeyreference"]; + if (!refDictionary) { + [linkPool release]; + return; + } + + // Save existing scroll position and details and mark that state is being modified + [spHistoryControllerInstance updateHistoryEntries]; + [spHistoryControllerInstance setModifyingState:YES]; + + NSString *targetFilterValue = [tableValues cellDataAtRow:[theArrowCell getClickedRow] column:dataColumnIndex]; + + // If the link is within the current table, apply filter settings manually + if ([[refDictionary objectForKey:@"table"] isEqualToString:selectedTable]) { + [fieldField selectItemWithTitle:[refDictionary objectForKey:@"column"]]; + [self setCompareTypes:self]; + if ([targetFilterValue isNSNull]) { + [compareField selectItemWithTitle:@"IS NULL"]; + } else { + [argumentField setStringValue:targetFilterValue]; + } + tableFilterRequired = YES; + } else { + + // Store the filter details to use when loading the target table + NSDictionary *filterSettings = [NSDictionary dictionaryWithObjectsAndKeys: + [refDictionary objectForKey:@"column"], @"filterField", + targetFilterValue, @"filterValue", + ([targetFilterValue isNSNull]?@"IS NULL":nil), @"filterComparison", + nil]; + [self setFiltersToRestore:filterSettings]; + + // Attempt to switch to the target table + if (![tablesListInstance selectItemWithName:[refDictionary objectForKey:@"table"]]) { + NSBeep(); + [self setFiltersToRestore:nil]; + } + } + + // End state and ensure a new history entry + [spHistoryControllerInstance setModifyingState:NO]; + [spHistoryControllerInstance updateHistoryEntries]; + + // End the task + [tableDocumentInstance endTask]; + + // If the same table is the target, trigger a filter task on the main thread + if (tableFilterRequired) + [self performSelectorOnMainThread:@selector(filterTable:) withObject:self waitUntilDone:NO]; + + // Empty the loading pool and exit the thread + [linkPool drain]; +} + +/** + * Sets the compare types for the filter and the appropriate formatter for the textField + */ +- (IBAction)setCompareTypes:(id)sender +{ + + if(contentFilters == nil + || ![contentFilters objectForKey:@"number"] + || ![contentFilters objectForKey:@"string"] + || ![contentFilters objectForKey:@"date"]) { + NSLog(@"Error while setting filter types."); + NSBeep(); + return; + } + + + [compareField removeAllItems]; + + NSString *fieldTypeGrouping; + if([[tableDataInstance columnWithName:[[fieldField selectedItem] title]] objectForKey:@"typegrouping"]) + fieldTypeGrouping = [NSString stringWithString:[[tableDataInstance columnWithName:[[fieldField selectedItem] title]] objectForKey:@"typegrouping"]]; + else + return; + + if ( [fieldTypeGrouping isEqualToString:@"date"] ) { + compareType = @"date"; + + /* + if ([fieldType isEqualToString:@"timestamp"]) { + [argumentField setFormatter:[[NSDateFormatter alloc] + initWithDateFormat:@"%Y-%m-%d %H:%M:%S" allowNaturalLanguage:YES]]; + } + if ([fieldType isEqualToString:@"datetime"]) { + [argumentField setFormatter:[[NSDateFormatter alloc] initWithDateFormat:@"%Y-%m-%d %H:%M:%S" allowNaturalLanguage:YES]]; + } + if ([fieldType isEqualToString:@"date"]) { + [argumentField setFormatter:[[NSDateFormatter alloc] initWithDateFormat:@"%Y-%m-%d" allowNaturalLanguage:YES]]; + } + if ([fieldType isEqualToString:@"time"]) { + [argumentField setFormatter:[[NSDateFormatter alloc] initWithDateFormat:@"%H:%M:%S" allowNaturalLanguage:YES]]; + } + if ([fieldType isEqualToString:@"year"]) { + [argumentField setFormatter:[[NSDateFormatter alloc] initWithDateFormat:@"%Y" allowNaturalLanguage:YES]]; + } + */ + + // TODO: A bug in the framework previously meant enum fields had to be treated as string fields for the purposes + // of comparison - this can now be split out to support additional comparison fucntionality if desired. + } else if ([fieldTypeGrouping isEqualToString:@"string"] || [fieldTypeGrouping isEqualToString:@"binary"] + || [fieldTypeGrouping isEqualToString:@"textdata"] || [fieldTypeGrouping isEqualToString:@"blobdata"] + || [fieldTypeGrouping isEqualToString:@"enum"]) { + + compareType = @"string"; + // [argumentField setFormatter:nil]; + + } else if ([fieldTypeGrouping isEqualToString:@"bit"] || [fieldTypeGrouping isEqualToString:@"integer"] + || [fieldTypeGrouping isEqualToString:@"float"]) { + compareType = @"number"; + // [argumentField setFormatter:numberFormatter]; + + } else { + compareType = @""; + NSBeep(); + NSLog(@"ERROR: unknown type for comparision: %@, in %@", [[tableDataInstance columnWithName:[[fieldField selectedItem] title]] objectForKey:@"type"], fieldTypeGrouping); + } + + // Add IS NULL and IS NOT NULL as they should always be available + // [compareField addItemWithTitle:@"IS NULL"]; + // [compareField addItemWithTitle:@"IS NOT NULL"]; + + // Remove user-defined filters first + if([numberOfDefaultFilters objectForKey:compareType]) { + NSUInteger cycles = [[contentFilters objectForKey:compareType] count] - [[numberOfDefaultFilters objectForKey:compareType] integerValue]; + while(cycles > 0) { + [[contentFilters objectForKey:compareType] removeLastObject]; + cycles--; + } + } + + // Load global user-defined content filters + if([prefs objectForKey:SPContentFilters] + && [contentFilters objectForKey:compareType] + && [[prefs objectForKey:SPContentFilters] objectForKey:compareType]) + { + [[contentFilters objectForKey:compareType] addObjectsFromArray:[[prefs objectForKey:SPContentFilters] objectForKey:compareType]]; + } + + // Load doc-based user-defined content filters + if([[SPQueryController sharedQueryController] contentFilterForFileURL:[tableDocumentInstance fileURL]]) { + id filters = [[SPQueryController sharedQueryController] contentFilterForFileURL:[tableDocumentInstance fileURL]]; + if([filters objectForKey:compareType]) + [[contentFilters objectForKey:compareType] addObjectsFromArray:[filters objectForKey:compareType]]; + } + + // Rebuild operator popup menu + NSUInteger i = 0; + NSMenu *menu = [compareField menu]; + if([contentFilters objectForKey:compareType]) + for(id filter in [contentFilters objectForKey:compareType]) { + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:([filter objectForKey:@"MenuLabel"])?[filter objectForKey:@"MenuLabel"]:@"not specified" action:NULL keyEquivalent:@""]; + // Create the tooltip + if([filter objectForKey:@"Tooltip"]) + [item setToolTip:[filter objectForKey:@"Tooltip"]]; + else { + NSMutableString *tip = [[NSMutableString alloc] init]; + [tip setString:[[filter objectForKey:@"Clause"] stringByReplacingOccurrencesOfRegex:@"(?<!\\\\)(\\$\\{.*?\\})" withString:@"[arg]"]]; + if([tip isMatchedByRegex:@"(?<!\\\\)\\$BINARY"]) { + [tip replaceOccurrencesOfRegex:@"(?<!\\\\)\\$BINARY" withString:@""]; + [tip appendString:NSLocalizedString(@"\n\nPress ⇧ for binary search (case-sensitive).", @"\n\npress shift for binary search tooltip message")]; + } + [tip flushCachedRegexData]; + [tip replaceOccurrencesOfRegex:@"(?<!\\\\)\\$CURRENT_FIELD" withString:[[fieldField titleOfSelectedItem] backtickQuotedString]]; + [tip flushCachedRegexData]; + [item setToolTip:tip]; + [tip release]; + } + [item setTag:i]; + [menu addItem:item]; + [item release]; + i++; + } + + [menu addItem:[NSMenuItem separatorItem]]; + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Edit Filters…", @"edit filter") action:NULL keyEquivalent:@""]; + [item setToolTip:NSLocalizedString(@"Edit user-defined Filters…", @"edit user-defined filter")]; + [item setTag:i]; + [menu addItem:item]; + [item release]; + + // Update the argumentField enabled state + [self performSelectorOnMainThread:@selector(toggleFilterField:) withObject:self waitUntilDone:YES]; + + // set focus on argumentField + [argumentField performSelectorOnMainThread:@selector(selectText:) withObject:self waitUntilDone:YES]; + +} + +- (void)openContentFilterManager +{ + [compareField selectItemWithTag:lastSelectedContentFilterIndex]; + + // init query favorites controller + [prefs synchronize]; + if(contentFilterManager) [contentFilterManager release]; + contentFilterManager = [[SPContentFilterManager alloc] initWithDelegate:self forFilterType:compareType]; + + // Open query favorite manager + [NSApp beginSheet:[contentFilterManager window] + modalForWindow:[tableDocumentInstance parentWindow] + modalDelegate:contentFilterManager + didEndSelector:nil + contextInfo:nil]; +} + +/* + * Tries to write a new row to the database. + * Returns YES if row is written to database, otherwise NO; also returns YES if no row + * is being edited and nothing has to be written to the database. + */ +- (BOOL)addRowToDB +{ + NSMutableString *queryString; + id rowObject; + NSMutableString *rowValue = [NSMutableString string]; + NSString *currentTime = [[NSDate date] descriptionWithCalendarFormat:@"%H:%M:%S" timeZone:nil locale:nil]; + BOOL prefsLoadBlobsAsNeeded = [prefs boolForKey:SPLoadBlobsAsNeeded]; + NSInteger i; + + if ( !isEditingRow || currentlyEditingRow == -1) { + return YES; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:tableDocumentInstance]; + + // If editing, compare the new row to the old row and if they are identical finish editing without saving. + if (!isEditingNewRow && [oldRow isEqualToArray:[tableValues rowContentsAtIndex:currentlyEditingRow]]) { + isEditingRow = NO; + currentlyEditingRow = -1; + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:tableDocumentInstance]; + return YES; + } + + NSMutableArray *fieldValues = [[NSMutableArray alloc] init]; + + // Get the field values + for ( i = 0 ; i < [dataColumns count] ; i++ ) { + rowObject = [tableValues cellDataAtRow:currentlyEditingRow column:i]; + + // Add not loaded placeholders directly for easy comparison when added + if (prefsLoadBlobsAsNeeded && !isEditingNewRow && [rowObject isSPNotLoaded]) + { + [fieldValues addObject:[SPNotLoaded notLoaded]]; + continue; + + // Catch CURRENT_TIMESTAMP automatic updates - if the row is new and the cell value matches + // the default value, or if the cell hasn't changed, update the current timestamp. + } else if ([[NSArrayObjectAtIndex(dataColumns, i) objectForKey:@"onupdatetimestamp"] integerValue] + && ( (isEditingNewRow && [rowObject isEqualTo:[NSArrayObjectAtIndex(dataColumns, i) objectForKey:@"default"]]) + || (!isEditingNewRow && [rowObject isEqualTo:NSArrayObjectAtIndex(oldRow, i)]))) + { + [rowValue setString:@"CURRENT_TIMESTAMP"]; + + // Convert the object to a string (here we can add special treatment for date-, number- and data-fields) + } else if ( [rowObject isNSNull] + || ([rowObject isMemberOfClass:[NSString class]] && [[rowObject description] isEqualToString:@""]) ) { + + //NULL when user entered the nullValue string defined in the prefs or when a number field isn't set + // problem: when a number isn't set, sequel-pro enters 0 + // -> second if argument isn't necessary! + [rowValue setString:@"NULL"]; + } else { + + // I don't believe any of these class matches are ever met at present. + if ( [rowObject isKindOfClass:[NSCalendarDate class]] ) { + [rowValue setString:[NSString stringWithFormat:@"'%@'", [mySQLConnection prepareString:[rowObject description]]]]; + } else if ( [rowObject isKindOfClass:[NSNumber class]] ) { + [rowValue setString:[rowObject stringValue]]; + } else if ( [rowObject isKindOfClass:[NSData class]] ) { + [rowValue setString:[NSString stringWithFormat:@"X'%@'", [mySQLConnection prepareBinaryData:rowObject]]]; + } else { + if ([[rowObject description] isEqualToString:@"CURRENT_TIMESTAMP"]) { + [rowValue setString:@"CURRENT_TIMESTAMP"]; + } else if ([[NSArrayObjectAtIndex(dataColumns, i) objectForKey:@"typegrouping"] isEqualToString:@"bit"]) { + [rowValue setString:((![[rowObject description] length] || [[rowObject description] isEqualToString:@"0"])?@"0":@"1")]; + } else if ([[NSArrayObjectAtIndex(dataColumns, i) objectForKey:@"typegrouping"] isEqualToString:@"date"] + && [[rowObject description] isEqualToString:@"NOW()"]) { + [rowValue setString:@"NOW()"]; + } else { + [rowValue setString:[NSString stringWithFormat:@"'%@'", [mySQLConnection prepareString:[rowObject description]]]]; + } + } + } + [fieldValues addObject:[NSString stringWithString:rowValue]]; + } + + // Use INSERT syntax when creating new rows - no need to do not loaded checking, as all values have been entered + if ( isEditingNewRow ) { + queryString = [NSString stringWithFormat:@"INSERT INTO %@ (%@) VALUES (%@)", + [selectedTable backtickQuotedString], [[tableDataInstance columnNames] componentsJoinedAndBacktickQuoted], [fieldValues componentsJoinedByString:@","]]; + + // Use UPDATE syntax otherwise + } else { + BOOL firstCellOutput = NO; + queryString = [NSMutableString stringWithFormat:@"UPDATE %@ SET ", [selectedTable backtickQuotedString]]; + for ( i = 0 ; i < [dataColumns count] ; i++ ) { + + // If data column loading is deferred and the value is the not loaded string, skip this cell + if (prefsLoadBlobsAsNeeded && [[fieldValues objectAtIndex:i] isSPNotLoaded]) continue; + + if (firstCellOutput) [queryString appendString:@", "]; + else firstCellOutput = YES; + + [queryString appendString:[NSString stringWithFormat:@"%@=%@", + [[NSArrayObjectAtIndex(dataColumns, i) objectForKey:@"name"] backtickQuotedString], [fieldValues objectAtIndex:i]]]; + } + [queryString appendString:[NSString stringWithFormat:@" WHERE %@", [self argumentForRow:-2]]]; + } + + // If UTF-8 via Latin1 view encoding is set convert the queryString into Latin1 and + // set the MySQL connection to Latin1 before executing this query to allow editing. + // After executing reset all. + if([tableDocumentInstance connectionEncodingViaLatin1:mySQLConnection]) { + + NSStringEncoding currentEncoding = [mySQLConnection encoding]; + NSString *latin1String = [[NSString alloc] initWithCString:[queryString UTF8String] encoding:NSISOLatin1StringEncoding]; + [mySQLConnection setEncoding:NSISOLatin1StringEncoding]; + [mySQLConnection queryString:@"SET NAMES 'latin1'"]; + [mySQLConnection queryString:latin1String]; + [mySQLConnection setEncoding:currentEncoding]; + [mySQLConnection queryString:@"SET NAMES 'utf8'"]; + [mySQLConnection queryString:@"SET CHARACTER_SET_RESULTS=latin1"]; + [latin1String release]; + + } else { + [mySQLConnection queryString:queryString]; + } + + [fieldValues release]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:tableDocumentInstance]; + + // If no rows have been changed, show error if appropriate. + if ( ![mySQLConnection affectedRows] && ![mySQLConnection queryErrored] ) { + 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(); + } + [tableValues replaceRowAtIndex:currentlyEditingRow withRowContents:oldRow]; + isEditingRow = NO; + isEditingNewRow = NO; + currentlyEditingRow = -1; + [[SPQueryController sharedQueryController] showErrorInConsole:[NSString stringWithFormat:NSLocalizedString(@"/* WARNING %@ No rows have been affected */\n", @"warning shown in the console when no rows have been affected after writing to the db"), currentTime] connection:[tableDocumentInstance name]]; + return YES; + + // On success... + } else if ( ![mySQLConnection queryErrored] ) { + isEditingRow = NO; + + // New row created successfully + if ( isEditingNewRow ) { + if ( [prefs boolForKey:SPReloadAfterAddingRow] ) { + [[tableDocumentInstance parentWindow] endEditingFor:nil]; + previousTableRowsCount = tableRowsCount; + [self loadTableValues]; + } else { + + // Set the insertId for fields with auto_increment + for ( i = 0; i < [dataColumns count] ; i++ ) { + if ([[NSArrayObjectAtIndex(dataColumns, i) objectForKey:@"autoincrement"] integerValue]) { + [tableValues replaceObjectInRow:currentlyEditingRow column:i withObject:[[NSNumber numberWithLong:[mySQLConnection insertId]] description]]; + } + } + } + isEditingNewRow = NO; + + // Existing row edited successfully + } else { + + // Reload table if set to - otherwise no action required. + if ( [prefs boolForKey:SPReloadAfterEditingRow] ) { + [[tableDocumentInstance parentWindow] endEditingFor:nil]; + previousTableRowsCount = tableRowsCount; + [self loadTableValues]; + } + } + currentlyEditingRow = -1; + + return YES; + + // Report errors which have occurred + } else { + SPBeginAlertSheet(NSLocalizedString(@"Couldn't write row", @"Couldn't write row error"), NSLocalizedString(@"Edit row", @"Edit row button"), NSLocalizedString(@"Discard changes", @"discard changes button"), nil, [tableDocumentInstance parentWindow], self, @selector(addRowErrorSheetDidEnd:returnCode:contextInfo:), nil, + [NSString stringWithFormat:NSLocalizedString(@"MySQL said:\n\n%@", @"message of panel when error while adding row to db"), [mySQLConnection getLastErrorMessage]]); + return NO; + } +} + +/* + * Handle the user decision as a result of an addRow error. + */ +- (void) addRowErrorSheetDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo +{ + // Order out current sheet to suppress overlapping of sheets + [[alert window] orderOut:nil]; + + // Edit row selected - reselect the row, and start editing. + if ( returnCode == NSAlertDefaultReturn ) { + [tableContentView selectRowIndexes:[NSIndexSet indexSetWithIndex:currentlyEditingRow] byExtendingSelection:NO]; + [tableContentView performSelector:@selector(keyDown:) withObject:[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:0x24] afterDelay:0.0]; + + // Discard changes selected + } else { + if ( !isEditingNewRow ) { + [tableValues replaceRowAtIndex:currentlyEditingRow withRowContents:oldRow]; + isEditingRow = NO; + } else { + tableRowsCount--; + [tableValues removeRowAtIndex:currentlyEditingRow]; + isEditingRow = NO; + isEditingNewRow = NO; + } + currentlyEditingRow = -1; + } + [tableContentView reloadData]; +} + +/* + * A method to be called whenever the table selection changes; checks whether the current + * row is being edited, and if so attempts to save it. Returns YES if no save was necessary + * or the save was successful, and NO if a save was necessary and failed - in which case further + * editing is required. In that case this method will reselect the row in question for reediting. + */ +- (BOOL)saveRowOnDeselect +{ + // Save any edits which have been made but not saved to the table yet. + [[tableDocumentInstance parentWindow] endEditingFor:nil]; + + // If no rows are currently being edited, or a save is in progress, return success at once. + if (!isEditingRow || isSavingRow) return YES; + isSavingRow = YES; + + // Attempt to save the row, and return YES if the save succeeded. + if ([self addRowToDB]) { + isSavingRow = NO; + return YES; + } + + // Saving failed - return failure. + isSavingRow = NO; + return NO; +} + +/* + * Returns the WHERE argument to identify a row. + * If "row" is -2, it uses the oldRow. + * Uses the primary key if available, otherwise uses all fields as argument and sets LIMIT to 1 + */ +- (NSString *)argumentForRow:(NSInteger)row +{ + MCPResult *theResult; + NSDictionary *theRow; + id tempValue; + NSMutableString *value = [NSMutableString string]; + NSMutableString *argument = [NSMutableString string]; + // NSString *columnType; + NSArray *columnNames; + NSInteger i; + + if ( row == -1 ) + return @""; + + // Retrieve the field names for this table from the data cache. This is used when requesting all data as part + // of the fieldListForQuery method, and also to decide whether or not to preserve the current filter/sort settings. + columnNames = [tableDataInstance columnNames]; + + // Get the primary key if there is one + if ( !keys ) { + setLimit = NO; + keys = [[NSMutableArray alloc] init]; + theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM %@", [selectedTable backtickQuotedString]]]; + [theResult setReturnDataAsStrings:YES]; + if ([theResult numOfRows]) [theResult dataSeek:0]; + for ( i = 0 ; i < [theResult numOfRows] ; i++ ) { + theRow = [theResult fetchRowAsDictionary]; + if ( [[theRow objectForKey:@"Key"] isEqualToString:@"PRI"] ) { + [keys addObject:[theRow objectForKey:@"Field"]]; + } + } + } + + // If there is no primary key, all the fields are used in the argument. + if ( ![keys count] ) { + [keys setArray:columnNames]; + setLimit = YES; + + // When the option to not show blob or text options is set, we have a problem - we don't have + // the right values to use in the WHERE statement. Throw an error if this is the case. + if ( [prefs boolForKey:SPLoadBlobsAsNeeded] && [self tableContainsBlobOrTextColumns] ) { + SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocumentInstance parentWindow], self, nil, nil, + NSLocalizedString(@"You can't hide blob and text fields when working with tables without index.", @"message of panel when trying to edit tables without index and with hidden blob/text fields")); + [keys removeAllObjects]; + [tableContentView deselectAll:self]; + return @""; + } + } + + // Walk through the keys list constructing the argument list + for ( i = 0 ; i < [keys count] ; i++ ) { + if ( i ) + [argument appendString:@" AND "]; + + // Use the selected row if appropriate + if ( row >= 0 ) { + tempValue = [tableValues cellDataAtRow:row column:[[[tableDataInstance columnWithName:NSArrayObjectAtIndex(keys, i)] objectForKey:@"datacolumnindex"] integerValue]]; + + // Otherwise use the oldRow + } else { + tempValue = [oldRow objectAtIndex:[[[tableDataInstance columnWithName:NSArrayObjectAtIndex(keys, i)] objectForKey:@"datacolumnindex"] integerValue]]; + } + + if ( [tempValue isNSNull] ) { + [argument appendString:[NSString stringWithFormat:@"%@ IS NULL", [NSArrayObjectAtIndex(keys, i) backtickQuotedString]]]; + } else if ( [tempValue isSPNotLoaded] ) { + NSLog(@"Exceptional case: SPNotLoaded object found for method “argumentForRow:”!"); + return @""; + } else { + + if ( [tempValue isKindOfClass:[NSData class]] ) + [value setString:[NSString stringWithFormat:@"X'%@'", [mySQLConnection prepareBinaryData:tempValue]]]; + else + [value setString:[NSString stringWithFormat:@"'%@'", [mySQLConnection prepareString:tempValue]]]; + + [argument appendString:[NSString stringWithFormat:@"%@ = %@", [NSArrayObjectAtIndex(keys, i) backtickQuotedString], value]]; + } + } + + if (setLimit) [argument appendString:@" LIMIT 1"]; + + return argument; +} + + +/* + * Returns YES if the table contains any columns which are of any of the blob or text types, + * NO otherwise. + */ +- (BOOL)tableContainsBlobOrTextColumns +{ + NSInteger i; + + for ( i = 0 ; i < [dataColumns count]; i++ ) { + if ( [tableDataInstance columnIsBlobOrText:[NSArrayObjectAtIndex(dataColumns, i) objectForKey:@"name"]] ) { + return YES; + } + } + + return NO; +} + +/* + * Returns a string controlling which fields to retrieve for a query. Returns * (all fields) if the preferences + * option dontShowBlob isn't set; otherwise, returns a comma-separated list of all non-blob/text fields. + */ +- (NSString *)fieldListForQuery +{ + NSInteger i; + NSMutableArray *fields = [NSMutableArray array]; + + if (([prefs boolForKey:SPLoadBlobsAsNeeded]) && ([dataColumns count] > 0)) { + + NSArray *columnNames = [tableDataInstance columnNames]; + + for (i = 0 ; i < [columnNames count]; i++) + { + if (![tableDataInstance columnIsBlobOrText:[NSArrayObjectAtIndex(dataColumns, i) objectForKey:@"name"]] ) { + [fields addObject:[NSArrayObjectAtIndex(columnNames, i) backtickQuotedString]]; + } + else { + // For blob/text fields, select a null placeholder so the column count is still correct + [fields addObject:@"NULL"]; + } + } + + return [fields componentsJoinedByString:@","]; + } + else { + return @"*"; + } +} + +/* + * Close an open sheet. + */ +- (void)sheetDidEnd:(id)sheet returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo +{ + [sheet orderOut:self]; +} + +/** + * Show Error sheet (can be called from inside of a endSheet selector) + * via [self performSelector:@selector(showErrorSheetWithTitle:) withObject: afterDelay:] + */ +-(void)showErrorSheetWith:(id)error +{ + // error := first object is the title , second the message, only one button OK + SPBeginAlertSheet([error objectAtIndex:0], NSLocalizedString(@"OK", @"OK button"), + nil, nil, [tableDocumentInstance parentWindow], self, nil, nil, + [error objectAtIndex:1]); +} + +#pragma mark - +#pragma mark Retrieving and setting table state + +/** + * Provide a getter for the table's sort column name + */ +- (NSString *) sortColumnName +{ + if (!sortCol || !dataColumns) return nil; + + return [[dataColumns objectAtIndex:[sortCol integerValue]] objectForKey:@"name"]; +} + +/** + * Provide a getter for the table current sort order + */ +- (BOOL) sortColumnIsAscending +{ + return !isDesc; +} + +/** + * Provide a getter for the table's selected rows index set + */ +- (NSIndexSet *) selectedRowIndexes +{ + return [tableContentView selectedRowIndexes]; +} + +/** + * Provide a getter for the page number + */ +- (NSUInteger) pageNumber +{ + return contentPage; +} + +/** + * Provide a getter for the table's current viewport + */ +- (NSRect) viewport +{ + return [tableContentView visibleRect]; +} + +/** + * Provide a getter for the current filter details + */ +- (NSDictionary *) filterSettings +{ + NSDictionary *theDictionary; + + if (![fieldField isEnabled]) return nil; + + theDictionary = [NSDictionary dictionaryWithObjectsAndKeys: + [self tableFilterString], @"menuLabel", + [[fieldField selectedItem] title], @"filterField", + [[compareField selectedItem] title], @"filterComparison", + [NSNumber numberWithInteger:[[compareField selectedItem] tag]], @"filterComparisonTag", + [argumentField stringValue], @"filterValue", + [firstBetweenField stringValue], @"firstBetweenField", + [secondBetweenField stringValue], @"secondBetweenField", + nil]; + + return theDictionary; +} + +/** + * Set the sort column and sort order to restore on next table load + */ +- (void) setSortColumnNameToRestore:(NSString *)theSortColumnName isAscending:(BOOL)isAscending +{ + if (sortColumnToRestore) [sortColumnToRestore release], sortColumnToRestore = nil; + + if (theSortColumnName) { + sortColumnToRestore = [[NSString alloc] initWithString:theSortColumnName]; + sortColumnToRestoreIsAsc = isAscending; + } +} + +/** + * Sets the value for the page number to use on next table load + */ +- (void) setPageToRestore:(NSUInteger)thePage +{ + pageToRestore = thePage; +} + +/** + * Set the selected row indexes to restore on next table load + */ +- (void) setSelectedRowIndexesToRestore:(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) setViewportToRestore:(NSRect)theViewport +{ + selectionViewportToRestore = theViewport; +} + +/** + * Set the filter settings to restore (if possible) on next table load + */ +- (void) setFiltersToRestore:(NSDictionary *)filterSettings +{ + if (filterFieldToRestore) [filterFieldToRestore release], filterFieldToRestore = nil; + if (filterComparisonToRestore) [filterComparisonToRestore release], filterComparisonToRestore = nil; + if (filterValueToRestore) [filterValueToRestore release], filterValueToRestore = nil; + if (firstBetweenValueToRestore) [firstBetweenValueToRestore release], firstBetweenValueToRestore = nil; + if (secondBetweenValueToRestore) [secondBetweenValueToRestore release], secondBetweenValueToRestore = nil; + + if (filterSettings) { + if ([filterSettings objectForKey:@"filterField"]) + filterFieldToRestore = [[NSString alloc] initWithString:[filterSettings objectForKey:@"filterField"]]; + if ([filterSettings objectForKey:@"filterComparison"]) { + // Check if operator is BETWEEN, if so set up input fields + if([[filterSettings objectForKey:@"filterComparison"] isEqualToString:@"BETWEEN"]) { + [argumentField setHidden:YES]; + [betweenTextField setHidden:NO]; + [firstBetweenField setHidden:NO]; + [secondBetweenField setHidden:NO]; + [firstBetweenField setEnabled:YES]; + [secondBetweenField setEnabled:YES]; + } + + filterComparisonToRestore = [[NSString alloc] initWithString:[filterSettings objectForKey:@"filterComparison"]]; + } + if([[filterSettings objectForKey:@"filterComparison"] isEqualToString:@"BETWEEN"]) { + if ([filterSettings objectForKey:@"firstBetweenField"]) + firstBetweenValueToRestore = [[NSString alloc] initWithString:[filterSettings objectForKey:@"firstBetweenField"]]; + if ([filterSettings objectForKey:@"secondBetweenField"]) + secondBetweenValueToRestore = [[NSString alloc] initWithString:[filterSettings objectForKey:@"secondBetweenField"]]; + } else { + if ([filterSettings objectForKey:@"filterValue"] && ![[filterSettings objectForKey:@"filterValue"] isNSNull]) + filterValueToRestore = [[NSString alloc] initWithString:[filterSettings objectForKey:@"filterValue"]]; + } + } +} + +/** + * Convenience method for storing all current settings for restoration + */ +- (void) storeCurrentDetailsForRestoration +{ + [self setSortColumnNameToRestore:[self sortColumnName] isAscending:[self sortColumnIsAscending]]; + [self setPageToRestore:[self pageNumber]]; + [self setSelectedRowIndexesToRestore:[self selectedRowIndexes]]; + [self setViewportToRestore:[self viewport]]; + [self setFiltersToRestore:[self filterSettings]]; +} + +/** + * Convenience method for clearing any settings to restore + */ +- (void) clearDetailsToRestore +{ + [self setSortColumnNameToRestore:nil isAscending:YES]; + [self setPageToRestore:1]; + [self setSelectedRowIndexesToRestore:nil]; + [self setViewportToRestore:NSZeroRect]; + [self setFiltersToRestore:nil]; +} + +#pragma mark - +#pragma mark Table drawing and editing + +/** + * Updates the number of rows in the selected table. + * Attempts to use the fullResult count if available, also updating the + * table data store; otherwise, uses the table data store if accurate or + * falls back to a fetch if necessary and set in preferences. + * The prefs option "fetch accurate row counts" is used as a last resort as + * it can be very slow on large InnoDB tables which require a full table scan. + */ +- (void)updateNumberOfRows +{ + BOOL checkStatusCount = NO; + + // For unfiltered and non-limited tables, use the result count - and update the status count + if (!isLimited && !isFiltered && !isInterruptedLoad) { + maxNumRows = tableRowsCount; + maxNumRowsIsEstimate = NO; + [tableDataInstance setStatusValue:[NSString stringWithFormat:@"%ld", (long)maxNumRows] forKey:@"Rows"]; + [tableDataInstance setStatusValue:@"y" forKey:@"RowsCountAccurate"]; + [tableInfoInstance tableChanged:nil]; + [[tableDocumentInstance valueForKey:@"extendedTableInfoInstance"] performSelectorOnMainThread:@selector(loadTable:) withObject:selectedTable waitUntilDone:YES]; + + // Otherwise, if the table status value is accurate, use it + } else if ([[tableDataInstance statusValueForKey:@"RowsCountAccurate"] boolValue]) { + maxNumRows = [[tableDataInstance statusValueForKey:@"Rows"] integerValue]; + maxNumRowsIsEstimate = NO; + checkStatusCount = YES; + + // Choose whether to display an estimate, or to fetch the correct row count, based on prefs + } else if ([[prefs objectForKey:SPTableRowCountQueryLevel] integerValue] == SPRowCountFetchAlways + || ([[prefs objectForKey:SPTableRowCountQueryLevel] integerValue] == SPRowCountFetchIfCheap + && [tableDataInstance statusValueForKey:@"Data_length"] + && [[prefs objectForKey:SPTableRowCountCheapSizeBoundary] integerValue] > [[tableDataInstance statusValueForKey:@"Data_length"] integerValue])) + { + maxNumRows = [self fetchNumberOfRows]; + maxNumRowsIsEstimate = NO; + [tableDataInstance setStatusValue:[NSString stringWithFormat:@"%ld", (long)maxNumRows] forKey:@"Rows"]; + [tableDataInstance setStatusValue:@"y" forKey:@"RowsCountAccurate"]; + [tableInfoInstance tableChanged:nil]; + [[tableDocumentInstance valueForKey:@"extendedTableInfoInstance"] performSelectorOnMainThread:@selector(loadTable:) withObject:selectedTable waitUntilDone:YES]; + + // Use the estimate count + } else { + maxNumRows = [[tableDataInstance statusValueForKey:@"Rows"] integerValue]; + maxNumRowsIsEstimate = YES; + checkStatusCount = YES; + } + + // Check whether the estimated count requires updating, ie if the retrieved count exceeds it + if (checkStatusCount) { + NSInteger foundMaxRows; + if ([prefs boolForKey:SPLimitResults]) { + foundMaxRows = ((contentPage - 1) * [prefs integerForKey:SPLimitResultsValue]) + tableRowsCount; + if (foundMaxRows > maxNumRows) { + if (tableRowsCount == [prefs integerForKey:SPLimitResultsValue]) { + maxNumRows = foundMaxRows + 1; + maxNumRowsIsEstimate = YES; + } else { + maxNumRows = foundMaxRows; + maxNumRowsIsEstimate = NO; + } + } else if (!isInterruptedLoad && !isFiltered && tableRowsCount < [prefs integerForKey:SPLimitResultsValue]) { + maxNumRows = foundMaxRows; + maxNumRowsIsEstimate = NO; + } + } else if (tableRowsCount > maxNumRows) { + maxNumRows = tableRowsCount; + maxNumRowsIsEstimate = YES; + } + [tableDataInstance setStatusValue:[NSString stringWithFormat:@"%ld", (long)maxNumRows] forKey:@"Rows"]; + [tableDataInstance setStatusValue:maxNumRowsIsEstimate?@"n":@"y" forKey:@"RowsCountAccurate"]; + [tableInfoInstance tableChanged:nil]; + } +} + +/* + * Fetches the number of rows in the selected table using a "SELECT COUNT(1)" query and return it + */ +- (NSInteger)fetchNumberOfRows +{ + return [[[[mySQLConnection queryString:[NSString stringWithFormat:@"SELECT COUNT(1) FROM %@", [selectedTable backtickQuotedString]]] fetchRowAsArray] objectAtIndex:0] integerValue]; +} + +#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 = [tableValues cellDataAtRow:row column:[[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; +} + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView +{ + return tableRowsCount; +} + +- (id)tableView:(CMCopyTable *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex +{ + 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(&tableValuesLock); + if (rowIndex < tableRowsCount && columnIndex < [tableValues columnCount]) { + theValue = [[SPDataStorageObjectAtRowAndColumn(tableValues, rowIndex, columnIndex) copy] autorelease]; + } + pthread_mutex_unlock(&tableValuesLock); + + if (!theValue) return @"..."; + } else { + theValue = SPDataStorageObjectAtRowAndColumn(tableValues, rowIndex, columnIndex); + } + + if ([theValue isNSNull]) + return [prefs objectForKey:SPNullValue]; + + if ([theValue isKindOfClass:[NSData class]]) + return [theValue shortStringRepresentationUsingEncoding:[mySQLConnection encoding]]; + + if ([theValue isSPNotLoaded]) + return NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields"); + + return theValue; +} + +/** + * This function changes the text color of text/blob fields which are null or not yet loaded to gray + */ +- (void)tableView:(CMCopyTable *)aTableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn*)aTableColumn row:(NSInteger)rowIndex +{ + if (![cell respondsToSelector:@selector(setTextColor:)]) return; + + 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 indicate loading in these cases. + if (isWorking) { + pthread_mutex_lock(&tableValuesLock); + if (rowIndex < tableRowsCount && columnIndex < [tableValues columnCount]) { + theValue = SPDataStorageObjectAtRowAndColumn(tableValues, rowIndex, columnIndex); + } + pthread_mutex_unlock(&tableValuesLock); + + if (!theValue) { + [cell setTextColor:[NSColor lightGrayColor]]; + return; + } + } else { + theValue = SPDataStorageObjectAtRowAndColumn(tableValues, rowIndex, columnIndex); + } + + // If user wants to edit 'cell' set text color to black and return to avoid + // writing in gray if value was NULL + if ([aTableView editedColumn] != -1 + && [aTableView editedRow] == rowIndex + && [[NSArrayObjectAtIndex([aTableView tableColumns], [aTableView editedColumn]) identifier] integerValue] == columnIndex) { + [cell setTextColor:[NSColor blackColor]]; + return; + } + + // For null cells and not loaded cells, display the contents in gray. + if ([theValue isNSNull] || [theValue isSPNotLoaded]) { + [cell setTextColor:[NSColor lightGrayColor]]; + + // Otherwise, set the color to black - required as NSTableView reuses NSCells. + } else { + [cell setTextColor:[NSColor blackColor]]; + } +} + +- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex +{ + // Catch editing events in the row and if the row isn't currently being edited, + // start an edit. This allows edits including enum changes to save correctly. + if ( !isEditingRow ) { + [oldRow setArray:[tableValues rowContentsAtIndex:rowIndex]]; + isEditingRow = YES; + currentlyEditingRow = rowIndex; + } + + NSDictionary *column = NSArrayObjectAtIndex(dataColumns, [[aTableColumn identifier] integerValue]); + + if (anObject) { + + // Restore NULLs if necessary + if ([anObject isEqualToString:[prefs objectForKey:SPNullValue]] && [[column objectForKey:@"null"] boolValue]) + anObject = [NSNull null]; + + [tableValues replaceObjectInRow:rowIndex column:[[aTableColumn identifier] integerValue] withObject:anObject]; + } else { + [tableValues replaceObjectInRow:rowIndex column:[[aTableColumn identifier] integerValue] withObject:@""]; + } +} + +#pragma mark - +#pragma mark TableView delegate methods + +/** + * Sorts the tableView by the clicked column. + * If clicked twice, order is altered to descending. + * Performs the task in a new thread if necessary. + */ +- (void)tableView:(NSTableView*)tableView didClickTableColumn:(NSTableColumn *)tableColumn +{ + + if ( [selectedTable isEqualToString:@""] || !selectedTable ) + return; + + // Prevent sorting while the table is still loading + if ([tableDocumentInstance isWorking]) return; + + // Start the task + [tableDocumentInstance startTaskWithDescription:NSLocalizedString(@"Sorting table...", @"Sorting table task description")]; + if ([NSThread isMainThread]) { + [NSThread detachNewThreadSelector:@selector(sortTableTaskWithColumn:) toTarget:self withObject:tableColumn]; + } else { + [self sortTableTaskWithColumn:tableColumn]; + } +} +- (void)sortTableTaskWithColumn:(NSTableColumn *)tableColumn +{ + NSAutoreleasePool *sortPool = [[NSAutoreleasePool alloc] init]; + + // Check whether a save of the current row is required. + if (![[self onMainThread] saveRowOnDeselect]) { + [sortPool drain]; + return; + } + + // Sets order descending if a header is clicked twice + if ([[tableColumn identifier] isEqualTo:sortCol]) { + isDesc = !isDesc; + } else { + isDesc = NO; + [[tableContentView onMainThread] setIndicatorImage:nil inTableColumn:[tableContentView tableColumnWithIdentifier:sortCol]]; + } + if (sortCol) [sortCol release]; + sortCol = [[NSNumber alloc] initWithInteger:[[tableColumn identifier] integerValue]]; + + // Set the highlight and indicatorImage + [[tableContentView onMainThread] setHighlightedTableColumn:tableColumn]; + if (isDesc) { + [[tableContentView onMainThread] setIndicatorImage:[NSImage imageNamed:@"NSDescendingSortIndicator"] inTableColumn:tableColumn]; + } else { + [[tableContentView onMainThread] setIndicatorImage:[NSImage imageNamed:@"NSAscendingSortIndicator"] inTableColumn:tableColumn]; + } + + // Update data using the new sort order + previousTableRowsCount = tableRowsCount; + [self loadTableValues]; + + if ([mySQLConnection queryErrored]) { + SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocumentInstance parentWindow], self, nil, nil, + [NSString stringWithFormat:NSLocalizedString(@"Couldn't sort table. MySQL said: %@", @"message of panel when sorting of table failed"), [mySQLConnection getLastErrorMessage]]); + [tableDocumentInstance endTask]; + [sortPool drain]; + return; + } + + [tableDocumentInstance endTask]; + [sortPool drain]; +} + +- (void)tableViewSelectionDidChange:(NSNotification *)aNotification +{ + // Check our notification object is our table content view + if ([aNotification object] != tableContentView) return; + + // If we are editing a row, attempt to save that row - if saving failed, reselect the edit row. + if (isEditingRow && [tableContentView selectedRow] != currentlyEditingRow && ![self saveRowOnDeselect]) return; + + if (![tableDocumentInstance isWorking]) { + + // Update the row selection count + // and update the status of the delete/duplicate buttons + if ([tableContentView numberOfSelectedRows] > 0) { + [copyButton setEnabled:YES]; + [removeButton setEnabled:YES]; + } + else { + [copyButton setEnabled:NO]; + [removeButton setEnabled:NO]; + } + } + + [self updateCountText]; +} + +- (void)tableViewColumnDidResize:(NSNotification *)aNotification +/* + saves the new column size in the preferences + */ +{ + // sometimes the column has no identifier. I can't figure out what is causing it, so we just skip over this item + if (![[[aNotification userInfo] objectForKey:@"NSTableColumn"] identifier]) + return; + + NSMutableDictionary *tableColumnWidths; + NSString *database = [NSString stringWithFormat:@"%@@%@", [tableDocumentInstance database], [tableDocumentInstance host]]; + NSString *table = [tablesListInstance tableName]; + + // get tableColumnWidths object + if ( [prefs objectForKey:SPTableColumnWidths] != nil ) { + tableColumnWidths = [NSMutableDictionary dictionaryWithDictionary:[prefs objectForKey:SPTableColumnWidths]]; + } else { + tableColumnWidths = [NSMutableDictionary dictionary]; + } + // get database object + if ( [tableColumnWidths objectForKey:database] == nil ) { + [tableColumnWidths setObject:[NSMutableDictionary dictionary] forKey:database]; + } else { + [tableColumnWidths setObject:[NSMutableDictionary dictionaryWithDictionary:[tableColumnWidths objectForKey:database]] forKey:database]; + + } + // get table object + if ( [[tableColumnWidths objectForKey:database] objectForKey:table] == nil ) { + [[tableColumnWidths objectForKey:database] setObject:[NSMutableDictionary dictionary] forKey:table]; + } else { + [[tableColumnWidths objectForKey:database] setObject:[NSMutableDictionary dictionaryWithDictionary:[[tableColumnWidths objectForKey:database] objectForKey:table]] forKey:table]; + + } + // save column size + [[[tableColumnWidths objectForKey:database] objectForKey:table] setObject:[NSNumber numberWithDouble:[(NSTableColumn *)[[aNotification userInfo] objectForKey:@"NSTableColumn"] width]] forKey:[[[[aNotification userInfo] objectForKey:@"NSTableColumn"] headerCell] stringValue]]; + [prefs setObject:tableColumnWidths forKey:SPTableColumnWidths]; +} + +/** + * Confirm whether to allow editing of a row. Returns YES by default, unless the multipleLineEditingButton is in + * the ON state, or for blob or text fields - in those cases opens a sheet for editing instead and returns NO. + */ +- (BOOL)tableView:(NSTableView *)aTableView shouldEditTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex +{ + if ([tableDocumentInstance isWorking]) return NO; + + // Ensure that row is editable since it could contain "(not loaded)" columns together with + // issue that the table has no primary key + NSString *wherePart = [NSString stringWithString:[self argumentForRow:[tableContentView selectedRow]]]; + if ([wherePart length] == 0) return NO; + + // If the selected cell hasn't been loaded, load it. + if ([[tableValues cellDataAtRow:rowIndex column:[[aTableColumn identifier] integerValue]] isSPNotLoaded]) { + + // Only get the data for the selected column, not all of them + NSString *query = [NSString stringWithFormat:@"SELECT %@ FROM %@ WHERE %@", [[[aTableColumn headerCell] stringValue] backtickQuotedString], [selectedTable backtickQuotedString], wherePart]; + + MCPResult *tempResult = [mySQLConnection queryString:query]; + if (![tempResult numOfRows]) { + SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocumentInstance parentWindow], self, nil, nil, + NSLocalizedString(@"Couldn't load the row. Reload the table to be sure that the row exists and use a primary key for your table.", @"message of panel when loading of row failed")); + return NO; + } + + NSArray *tempRow = [tempResult fetchRowAsArray]; + [tableValues replaceObjectInRow:rowIndex column:[[tableContentView tableColumns] indexOfObject:aTableColumn] withObject:[tempRow objectAtIndex:0]]; + [tableContentView reloadData]; + } + + BOOL isBlob = [tableDataInstance columnIsBlobOrText:[[aTableColumn headerCell] stringValue]]; + + // Open the sheet if the multipleLineEditingButton is enabled or the column was a blob or a text. + if ([multipleLineEditingButton state] == NSOnState || isBlob) { + + SPFieldEditorController *fieldEditor = [[SPFieldEditorController alloc] init]; + + [fieldEditor setTextMaxLength:[[[aTableColumn dataCellForRow:rowIndex] formatter] textLimit]]; + + id cellValue = [tableValues cellDataAtRow:rowIndex column:[[aTableColumn identifier] integerValue]]; + if ([cellValue isNSNull]) cellValue = [NSString stringWithString:[prefs objectForKey:SPNullValue]]; + + id editData = [[fieldEditor editWithObject:cellValue + fieldName:[[aTableColumn headerCell] stringValue] + usingEncoding:[mySQLConnection encoding] + isObjectBlob:isBlob + isEditable:YES + withWindow:[tableDocumentInstance parentWindow]] retain]; + + if (editData) { + if (!isEditingRow) { + [oldRow setArray:[tableValues rowContentsAtIndex:rowIndex]]; + isEditingRow = YES; + currentlyEditingRow = rowIndex; + } + + if ([editData isKindOfClass:[NSString class]] + && [editData isEqualToString:[prefs objectForKey:SPNullValue]] + && [[NSArrayObjectAtIndex(dataColumns, [[aTableColumn identifier] integerValue]) objectForKey:@"null"] boolValue]) + { + [editData release]; + editData = [[NSNull null] retain]; + } + [tableValues replaceObjectInRow:rowIndex column:[[aTableColumn identifier] integerValue] withObject:[[editData copy] autorelease]]; + } + + [fieldEditor release]; + + if (editData) [editData release]; + + return NO; + } + + return YES; +} + +/** + * Enable drag from tableview + */ +- (BOOL)tableView:(NSTableView *)aTableView writeRowsWithIndexes:(NSIndexSet *)rows toPasteboard:(NSPasteboard*)pboard +{ + if (aTableView == tableContentView) { + NSString *tmp; + + // By holding ⌘, ⇧, or/and ⌥ copies selected rows as SQL INSERTS + // otherwise \t delimited lines + if([[NSApp currentEvent] modifierFlags] & (NSCommandKeyMask|NSShiftKeyMask|NSAlternateKeyMask)) + tmp = [tableContentView selectedRowsAsSqlInserts]; + else + tmp = [tableContentView draggedRowsAsTabString]; + + if ( nil != tmp && [tmp length] ) + { + [pboard declareTypes:[NSArray arrayWithObjects: NSTabularTextPboardType, + NSStringPboardType, nil] + owner:nil]; + + [pboard setString:tmp forType:NSStringPboardType]; + [pboard setString:tmp forType:NSTabularTextPboardType]; + return YES; + } + } + + return NO; +} + +/** + * Disable row selection while the document is working. + */ +- (BOOL)tableView:(NSTableView *)aTableView shouldSelectRow:(NSInteger)rowIndex +{ + return tableRowsSelectable; +} + +#pragma mark - +#pragma mark SplitView delegate methods + +- (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview +{ + return NO; +} + +- (CGFloat)splitView:(NSSplitView *)sender constrainMaxCoordinate:(CGFloat)proposedMax ofSubviewAt:(NSInteger)offset +{ + return (proposedMax - 180); +} + +- (CGFloat)splitView:(NSSplitView *)sender constrainMinCoordinate:(CGFloat)proposedMin ofSubviewAt:(NSInteger)offset +{ + return (proposedMin + 200); +} + +#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:SPMainToolbarTableContent]) + return; + + [addButton setEnabled:NO]; + [removeButton setEnabled:NO]; + [copyButton setEnabled:NO]; + [reloadButton setEnabled:NO]; + [filterButton setEnabled:NO]; + tableRowsSelectable = NO; + [paginationPreviousButton setEnabled:NO]; + [paginationNextButton setEnabled:NO]; + [paginationButton 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:SPMainToolbarTableContent]) + return; + + if ( ![[[tableDataInstance statusValues] objectForKey:@"Rows"] isNSNull] && selectedTable && [selectedTable length] && [tableDataInstance tableEncoding]) { + [addButton setEnabled:YES]; + [self updatePaginationState]; + [reloadButton setEnabled:YES]; + } + if ([tableContentView numberOfSelectedRows] > 0) { + [removeButton setEnabled:YES]; + [copyButton setEnabled:YES]; + } + [filterButton setEnabled:[fieldField isEnabled]]; + tableRowsSelectable = YES; +} + +#pragma mark - +#pragma mark Other methods + +/* + * Trap the enter, escape, tab and arrow keys, overriding default behaviour and continuing/ending editing, + * only within the current row. + */ +- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command +{ + NSString *fieldType; + NSUInteger row, column, i; + + row = [tableContentView editedRow]; + column = [tableContentView editedColumn]; + + // Trap tab key + if ( [textView methodForSelector:command] == [textView methodForSelector:@selector(insertTab:)] ) + { + [[control window] makeFirstResponder:control]; + + // Save the current line if it's the last field in the table + if ( column == ( [tableContentView numberOfColumns] - 1 ) ) { + [self addRowToDB]; + } else { + + // Check if next column is a blob column, and skip to the next non-blob column + i = 1; + while ( + (fieldType = [[tableDataInstance columnWithName:[[NSArrayObjectAtIndex([tableContentView tableColumns], column+i) headerCell] stringValue]] objectForKey:@"typegrouping"]) + && ([fieldType isEqualToString:@"textdata"] || [fieldType isEqualToString:@"blobdata"]) + ) { + i++; + + // If there are no columns after the latest blob or text column, save the current line. + if ( (column+i) >= [tableContentView numberOfColumns] ) { + [self addRowToDB]; + return TRUE; + } + } + + // Edit the column after the blob column + [tableContentView editColumn:column+i row:row withEvent:nil select:YES]; + } + return TRUE; + } + + // Trap enter key + else if ( [textView methodForSelector:command] == [textView methodForSelector:@selector(insertNewline:)] ) + { + [[control window] makeFirstResponder:control]; + [self addRowToDB]; + return TRUE; + } + + // Trap down arrow key + else if ( [textView methodForSelector:command] == [textView methodForSelector:@selector(moveDown:)] ) + { + NSUInteger newRow = row+1; + if (newRow>=tableRowsCount) return TRUE; //check if we're already at the end of the list + + [[control window] makeFirstResponder:control]; + [self addRowToDB]; + + if (newRow>=tableRowsCount) return TRUE; //check again. addRowToDB could reload the table and change the number of rows + if (column>=[tableValues columnCount]) return TRUE; //the column count could change too + + [tableContentView selectRowIndexes:[NSIndexSet indexSetWithIndex:newRow] byExtendingSelection:NO]; + [tableContentView editColumn:column row:newRow withEvent:nil select:YES]; + return TRUE; + } + + // Trap up arrow key + else if ( [textView methodForSelector:command] == [textView methodForSelector:@selector(moveUp:)] ) + { + if (row==0) return TRUE; //already at the beginning of the list + NSUInteger newRow = row-1; + + [[control window] makeFirstResponder:control]; + [self addRowToDB]; + + if (newRow>=tableRowsCount) return TRUE; // addRowToDB could reload the table and change the number of rows + if (column>=[tableValues columnCount]) return TRUE; //the column count could change too + + [tableContentView selectRowIndexes:[NSIndexSet indexSetWithIndex:newRow] byExtendingSelection:NO]; + [tableContentView editColumn:column row:newRow withEvent:nil select:YES]; + return TRUE; + } + + // Trap the escape key + else if ( [[control window] methodForSelector:command] == [[control window] methodForSelector:@selector(_cancelKey:)] || + [textView methodForSelector:command] == [textView methodForSelector:@selector(complete:)] ) + { + + // Abort editing + [control abortEditing]; + if ( isEditingRow && !isEditingNewRow ) { + isEditingRow = NO; + [tableValues replaceRowAtIndex:row withRowContents:oldRow]; + } else if ( isEditingNewRow ) { + isEditingRow = NO; + isEditingNewRow = NO; + tableRowsCount--; + [tableValues removeRowAtIndex:row]; + [self updateCountText]; + [tableContentView reloadData]; + } + currentlyEditingRow = -1; + return TRUE; + } + else + { + return FALSE; + } +} + +/** + * 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]) { + [tableContentView setGridStyleMask:([[change objectForKey:NSKeyValueChangeNewKey] boolValue]) ? NSTableViewSolidVerticalGridLineMask : NSTableViewGridNone]; + } + // Table font preference changed + else if ([keyPath isEqualToString:SPGlobalResultTableFont]) { + NSFont *tableFont = [NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]; + [tableContentView setRowHeight:2.0f+NSSizeToCGSize([[NSString stringWithString:@"{ǞṶḹÜ∑zgyf"] sizeWithAttributes:[NSDictionary dictionaryWithObject:tableFont forKey:NSFontAttributeName]]).height]; + [tableContentView setFont:tableFont]; + [tableContentView reloadData]; + } +} + +/** + * Menu validation + */ +- (BOOL)validateMenuItem:(NSMenuItem *)menuItem +{ + // Remove row + if ([menuItem action] == @selector(removeRow:)) { + [menuItem setTitle:([tableContentView numberOfSelectedRows] > 1) ? @"Delete Rows" : @"Delete Row"]; + + return ([tableContentView numberOfSelectedRows] > 0); + } + + // Duplicate row + if ([menuItem action] == @selector(copyRow:)) { + return ([tableContentView numberOfSelectedRows] == 1); + } + + return YES; +} + +/** + * Makes the content filter field have focus by making it the first responder. + */ +- (void)makeContentFilterHaveFocus +{ + + NSDictionary *filter = [[contentFilters objectForKey:compareType] objectAtIndex:[[compareField selectedItem] tag]]; + + if([filter objectForKey:@"NumberOfArguments"]) { + NSUInteger numOfArgs = [[filter objectForKey:@"NumberOfArguments"] integerValue]; + switch(numOfArgs) { + case 2: + [[tableDocumentInstance parentWindow] makeFirstResponder:firstBetweenField]; + break; + case 1: + [[tableDocumentInstance parentWindow] makeFirstResponder:argumentField]; + break; + default: + [[tableDocumentInstance parentWindow] makeFirstResponder:compareField]; + } + } +} + +#pragma mark - + +// Last but not least +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + [tableValues release]; + pthread_mutex_destroy(&tableValuesLock); + [dataColumns release]; + [oldRow release]; + if (selectedTable) [selectedTable release]; + if (contentFilters) [contentFilters release]; + if (numberOfDefaultFilters) [numberOfDefaultFilters release]; + if (keys) [keys release]; + if (sortCol) [sortCol release]; + [usedQuery release]; + if (sortColumnToRestore) [sortColumnToRestore release]; + if (selectionIndexToRestore) [selectionIndexToRestore release]; + if (filterFieldToRestore) filterFieldToRestore = nil; + if (filterComparisonToRestore) filterComparisonToRestore = nil; + if (filterValueToRestore) filterValueToRestore = nil; + if (firstBetweenValueToRestore) firstBetweenValueToRestore = nil; + if (secondBetweenValueToRestore) secondBetweenValueToRestore = nil; + + [super dealloc]; +} + +@end |