diff options
Diffstat (limited to 'Source/SPCopyTable.m')
-rw-r--r-- | Source/SPCopyTable.m | 632 |
1 files changed, 632 insertions, 0 deletions
diff --git a/Source/SPCopyTable.m b/Source/SPCopyTable.m new file mode 100644 index 00000000..6a65a934 --- /dev/null +++ b/Source/SPCopyTable.m @@ -0,0 +1,632 @@ +// +// $Id$ +// +// SPCopyTable.m +// sequel-pro +// +// Created by Stuart Glenn on Wed Apr 21 2004. +// Changed by Lorenz Textor on Sat Nov 13 2004 +// Copyright (c) 2004 Stuart Glenn. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +#import <MCPKit/MCPKit.h> + +#import "SPCopyTable.h" +#import "SPArrayAdditions.h" +#import "SPStringAdditions.h" +#import "SPTableContent.h" +#import "SPTableTriggers.h" +#import "SPTableRelations.h" +#import "SPCustomQuery.h" +#import "SPNotLoaded.h" +#import "SPConstants.h" +#import "SPDataStorage.h" + +NSInteger MENU_EDIT_COPY = 2001; +NSInteger MENU_EDIT_COPY_WITH_COLUMN = 2002; +NSInteger MENU_EDIT_COPY_AS_SQL = 2003; + +@implementation SPCopyTable + +- (void)copy:(id)sender +{ + NSString *tmp = nil; + + if([sender tag] == MENU_EDIT_COPY_AS_SQL) { + tmp = [self selectedRowsAsSqlInserts]; + if ( nil != tmp ) + { + NSPasteboard *pb = [NSPasteboard generalPasteboard]; + + [pb declareTypes:[NSArray arrayWithObjects: NSStringPboardType, nil] + owner:nil]; + + [pb setString:tmp forType:NSStringPboardType]; + } + } else { + tmp = [self selectedRowsAsTabStringWithHeaders:([sender tag] == MENU_EDIT_COPY_WITH_COLUMN)]; + if ( nil != tmp ) + { + NSPasteboard *pb = [NSPasteboard generalPasteboard]; + + [pb declareTypes:[NSArray arrayWithObjects: NSTabularTextPboardType, + NSStringPboardType, nil] + owner:nil]; + + [pb setString:tmp forType:NSStringPboardType]; + [pb setString:tmp forType:NSTabularTextPboardType]; + } + } +} + +//allow for drag-n-drop out of the application as a copy +- (NSUInteger)draggingSourceOperationMaskForLocal:(BOOL)isLocal +{ + return NSDragOperationCopy; +} + +/** + * Only have the copy menu item enabled when row(s) are selected in + * supported tables. + */ +- (BOOL)validateMenuItem:(NSMenuItem*)anItem +{ + NSInteger menuItemTag = [anItem tag]; + + // Don't validate anything other than the copy commands + if (menuItemTag != MENU_EDIT_COPY && menuItemTag != MENU_EDIT_COPY_WITH_COLUMN && menuItemTag != MENU_EDIT_COPY_AS_SQL) { + return YES; + } + + // Don't enable menus for relations or triggers - no action to take yet + if ([[self delegate] isKindOfClass:[SPTableRelations class]] || [[self delegate] isKindOfClass:[SPTableTriggers class]]) { + return NO; + } + + // Enable the Copy [with column names] commands if a row is selected + if (menuItemTag == MENU_EDIT_COPY || menuItemTag == MENU_EDIT_COPY_WITH_COLUMN) { + return ([self numberOfSelectedRows] > 0); + } + + // Enable the Copy as SQL commands if rows are selected and column definitions are available + if (menuItemTag == MENU_EDIT_COPY_AS_SQL) { + return (columnDefinitions != nil && [self numberOfSelectedRows] > 0); + } + + return NO; +} + +//get selected rows a string of newline separated lines of tab separated fields +//the value in each field is from the objects description method +- (NSString *)selectedRowsAsTabStringWithHeaders:(BOOL)withHeaders +{ + if ([self numberOfSelectedRows] == 0) return nil; + + NSIndexSet *selectedRows = [self selectedRowIndexes]; + + NSArray *columns = [self tableColumns]; + NSUInteger numColumns = [columns count]; + NSMutableString *result = [NSMutableString stringWithCapacity:2000]; + + // Add the table headers if requested to do so + if (withHeaders) { + NSUInteger i; + for( i = 0; i < numColumns; i++ ){ + [result appendString:[NSString stringWithFormat:@"%@\t", [[NSArrayObjectAtIndex(columns, i) headerCell] stringValue]]]; + } + [result appendString:[NSString stringWithFormat:@"\n"]]; + } + + NSUInteger c; + id cellData = nil; + + // Create an array of table column mappings for fast iteration + NSUInteger *columnMappings = malloc(numColumns * sizeof(NSUInteger)); + for ( c = 0; c < numColumns; c++) { + columnMappings[c] = [[[columns objectAtIndex:c] identifier] unsignedIntValue]; + } + + // Loop through the rows, adding their descriptive contents + NSUInteger rowIndex = [selectedRows firstIndex]; + while ( rowIndex != NSNotFound ) + { + for ( c = 0; c < numColumns; c++) { + cellData = SPDataStorageObjectAtRowAndColumn(tableStorage, rowIndex, columnMappings[c]); + + // Copy the shown representation of the cell - custom NULL display strings, (not loaded), + // and the string representation of any blobs or binary texts. + if (cellData) { + if ([cellData isNSNull]) + [result appendString:[NSString stringWithFormat:@"%@\t", [prefs objectForKey:SPNullValue]]]; + else if ([cellData isSPNotLoaded]) + [result appendString:[NSString stringWithFormat:@"%@\t", NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields")]]; + else if ([cellData isKindOfClass:[NSData class]]) { + NSString *displayString = [[NSString alloc] initWithData:cellData encoding:[mySQLConnection encoding]]; + if (!displayString) displayString = [[NSString alloc] initWithData:cellData encoding:NSASCIIStringEncoding]; + if (displayString) { + [result appendString:displayString]; + [displayString release]; + [result appendString:@"\t"]; + } + } else + [result appendString:[NSString stringWithFormat:@"%@\t", [cellData description]]]; + } else { + [result appendString:@"\t"]; + } + } + + // Remove the trailing tab and add the linebreak + if ([result length]){ + [result deleteCharactersInRange:NSMakeRange([result length]-1, 1)]; + } + [result appendString:[NSString stringWithFormat:@"\n"]]; + + // Select the next row index + rowIndex = [selectedRows indexGreaterThanIndex:rowIndex]; + } + + // Remove the trailing line end + if ([result length]) { + [result deleteCharactersInRange:NSMakeRange([result length]-1, 1)]; + } + + free(columnMappings); + + return result; +} + +/* + * Return selected rows as SQL INSERT INTO `foo` VALUES (baz) string. + * If no selected table name is given `<table>` will be used instead. + */ +- (NSString *)selectedRowsAsSqlInserts +{ + + if ( [self numberOfSelectedRows] < 1 ) return nil; + + NSArray *columns = [self tableColumns]; + NSUInteger numColumns = [columns count]; + + NSIndexSet *selectedRows = [self selectedRowIndexes]; + NSMutableString *value = [NSMutableString stringWithCapacity:10]; + + id cellData = nil; + + NSUInteger rowCounter = 0; + NSUInteger penultimateRowIndex = [selectedRows count]; + NSUInteger c; + NSUInteger valueLength = 0; + + NSMutableString *result = [NSMutableString stringWithCapacity:2000]; + + // Create an array of table column names + NSMutableArray *tbHeader = [NSMutableArray arrayWithCapacity:numColumns]; + for (id enumObj in columns) { + [tbHeader addObject:[[enumObj headerCell] stringValue]]; + } + + // Create arrays of table column mappings and types for fast iteration + NSUInteger *columnMappings = malloc(numColumns * sizeof(NSUInteger)); + NSUInteger *columnTypes = malloc(numColumns * sizeof(NSUInteger)); + for ( c = 0; c < numColumns; c++) { + columnMappings[c] = [[[columns objectAtIndex:c] identifier] unsignedIntValue]; + + NSString *t = [[columnDefinitions objectAtIndex:columnMappings[c]] objectForKey:@"typegrouping"]; + + // Numeric data + if ([t isEqualToString:@"bit"] || [t isEqualToString:@"integer"] || [t isEqualToString:@"float"]) + columnTypes[c] = 0; + + // Blob data or long text data + else if ([t isEqualToString:@"blobdata"] || [t isEqualToString:@"textdata"]) + columnTypes[c] = 2; + + // Default to strings + else + columnTypes[c] = 1; + } + + // Begin the SQL string + [result appendString:[NSString stringWithFormat:@"INSERT INTO %@ (%@)\nVALUES\n", + [(selectedTable == nil)?@"<table>":selectedTable backtickQuotedString], [tbHeader componentsJoinedAndBacktickQuoted]]]; + + NSUInteger rowIndex = [selectedRows firstIndex]; + while ( rowIndex != NSNotFound ) + { + [value appendString:@"\t("]; + cellData = nil; + rowCounter++; + for ( c = 0; c < numColumns; c++ ) + { + cellData = SPDataStorageObjectAtRowAndColumn(tableStorage, rowIndex, columnMappings[c]); + + // If the data is not loaded, attempt to fetch the value + if ([cellData isSPNotLoaded] && [[self delegate] isKindOfClass:[SPTableContent class]]) { + + // Abort if no table name given, not table content, or if there are no indices on this table + if (!selectedTable || ![[self delegate] isKindOfClass:[SPTableContent class]] || ![[tableInstance argumentForRow:rowIndex] length]) { + NSBeep(); + free(columnMappings); + free(columnTypes); + return nil; + } + + // Use the argumentForRow to retrieve the missing information + // TODO - this could be preloaded for all selected rows rather than cell-by-cell + cellData = [mySQLConnection getFirstFieldFromQuery: + [NSString stringWithFormat:@"SELECT %@ FROM %@ WHERE %@", + [[tbHeader objectAtIndex:columnMappings[c]] backtickQuotedString], + [selectedTable backtickQuotedString], + [tableInstance argumentForRow:rowIndex]]]; + } + + // Check for NULL value + if ([cellData isNSNull]) { + [value appendString:@"NULL, "]; + continue; + + } else if (cellData) { + + // Check column type and insert the data accordingly + switch(columnTypes[c]) { + + // Convert numeric types to unquoted strings + case 0: + [value appendString:[NSString stringWithFormat:@"%@, ", [cellData description]]]; + break; + + // Quote string, text and blob types appropriately + case 1: + case 2: + if ([cellData isKindOfClass:[NSData class]]) { + [value appendString:[NSString stringWithFormat:@"X'%@', ", [mySQLConnection prepareBinaryData:cellData]]]; + } else { + [value appendString:[NSString stringWithFormat:@"'%@', ", [mySQLConnection prepareString:[cellData description]]]]; + } + break; + + // Unhandled cases - abort + default: + NSBeep(); + free(columnMappings); + free(columnTypes); + return nil; + } + + // If nil is encountered, abort + } else { + NSBeep(); + free(columnMappings); + free(columnTypes); + return nil; + } + } + + // Remove the trailing ', ' from the query + if ( [value length] > 2 ) + [value deleteCharactersInRange:NSMakeRange([value length]-2, 2)]; + + valueLength += [value length]; + + // Close this VALUES group and set up the next one if appropriate + if ( rowCounter != penultimateRowIndex ) { + + // Add a new INSERT starter command every ~250k of data. + if ( valueLength > 250000 ) { + [result appendString:value]; + [result appendString:[NSString stringWithFormat:@");\n\nINSERT INTO %@ (%@)\nVALUES\n", + [(selectedTable == nil)?@"<table>":selectedTable backtickQuotedString], [tbHeader componentsJoinedAndBacktickQuoted]]]; + [value setString:@""]; + valueLength = 0; + } else { + [value appendString:@"),\n"]; + } + + } else { + [value appendString:@"),\n"]; + [result appendString:value]; + } + + // Get the next selected row index + rowIndex = [selectedRows indexGreaterThanIndex:rowIndex]; + + } + + // Remove the trailing ",\n" from the query string + if ( [result length] > 3 ) + [result deleteCharactersInRange:NSMakeRange([result length]-2, 2)]; + + [result appendString:@";\n"]; + + free(columnMappings); + free(columnTypes); + + return result; +} + + +//get dragged rows a string of newline separated lines of tab separated fields +//the value in each field is from the objects description method +- (NSString *)draggedRowsAsTabString +{ + NSArray *columns = [self tableColumns]; + NSUInteger numColumns = [columns count]; + NSIndexSet *selectedRows = [self selectedRowIndexes]; + + NSMutableString *result = [NSMutableString stringWithCapacity:2000]; + NSUInteger c; + id cellData = nil; + + // Create an array of table column mappings for fast iteration + NSUInteger *columnMappings = malloc(numColumns * sizeof(NSUInteger)); + for ( c = 0; c < numColumns; c++) { + columnMappings[c] = [[[columns objectAtIndex:c] identifier] unsignedIntValue]; + } + + // Loop through the rows, adding their descriptive contents + NSUInteger rowIndex = [selectedRows firstIndex]; + while ( rowIndex != NSNotFound ) + { + for ( c = 0; c < numColumns; c++) { + cellData = SPDataStorageObjectAtRowAndColumn(tableStorage, rowIndex, columnMappings[c]); + + // Copy the shown representation of the cell - custom NULL display strings, (not loaded), + // and the string representation of any blobs or binary texts. + if (cellData) { + if ([cellData isNSNull]) + [result appendString:[NSString stringWithFormat:@"%@\t", [prefs objectForKey:SPNullValue]]]; + else if ([cellData isSPNotLoaded]) + [result appendString:[NSString stringWithFormat:@"%@\t", NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields")]]; + else if ([cellData isKindOfClass:[NSData class]]) { + NSString *displayString = [[NSString alloc] initWithData:cellData encoding:[mySQLConnection encoding]]; + if (!displayString) displayString = [[NSString alloc] initWithData:cellData encoding:NSASCIIStringEncoding]; + if (displayString) { + [result appendString:displayString]; + [displayString release]; + } + } else + [result appendString:[NSString stringWithFormat:@"%@\t", [cellData description]]]; + } else { + [result appendString:@"\t"]; + } + } + + if ([result length]) { + [result deleteCharactersInRange:NSMakeRange([result length]-1, 1)]; + } + + [result appendString:[NSString stringWithFormat:@"\n"]]; + + // Retrieve the next selected row index + rowIndex = [selectedRows indexGreaterThanIndex:rowIndex]; + } + + // Trim the trailing line ending + if ([result length]) { + [result deleteCharactersInRange:NSMakeRange([result length]-1, 1)]; + } + + free(columnMappings); + + return result; +} + +/** + * Init self with data coming from the table content view. Mainly used for copying data properly. + */ +- (void)setTableInstance:(id)anInstance withTableData:(SPDataStorage *)theTableStorage withColumns:(NSArray *)columnDefs withTableName:(NSString *)aTableName withConnection:(id)aMySqlConnection +{ + selectedTable = aTableName; + mySQLConnection = aMySqlConnection; + tableInstance = anInstance; + tableStorage = theTableStorage; + + if (columnDefinitions) [columnDefinitions release]; + columnDefinitions = [[NSArray alloc] initWithArray:columnDefs]; +} + +/* + * Update the table storage location if necessary. + */ +- (void)setTableData:(SPDataStorage *)theTableStorage +{ + tableStorage = theTableStorage; +} + +/** + * Autodetect column widths for a specified font. + */ +- (NSDictionary *) autodetectColumnWidths +{ + NSMutableDictionary *columnWidths = [NSMutableDictionary dictionaryWithCapacity:[columnDefinitions count]]; + NSUInteger columnWidth; + NSUInteger allColumnWidths = 0; + + for (NSDictionary *columnDefinition in columnDefinitions) { + if ([[NSThread currentThread] isCancelled]) return nil; + + columnWidth = [self autodetectWidthForColumnDefinition:columnDefinition maxRows:100]; + [columnWidths setObject:[NSNumber numberWithUnsignedInteger:columnWidth] forKey:[columnDefinition objectForKey:@"datacolumnindex"]]; + allColumnWidths += columnWidth; + } + + // Compare the column widths to the table width. If wider, narrow down wide columns as necessary + if (allColumnWidths > [self bounds].size.width) { + NSUInteger availableWidthToReduce = 0; + + // Look for columns that are wider than the multi-column max + for (NSString *columnIdentifier in columnWidths) { + columnWidth = [[columnWidths objectForKey:columnIdentifier] unsignedIntegerValue]; + if (columnWidth > SP_MAX_CELL_WIDTH_MULTICOLUMN) availableWidthToReduce += columnWidth - SP_MAX_CELL_WIDTH_MULTICOLUMN; + } + + // Determine how much width can be reduced + NSUInteger widthToReduce = allColumnWidths - [self bounds].size.width; + if (availableWidthToReduce < widthToReduce) widthToReduce = availableWidthToReduce; + + // Proportionally decrease the column sizes + if (widthToReduce) { + NSArray *columnIdentifiers = [columnWidths allKeys]; + for (NSString *columnIdentifier in columnIdentifiers) { + columnWidth = [[columnWidths objectForKey:columnIdentifier] unsignedIntegerValue]; + if (columnWidth > SP_MAX_CELL_WIDTH_MULTICOLUMN) { + columnWidth -= ceil((double)(columnWidth - SP_MAX_CELL_WIDTH_MULTICOLUMN) / availableWidthToReduce * widthToReduce); + [columnWidths setObject:[NSNumber numberWithUnsignedInteger:columnWidth] forKey:columnIdentifier]; + } + } + } + } + + return columnWidths; +} + +/** + * Autodetect the column width for a specified column - derived from the supplied + * column definition, using the stored data and the specified font. + */ +- (NSUInteger)autodetectWidthForColumnDefinition:(NSDictionary *)columnDefinition maxRows:(NSUInteger)rowsToCheck +{ + CGFloat columnBaseWidth; + id contentString; + NSUInteger cellWidth, maxCellWidth, i; + NSRange linebreakRange; + double rowStep; + NSFont *tableFont = [NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPGlobalResultTableFont]]; + NSUInteger columnIndex = [[columnDefinition objectForKey:@"datacolumnindex"] unsignedIntegerValue]; + NSDictionary *stringAttributes = [NSDictionary dictionaryWithObject:tableFont forKey:NSFontAttributeName]; + + // Check the number of rows available to check, sampling every n rows + if ([tableStorage count] < rowsToCheck) { + rowStep = 1; + } else { + rowStep = floor([tableStorage count] / rowsToCheck); + } + rowsToCheck = [tableStorage count]; + + // Set a default padding for this column + columnBaseWidth = 24; + + // Iterate through the data store rows, checking widths + maxCellWidth = 0; + for (i = 0; i < rowsToCheck; i += rowStep) { + + // Retrieve the cell's content + contentString = [tableStorage cellDataAtRow:i column:columnIndex]; + + // Replace NULLs with their placeholder string + if ([contentString isNSNull]) { + contentString = [prefs objectForKey:SPNullValue]; + + // Same for cells for which loading has been deferred - likely blobs + } else if ([contentString isSPNotLoaded]) { + contentString = NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields"); + + } else { + + // Otherwise, ensure the cell is represented as a short string + if ([contentString isKindOfClass:[NSData class]]) { + contentString = [contentString shortStringRepresentationUsingEncoding:[mySQLConnection encoding]]; + } else if ([contentString length] > 500) { + contentString = [contentString substringToIndex:500]; + } + + // If any linebreaks are present, use only the visible part of the string + linebreakRange = [contentString rangeOfCharacterFromSet:[NSCharacterSet newlineCharacterSet]]; + if (linebreakRange.location != NSNotFound) { + contentString = [contentString substringToIndex:linebreakRange.location]; + } + } + + // Calculate the width, using it if it's higher than the current stored width + cellWidth = [contentString sizeWithAttributes:stringAttributes].width; + if (cellWidth > maxCellWidth) maxCellWidth = cellWidth; + if (maxCellWidth > SP_MAX_CELL_WIDTH) { + maxCellWidth = SP_MAX_CELL_WIDTH; + break; + } + } + + // If the column has a foreign key link, expand the width; and also for enums + if ([columnDefinition objectForKey:@"foreignkeyreference"]) { + maxCellWidth += 18; + } else if ([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"enum"]) { + maxCellWidth += 8; + } + + // Add the padding + maxCellWidth += columnBaseWidth; + + // If the header width is wider than this expanded width, use it instead + cellWidth = [[columnDefinition objectForKey:@"name"] sizeWithAttributes:[NSDictionary dictionaryWithObject:[NSFont labelFontOfSize:[NSFont smallSystemFontSize]] forKey:NSFontAttributeName]].width; + if (cellWidth + 10 > maxCellWidth) maxCellWidth = cellWidth + 10; + + return maxCellWidth; +} + +- (void)keyDown:(NSEvent *)theEvent +{ + // RETURN or ENTER invoke editing mode for selected row + // by calling tableView:shouldEditTableColumn: to validate + if([[[[self delegate] class] description] isEqualToString:@"SPTableContent"]) { + + id tableContentView = [[self delegate] valueForKeyPath:@"tableContentView"]; + if([tableContentView numberOfSelectedRows] == 1 && ([theEvent keyCode] == 36 || [theEvent keyCode] == 76)) { + if([[self delegate] tableView:tableContentView shouldEditTableColumn:[[tableContentView tableColumns] objectAtIndex:0] row:[tableContentView selectedRow]]) { + [self editColumn:0 row:[self selectedRow] withEvent:nil select:YES]; + return; + } + } + } + if([[[[self delegate] class] description] isEqualToString:@"SPCustomQuery"]) { + id tableContentView = [[self delegate] valueForKeyPath:@"customQueryView"]; + if([tableContentView numberOfSelectedRows] == 1 && ([theEvent keyCode] == 36 || [theEvent keyCode] == 76)) { + + // TODO: this works until the user presses OK in the Field Editor Sheet!! + // in the future we should store the new row data temporarily and then + // after editing the last column update the db field by field (ask HansJB) + NSInteger colNum = [[tableContentView tableColumns] count]; + NSInteger i; + for(i=0; i<colNum; i++) { + [[self delegate] tableView:tableContentView shouldEditTableColumn:[[tableContentView tableColumns] objectAtIndex:i] row:[tableContentView selectedRow]]; + } + return; + } + } + + [super keyDown:theEvent]; +} + +#pragma mark - + +- (void) awakeFromNib +{ + columnDefinitions = nil; + prefs = [[NSUserDefaults standardUserDefaults] retain]; + + if ([NSTableView instancesRespondToSelector:@selector(awakeFromNib)]) { + [super awakeFromNib] ; + } +} + +- (void) dealloc +{ + if (columnDefinitions) [columnDefinitions release]; + [prefs release]; + + [super dealloc]; +} + +@end |