//
// SPCopyTable.m
// sequel-pro
//
// Created by Stuart Glenn on April 21, 2004.
// Changed by Lorenz Textor on November 13, 2004
// Copyright (c) 2004 Stuart Glenn. All rights reserved.
// Copyright (c) 2012 Sequel Pro Team. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
// More info at
#import "SPCopyTable.h"
#import "SPTableContent.h"
#import "SPTableTriggers.h"
#import "SPTableRelations.h"
#import "SPCustomQuery.h"
#import "SPDataStorage.h"
#import "SPTextAndLinkCell.h"
#import "SPTooltip.h"
#import "SPAlertSheets.h"
#ifndef SP_CODA /* headers */
#import "SPBundleHTMLOutputController.h"
#endif
#import "SPGeometryDataView.h"
#ifndef SP_CODA /* headers */
#import "SPBundleEditorController.h"
#import "SPAppController.h"
#endif
#import "SPTablesList.h"
#import "SPBundleCommandRunner.h"
#import "SPDatabaseContentViewDelegate.h"
#import
NSInteger SPEditMenuCopy = 2001;
NSInteger SPEditMenuCopyWithColumns = 2002;
NSInteger SPEditCopyAsSQL = 2003;
static const NSInteger kBlobExclude = 1;
static const NSInteger kBlobInclude = 2;
static const NSInteger kBlobAsFile = 3;
static const NSInteger kBlobAsImageFile = 4;
@implementation SPCopyTable
/**
* Hold the selected range of the current table cell editor to be able to set this passed
* selection in the field editor's editTextView
*/
@synthesize fieldEditorSelectedRange;
@synthesize tmpBlobFileDirectory;
/**
* Cell editing in SPCustomQuery or for views in SPTableContent
*/
- (BOOL)isCellEditingMode
{
return ([[self delegate] isKindOfClass:[SPCustomQuery class]]
|| ([[self delegate] isKindOfClass:[SPTableContent class]]
&& [(NSObject*)[self delegate] valueForKeyPath:@"tablesListInstance"]
&& [(SPTablesList*)([(NSObject*)[self delegate] valueForKeyPath:@"tablesListInstance"]) tableType] == SPTableTypeView));
}
/**
* Check if current edited cell represents a class other than a normal NSString
* like pop-up menus for enum or set
*/
- (BOOL)isCellComplex
{
return (![[self preparedCellAtColumn:[self editedColumn] row:[self editedRow]] isKindOfClass:[SPTextAndLinkCell class]]);
}
#pragma mark -
/**
* Handles the general Copy action of selected rows in the table according to sender
*/
- (void)copy:(id)sender
{
#ifndef SP_CODA /* copy table rows */
NSString *tmp = nil;
if ([sender tag] == SPEditCopyAsSQL) {
tmp = [self rowsAsSqlInsertsOnlySelectedRows:YES];
if (tmp != nil){
NSPasteboard *pb = [NSPasteboard generalPasteboard];
[pb declareTypes:[NSArray arrayWithObjects: NSStringPboardType, nil] owner:nil];
[pb setString:tmp forType:NSStringPboardType];
}
}
else {
tmp = [self rowsAsTabStringWithHeaders:([sender tag] == SPEditMenuCopyWithColumns) onlySelectedRows:YES blobHandling:kBlobInclude];
if (tmp != nil) {
NSPasteboard *pb = [NSPasteboard generalPasteboard];
[pb declareTypes:[NSArray arrayWithObjects:NSTabularTextPboardType, NSStringPboardType, nil] owner:nil];
[pb setString:tmp forType:NSStringPboardType];
[pb setString:tmp forType:NSTabularTextPboardType];
}
}
#endif
}
#ifdef SP_CODA
- (void)delete:(id)sender
{
[tableInstance removeRow:self];
}
#endif
/**
* Get selected rows a string of newline separated lines of tab separated fields
* the value in each field is from the objects description method
*/
#ifndef SP_CODA /* get rows as string */
- (NSString *)rowsAsTabStringWithHeaders:(BOOL)withHeaders onlySelectedRows:(BOOL)onlySelected blobHandling:(NSInteger)withBlobHandling
{
if (onlySelected && [self numberOfSelectedRows] == 0) return nil;
NSIndexSet *selectedRows;
if(onlySelected)
selectedRows = [self selectedRowIndexes];
else
selectedRows = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [tableStorage count])];
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++ ){
if([result length])
[result appendString:@"\t"];
[result appendString:[[NSArrayObjectAtIndex(columns, i) headerCell] stringValue]];
}
[result appendString:@"\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] = (NSUInteger)[[NSArrayObjectAtIndex(columns, c) identifier] integerValue];
// Loop through the rows, adding their descriptive contents
NSUInteger rowIndex = [selectedRows firstIndex];
NSString *nullString = [prefs objectForKey:SPNullValue];
Class spmysqlGeometryData = [SPMySQLGeometryData class];
NSUInteger rowCounter = 0;
if((withBlobHandling == kBlobAsFile || withBlobHandling == kBlobAsImageFile) && tmpBlobFileDirectory && [tmpBlobFileDirectory length]) {
NSFileManager *fm = [NSFileManager defaultManager];
[fm removeItemAtPath:tmpBlobFileDirectory error:nil];
[fm createDirectoryAtPath:tmpBlobFileDirectory withIntermediateDirectories:YES attributes:nil error:nil];
}
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),
// definable representation of any blobs or binary texts.
if (cellData) {
if ([cellData isNSNull])
[result appendFormat:@"%@\t", nullString];
else if ([cellData isSPNotLoaded])
[result appendFormat:@"%@\t", NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields")];
else if ([cellData isKindOfClass:[NSData class]]) {
if(withBlobHandling == kBlobInclude) {
NSString *displayString = [[NSString alloc] initWithData:cellData encoding:[mySQLConnection stringEncoding]];
if (!displayString) displayString = [[NSString alloc] initWithData:cellData encoding:NSASCIIStringEncoding];
if (displayString) {
[result appendFormat:@"%@\t", displayString];
[displayString release];
}
}
else if(withBlobHandling == kBlobAsFile && tmpBlobFileDirectory && [tmpBlobFileDirectory length]) {
NSString *fp = [NSString stringWithFormat:@"%@/%ld_%ld.dat", tmpBlobFileDirectory, (long)rowCounter, (long)c];
[cellData writeToFile:fp atomically:NO];
[result appendFormat:@"%@\t", fp];
}
else if(withBlobHandling == kBlobAsImageFile && tmpBlobFileDirectory && [tmpBlobFileDirectory length]) {
NSString *fp = [NSString stringWithFormat:@"%@/%ld_%ld.tif", tmpBlobFileDirectory, (long)rowCounter, (long)c];
NSImage *image = [[NSImage alloc] initWithData:cellData];
if (image) {
NSData *d = [[NSData alloc] initWithData:[image TIFFRepresentationUsingCompression:NSTIFFCompressionLZW factor:1]];
[d writeToFile:fp atomically:NO];
if(d) [d release], d = nil;
[image release];
} else {
NSString *noData = @"";
[noData writeToFile:fp atomically:NO encoding:NSUTF8StringEncoding error:NULL];
}
[result appendFormat:@"%@\t", fp];
}
else {
[result appendString:@"BLOB\t"];
}
}
else if ([cellData isKindOfClass:spmysqlGeometryData]) {
if((withBlobHandling == kBlobAsFile || withBlobHandling == kBlobAsImageFile) && tmpBlobFileDirectory && [tmpBlobFileDirectory length]) {
NSString *fp = [NSString stringWithFormat:@"%@/%ld_%ld.pdf", tmpBlobFileDirectory, (long)rowCounter, (long)c];
SPGeometryDataView *v = [[SPGeometryDataView alloc] initWithCoordinates:[cellData coordinates]];
NSData *thePDF = [v pdfData];
if(thePDF) {
[thePDF writeToFile:fp atomically:NO];
[result appendFormat:@"%@\t", fp];
} else {
[result appendFormat:@"%@\t", [cellData wktString]];
}
if(v) [v release], v = nil;
} else {
[result appendFormat:@"%@\t", [cellData wktString]];
}
}
else
[result appendFormat:@"%@\t", [[[cellData description] stringByReplacingOccurrencesOfString:@"\n" withString:@"↵"] stringByReplacingOccurrencesOfString:@"\t" withString:@"⇥"]];
} else {
[result appendString:@"\t"];
}
}
rowCounter++;
// Remove the trailing tab and add the linebreak
if ([result length]){
[result deleteCharactersInRange:NSMakeRange([result length]-1, 1)];
}
[result appendString:@"\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;
}
/**
* Get selected rows a string of newline separated lines of , separated fields wrapped into quotes
* the value in each field is from the objects description method
*/
- (NSString *)rowsAsCsvStringWithHeaders:(BOOL)withHeaders onlySelectedRows:(BOOL)onlySelected blobHandling:(NSInteger)withBlobHandling
{
if (onlySelected && [self numberOfSelectedRows] == 0) return nil;
NSIndexSet *selectedRows;
if(onlySelected)
selectedRows = [self selectedRowIndexes];
else
selectedRows = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [tableStorage count])];
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++ ){
if([result length])
[result appendString:@","];
[result appendFormat:@"\"%@\"", [[[NSArrayObjectAtIndex(columns, i) headerCell] stringValue] stringByReplacingOccurrencesOfString:@"\"" withString:@"\"\""]];
}
[result appendString:@"\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] = (NSUInteger)[[NSArrayObjectAtIndex(columns, c) identifier] integerValue];
// Loop through the rows, adding their descriptive contents
NSUInteger rowIndex = [selectedRows firstIndex];
NSString *nullString = [prefs objectForKey:SPNullValue];
Class spmysqlGeometryData = [SPMySQLGeometryData class];
NSUInteger rowCounter = 0;
if((withBlobHandling == kBlobAsFile || withBlobHandling == kBlobAsImageFile) && tmpBlobFileDirectory && [tmpBlobFileDirectory length]) {
NSFileManager *fm = [NSFileManager defaultManager];
[fm removeItemAtPath:tmpBlobFileDirectory error:nil];
[fm createDirectoryAtPath:tmpBlobFileDirectory withIntermediateDirectories:YES attributes:nil error:nil];
}
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),
// definable representation of any blobs or binary texts.
if (cellData) {
if ([cellData isNSNull])
[result appendFormat:@"\"%@\",", nullString];
else if ([cellData isSPNotLoaded])
[result appendFormat:@"\"%@\",", NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields")];
else if ([cellData isKindOfClass:[NSData class]]) {
if(withBlobHandling == kBlobInclude) {
NSString *displayString = [[NSString alloc] initWithData:cellData encoding:[mySQLConnection stringEncoding]];
if (!displayString) displayString = [[NSString alloc] initWithData:cellData encoding:NSASCIIStringEncoding];
if (displayString) {
[result appendFormat:@"\"%@\",", displayString];
[displayString release];
}
}
else if(withBlobHandling == kBlobAsFile && tmpBlobFileDirectory && [tmpBlobFileDirectory length]) {
NSString *fp = [NSString stringWithFormat:@"%@/%ld_%ld.dat", tmpBlobFileDirectory, (long)rowCounter, (long)c];
[cellData writeToFile:fp atomically:NO];
[result appendFormat:@"\"%@\",", fp];
}
else if(withBlobHandling == kBlobAsImageFile && tmpBlobFileDirectory && [tmpBlobFileDirectory length]) {
NSString *fp = [NSString stringWithFormat:@"%@/%ld_%ld.tif", tmpBlobFileDirectory, (long)rowCounter, (long)c];
NSImage *image = [[NSImage alloc] initWithData:cellData];
if (image) {
NSData *d = [[NSData alloc] initWithData:[image TIFFRepresentationUsingCompression:NSTIFFCompressionLZW factor:1]];
[d writeToFile:fp atomically:NO];
if(d) [d release], d = nil;
[image release];
} else {
NSString *noData = @"";
[noData writeToFile:fp atomically:NO encoding:NSUTF8StringEncoding error:NULL];
}
[result appendFormat:@"\"%@\",", fp];
}
else {
[result appendString:@"\"BLOB\","];
}
}
else if ([cellData isKindOfClass:spmysqlGeometryData]) {
if((withBlobHandling == kBlobAsFile || withBlobHandling == kBlobAsImageFile) && tmpBlobFileDirectory && [tmpBlobFileDirectory length]) {
NSString *fp = [NSString stringWithFormat:@"%@/%ld_%ld.pdf", tmpBlobFileDirectory, (long)rowCounter, (long)c];
SPGeometryDataView *v = [[SPGeometryDataView alloc] initWithCoordinates:[cellData coordinates]];
NSData *thePDF = [v pdfData];
if(thePDF) {
[thePDF writeToFile:fp atomically:NO];
[result appendFormat:@"\"%@\",", fp];
} else {
[result appendFormat:@"\"%@\",", [cellData wktString]];
}
if(v) [v release], v = nil;
} else {
[result appendFormat:@"\"%@\",", [cellData wktString]];
}
}
else
[result appendFormat:@"\"%@\",", [cellData description]];
} else {
[result appendString:@","];
}
}
rowCounter++;
// Remove the trailing tab and add the linebreak
if ([result length]){
[result deleteCharactersInRange:NSMakeRange([result length]-1, 1)];
}
[result appendString:@"\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;
}
#endif
/*
* Return selected rows as SQL INSERT INTO `foo` VALUES (baz) string.
* If no selected table name is given `` will be used instead.
*/
- (NSString *)rowsAsSqlInsertsOnlySelectedRows:(BOOL)onlySelected
{
if (onlySelected && [self numberOfSelectedRows] == 0) return nil;
NSIndexSet *selectedRows = (onlySelected) ? [self selectedRowIndexes] : [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [tableStorage count])];
NSArray *columns = [self tableColumns];
NSUInteger numColumns = [columns count];
NSMutableString *value = [NSMutableString stringWithCapacity:10];
id cellData = nil;
NSUInteger rowCounter = 0;
NSUInteger penultimateRowIndex = [selectedRows count];
NSUInteger c;
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] = (NSUInteger)[[NSArrayObjectAtIndex(columns, c) identifier] integerValue];
NSString *t = [NSArrayObjectAtIndex(columnDefinitions, 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;
// GEOMETRY data
else if ([t isEqualToString:@"geometry"])
columnTypes[c] = 3;
// Default to strings
else
columnTypes[c] = 1;
}
// Begin the SQL string
[result appendFormat:@"INSERT INTO %@ (%@)\nVALUES\n",
[(selectedTable == nil) ? @"" : selectedTable backtickQuotedString], [tbHeader componentsJoinedAndBacktickQuoted]];
NSUInteger rowIndex = [selectedRows firstIndex];
Class spTableContentClass = [SPTableContent class];
Class nsDataClass = [NSData class];
while (rowIndex != NSNotFound)
{
[value appendString:@"\t("];
cellData = nil;
rowCounter++;
NSMutableArray *rowValues = [[NSMutableArray alloc] initWithCapacity:numColumns];
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:spTableContentClass]) {
// Abort if no table name given, not table content, or if there are no indices on this table
if (!selectedTable || ![[self delegate] isKindOfClass:spTableContentClass] || ![(NSString*)[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 %@",
[NSArrayObjectAtIndex(tbHeader, columnMappings[c]) backtickQuotedString],
[selectedTable backtickQuotedString],
[tableInstance argumentForRow:rowIndex]]];
}
// Check for NULL value
if ([cellData isNSNull]) {
[rowValues addObject:@"NULL"];
continue;
}
else if (cellData) {
// Check column type and insert the data accordingly
switch (columnTypes[c]) {
// Convert numeric types to unquoted strings
case 0:
[rowValues addObject:[cellData description]];
break;
// Quote string, text and blob types appropriately
case 1:
case 2:
if ([cellData isKindOfClass:nsDataClass]) {
[rowValues addObject:[mySQLConnection escapeAndQuoteData:cellData]];
} else {
[rowValues addObject:[mySQLConnection escapeAndQuoteString:[cellData description]]];
}
break;
// GEOMETRY
case 3:
[rowValues addObject:[mySQLConnection escapeAndQuoteData:[cellData data]]];
break;
// Unhandled cases - abort
default:
NSBeep();
free(columnMappings);
free(columnTypes);
[rowValues release];
return nil;
}
// If nil is encountered, abort
}
else {
NSBeep();
free(columnMappings);
free(columnTypes);
[rowValues release];
return nil;
}
}
// Add to the string in comma-separated form, and increment the string length
[value appendString:[rowValues componentsJoinedByString:@", "]];
[rowValues release];
// 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 ([value length] > 250000) {
[result appendFormat:@"%@);\n\nINSERT INTO %@ (%@)\nVALUES\n",
value,
[(selectedTable == nil) ? @"" : selectedTable backtickQuotedString],
[tbHeader componentsJoinedAndBacktickQuoted]];
[value setString:@""];
}
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;
}
/**
* Allow for drag-n-drop out of the application as a copy
*/
- (NSUInteger) draggingSourceOperationMaskForLocal:(BOOL)isLocal
{
return NSDragOperationCopy;
}
/**
* 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] = (NSUInteger)[[NSArrayObjectAtIndex(columns, c) identifier] integerValue];
// Loop through the rows, adding their descriptive contents
NSUInteger rowIndex = [selectedRows firstIndex];
NSString *nullString = [prefs objectForKey:SPNullValue];
Class nsDataClass = [NSData class];
Class spmysqlGeometryData = [SPMySQLGeometryData class];
NSStringEncoding connectionEncoding = [mySQLConnection stringEncoding];
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 appendFormat:@"%@\t", nullString];
else if ([cellData isSPNotLoaded])
[result appendFormat:@"%@\t", NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields")];
else if ([cellData isKindOfClass:nsDataClass]) {
NSString *displayString = [[NSString alloc] initWithData:cellData encoding:connectionEncoding];
if (!displayString) displayString = [[NSString alloc] initWithData:cellData encoding:NSASCIIStringEncoding];
if (displayString) {
[result appendString:displayString];
[displayString release];
}
}
else if ([cellData isKindOfClass:spmysqlGeometryData]) {
[result appendFormat:@"%@\t", [cellData wktString]];
} else
[result appendFormat:@"%@\t", [cellData description]];
} else {
[result appendString:@"\t"];
}
}
if ([result length]) {
[result deleteCharactersInRange:NSMakeRange([result length]-1, 1)];
}
[result appendString:@"\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;
}
#pragma mark -
/**
* 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 = nil;
columnDefinitions = [[NSArray alloc] initWithArray:columnDefs];
}
/*
* Update the table storage location if necessary.
*/
- (void) setTableData:(SPDataStorage *)theTableStorage
{
tableStorage = theTableStorage;
}
#pragma mark -
/**
* Autodetect column widths for a specified font.
*/
- (NSDictionary *) autodetectColumnWidths
{
NSMutableDictionary *columnWidths = [NSMutableDictionary dictionaryWithCapacity:[columnDefinitions count]];
NSUInteger columnWidth;
NSUInteger allColumnWidths = 0;
// Determine the available size
NSScrollView *parentScrollView = (NSScrollView*)[[self superview] superview];
CGFloat visibleTableWidth = [parentScrollView bounds].size.width - [NSScroller scrollerWidth] - [columnDefinitions count] * 3.5f;
for (NSDictionary *columnDefinition in columnDefinitions) {
if ([[NSThread currentThread] isCancelled]) return nil;
columnWidth = [self autodetectWidthForColumnDefinition:columnDefinition maxRows:100];
[columnWidths setObject:[NSString stringWithFormat:@"%llu", (unsigned long long)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 > visibleTableWidth) {
NSUInteger availableWidthToReduce = 0;
// Look for columns that are wider than the multi-column max
for (NSString *columnIdentifier in columnWidths) {
columnWidth = [[columnWidths objectForKey:columnIdentifier] integerValue];
if (columnWidth > SP_MAX_CELL_WIDTH_MULTICOLUMN) availableWidthToReduce += columnWidth - SP_MAX_CELL_WIDTH_MULTICOLUMN;
}
// Determine how much width can be reduced
NSUInteger widthToReduce = allColumnWidths - visibleTableWidth;
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] integerValue];
if (columnWidth > SP_MAX_CELL_WIDTH_MULTICOLUMN) {
columnWidth -= ceilf((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;
unichar breakChar;
#ifndef SP_CODA /* patch */
NSFont *tableFont = [NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPGlobalResultTableFont]];
#else
NSFont *tableFont = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
#endif
NSUInteger columnIndex = (NSUInteger)[[columnDefinition objectForKey:@"datacolumnindex"] integerValue];
NSDictionary *stringAttributes = [NSDictionary dictionaryWithObject:tableFont forKey:NSFontAttributeName];
Class spmysqlGeometryData = [SPMySQLGeometryData class];
// Check the number of rows available to check, sampling every n rows
if ([tableStorage count] < rowsToCheck)
rowStep = 1;
else
rowStep = floorf([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 part of the cell's content to get widths, topping out at a maximum length
contentString = SPDataStoragePreviewAtRowAndColumn(tableStorage, i, columnIndex, 500);
// If the cell hasn't loaded yet, skip processing
if (!contentString)
continue;
// Get WKT string out of the SPMySQLGeometryData for calculation
else if ([contentString isKindOfClass:spmysqlGeometryData])
contentString = [contentString wktString];
// Replace NULLs with their placeholder string
else 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 stringEncoding]];
} else if ([(NSString *)contentString length] > 500) {
contentString = [contentString substringToIndex:500];
}
// If any linebreaks are present, they are displayed as single characters; replace them with pilcrow/
// reverse pilcrow to match display output width.
linebreakRange = [contentString rangeOfCharacterFromSet:[NSCharacterSet newlineCharacterSet] options:NSLiteralSearch];
if (linebreakRange.location != NSNotFound) {
NSMutableString *singleLineString = [[[NSMutableString alloc] initWithString:contentString] autorelease];
while (linebreakRange.location != NSNotFound) {
breakChar = [singleLineString characterAtIndex:linebreakRange.location];
switch (breakChar) {
case '\n':
[singleLineString replaceCharactersInRange:linebreakRange withString:@"¶"];
break;
default:
[singleLineString replaceCharactersInRange:linebreakRange withString:@"⁋"];
if (breakChar == '\r' && NSMaxRange(linebreakRange) < [singleLineString length] && [singleLineString characterAtIndex:linebreakRange.location+1] == '\n') {
[singleLineString deleteCharactersInRange:NSMakeRange(linebreakRange.location+1, 1)];
}
}
linebreakRange = [singleLineString rangeOfCharacterFromSet:[NSCharacterSet newlineCharacterSet] options:NSLiteralSearch];
}
contentString = singleLineString;
}
}
// 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;
}
#pragma mark -
- (NSMenu *)menuForEvent:(NSEvent *)event
{
NSMenu *menu = [self menu];
#ifndef SP_CODA /* menuForEvent: */
if(![[self delegate] isKindOfClass:[SPCustomQuery class]] && ![[self delegate] isKindOfClass:[SPTableContent class]]) return menu;
[[NSApp delegate] reloadBundles:self];
// Remove 'Bundles' sub menu and separator
NSMenuItem *bItem = [menu itemWithTag:10000000];
if(bItem) {
NSInteger sepIndex = [menu indexOfItem:bItem]-1;
[menu removeItemAtIndex:sepIndex];
[menu removeItem:bItem];
}
NSArray *bundleCategories = [[NSApp delegate] bundleCategoriesForScope:SPBundleScopeDataTable];
NSArray *bundleItems = [[NSApp delegate] bundleItemsForScope:SPBundleScopeDataTable];
// Add 'Bundles' sub menu
if(bundleItems && [bundleItems count]) {
[menu addItem:[NSMenuItem separatorItem]];
NSMenu *bundleMenu = [[[NSMenu alloc] init] autorelease];
NSMenuItem *bundleSubMenuItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Bundles", @"bundles menu item label") action:nil keyEquivalent:@""];
[bundleSubMenuItem setTag:10000000];
[menu addItem:bundleSubMenuItem];
[menu setSubmenu:bundleMenu forItem:bundleSubMenuItem];
NSMutableArray *categorySubMenus = [NSMutableArray array];
NSMutableArray *categoryMenus = [NSMutableArray array];
if([bundleCategories count]) {
for(NSString* title in bundleCategories) {
[categorySubMenus addObject:[[[NSMenuItem alloc] initWithTitle:title action:nil keyEquivalent:@""] autorelease]];
[categoryMenus addObject:[[[NSMenu alloc] init] autorelease]];
[bundleMenu addItem:[categorySubMenus lastObject]];
[bundleMenu setSubmenu:[categoryMenus lastObject] forItem:[categorySubMenus lastObject]];
}
}
NSInteger i = 0;
for(NSDictionary *item in bundleItems) {
NSString *keyEq;
if([item objectForKey:SPBundleFileKeyEquivalentKey])
keyEq = [[item objectForKey:SPBundleFileKeyEquivalentKey] objectAtIndex:0];
else
keyEq = @"";
NSMenuItem *mItem = [[[NSMenuItem alloc] initWithTitle:[item objectForKey:SPBundleInternLabelKey] action:@selector(executeBundleItemForDataTable:) keyEquivalent:keyEq] autorelease];
if([keyEq length])
[mItem setKeyEquivalentModifierMask:[[[item objectForKey:SPBundleFileKeyEquivalentKey] objectAtIndex:1] intValue]];
if([item objectForKey:SPBundleFileTooltipKey])
[mItem setToolTip:[item objectForKey:SPBundleFileTooltipKey]];
[mItem setTag:1000000 + i++];
if([item objectForKey:SPBundleFileCategoryKey]) {
[[categoryMenus objectAtIndex:[bundleCategories indexOfObject:[item objectForKey:SPBundleFileCategoryKey]]] addItem:mItem];
} else {
[bundleMenu addItem:mItem];
}
}
[bundleSubMenuItem release];
}
#endif
return menu;
}
- (void)selectTableRows:(NSArray*)rowIndices
{
if (!rowIndices || ![rowIndices count]) return;
NSMutableIndexSet *selection = [NSMutableIndexSet indexSet];
NSInteger rows = [(id)[self delegate] numberOfRowsInTableView:self];
NSInteger i;
if(rows > 0) {
for (NSString* idx in rowIndices)
{
i = [idx integerValue];
if (i >= 0 && i < rows) {
[selection addIndex:i];
}
}
[self selectRowIndexes:selection byExtendingSelection:NO];
}
}
/**
* Only have the copy menu item enabled when row(s) are selected in
* supported tables.
*/
- (BOOL)validateMenuItem:(NSMenuItem*)anItem
{
#ifndef SP_CODA /* validateMenuItem: */
NSInteger menuItemTag = [anItem tag];
if ([anItem action] == @selector(performFindPanelAction:)) {
return (menuItemTag == 1 && [[self delegate] isKindOfClass:[SPTableContent class]]);
}
// Don't validate anything other than the copy commands
if (menuItemTag != SPEditMenuCopy && menuItemTag != SPEditMenuCopyWithColumns && menuItemTag != SPEditCopyAsSQL) {
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 == SPEditMenuCopy || menuItemTag == SPEditMenuCopyWithColumns) {
return ([self numberOfSelectedRows] > 0);
}
// Enable the Copy as SQL commands if rows are selected and column definitions are available
if (menuItemTag == SPEditCopyAsSQL) {
return (columnDefinitions != nil && [self numberOfSelectedRows] > 0);
}
#endif
#ifdef SP_CODA
if ( [anItem action] == @selector(selectAll:) )
return YES;
if ( [anItem action] == @selector(delete:) )
{
if ( [self numberOfSelectedRows] > 0 )
return YES;
}
#endif
return NO;
}
/**
* 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
{
NSInteger row, column;
row = [self editedRow];
column = [self editedColumn];
// Trap tab key
// -- for handling of blob fields and to check if it's editable look at [[self delegate] control:textShouldBeginEditing:]
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 ( [self numberOfColumns] - 1 == column ) {
if([[self delegate] respondsToSelector:@selector(saveRowToTable)])
[(SPTableContent*)[self delegate] saveRowToTable];
[[self window] makeFirstResponder:self];
} else {
// Select the next field for editing
[self editColumn:column+1 row:row withEvent:nil select:YES];
}
return YES;
}
// Trap shift-tab key
else if ( [textView methodForSelector:command] == [textView methodForSelector:@selector(insertBacktab:)] )
{
[[control window] makeFirstResponder:control];
// Save the current line if it's the last field in the table
if ( column < 1 ) {
if([[self delegate] respondsToSelector:@selector(saveRowToTable)])
[(SPTableContent*)([self delegate]) saveRowToTable];
[[self window] makeFirstResponder:self];
} else {
// Select the previous field for editing
[self editColumn:column-1 row:row withEvent:nil select:YES];
}
return YES;
}
// Trap enter key
else if ( [textView methodForSelector:command] == [textView methodForSelector:@selector(insertNewline:)] )
{
// If enum field is edited RETURN selects the new value instead of saving the entire row
if([self isCellComplex])
return YES;
[[control window] makeFirstResponder:control];
if([[self delegate] isKindOfClass:[SPTableContent class]] && ![self isCellEditingMode] && [[self delegate] respondsToSelector:@selector(saveRowToTable)])
[(SPTableContent*)[self delegate] saveRowToTable];
return YES;
}
// Trap down arrow key
else if ( [textView methodForSelector:command] == [textView methodForSelector:@selector(moveDown:)] )
{
// If enum field is edited ARROW key navigates through the popup list
if([self isCellComplex])
return NO;
// Check whether the editor is multiline - if so, allow the arrow down to change selection if it's not
// on the final line
if (NSMaxRange([[textView string] lineRangeForRange:[textView selectedRange]]) < [[textView string] length])
return NO;
NSInteger newRow = row+1;
// Check if we're already at the end of the list
if (newRow >= [(id)[self delegate] numberOfRowsInTableView:self]) return YES;
[[control window] makeFirstResponder:control];
if ([[self delegate] isKindOfClass:[SPTableContent class]] && ![self isCellEditingMode] && [[self delegate] respondsToSelector:@selector(saveRowToTable)]) {
[(SPTableContent*)([self delegate]) saveRowToTable];
}
// Check again. saveRowToTable could reload the table and change the number of rows
if (newRow>=[(id)[self delegate] numberOfRowsInTableView:self]) return YES;
// The column count could change too
if (tableStorage && (NSUInteger)column >= [tableStorage columnCount]) return YES;
[self selectRowIndexes:[NSIndexSet indexSetWithIndex:newRow] byExtendingSelection:NO];
[self editColumn:column row:newRow withEvent:nil select:YES];
return YES;
}
// Trap up arrow key
else if ( [textView methodForSelector:command] == [textView methodForSelector:@selector(moveUp:)] )
{
// If enum field is edited ARROW key navigates through the popup list
if ([self isCellComplex]) return NO;
// Check whether the editor is multiline - if so, allow the arrow up to change selection if it's not
// on the first line
if ([[textView string] lineRangeForRange:[textView selectedRange]].location > 0) return NO;
// Already at the beginning of the list
if (row == 0) return YES;
NSInteger newRow = row-1;
[[control window] makeFirstResponder:control];
if ([[self delegate] isKindOfClass:[SPTableContent class]] && ![self isCellEditingMode] && [[self delegate] respondsToSelector:@selector(saveRowToTable)]) {
[(SPTableContent *)[self delegate] saveRowToTable];
}
// saveRowToTable could reload the table and change the number of rows
if (newRow >= [(id)[self delegate] numberOfRowsInTableView:self]) return YES;
// The column count could change too
if (tableStorage && (NSUInteger)column>=[tableStorage columnCount]) return YES;
[self selectRowIndexes:[NSIndexSet indexSetWithIndex:newRow] byExtendingSelection:NO];
[self editColumn:column row:newRow withEvent:nil select:YES];
return YES;
}
return NO;
}
- (void)keyDown:(NSEvent *)theEvent
{
// RETURN or ENTER invoke editing mode for selected row
// by calling tableView:shouldEditTableColumn: to validate
if([self numberOfSelectedRows] == 1 && ([theEvent keyCode] == 36 || [theEvent keyCode] == 76)) {
[self editColumn:0 row:[self selectedRow] withEvent:nil select:YES];
return;
}
// Check if ESCAPE is hit and use it to cancel row editing if supported
if ([theEvent keyCode] == 53 && [[self delegate] respondsToSelector:@selector(cancelRowEditing)])
{
if ([[self delegate] performSelector:@selector(cancelRowEditing)]) return;
}
else if ([theEvent keyCode] == 48 && ([[self delegate] isKindOfClass:[SPCustomQuery class]]
|| [[self delegate] isKindOfClass:[SPTableContent class]])) {
[self editColumn:0 row:[self selectedRow] withEvent:nil select:YES];
return;
}
[super keyDown:theEvent];
}
#pragma mark -
#pragma mark Field editing checks
/**
* Determine whether to use the sheet for editing; do so if the multipleLineEditingButton is enabled,
* or if the column was a blob or a text, or if it contains linebreaks.
*/
- (BOOL)shouldUseFieldEditorForRow:(NSUInteger)rowIndex column:(NSUInteger)colIndex
{
// Retrieve the column definition
NSDictionary *columnDefinition = [[(id )[self delegate] dataColumnDefinitions] objectAtIndex:colIndex];
NSString *columnType = [columnDefinition objectForKey:@"typegrouping"];
// Return YES if the multiple line editing button is enabled - triggers sheet editing on all cells.
#ifndef SP_CODA
if ([prefs boolForKey:SPEditInSheetEnabled]) return YES;
#endif
// If the column is a BLOB or TEXT column, and not an enum, trigger sheet editing
BOOL isBlob = ([columnType isEqualToString:@"textdata"] || [columnType isEqualToString:@"blobdata"]);
if (isBlob && ![columnType isEqualToString:@"enum"]) return YES;
// Otherwise, check the cell value for newlines.
id cellValue = [tableStorage cellDataAtRow:rowIndex column:colIndex];
if ([cellValue isKindOfClass:[NSData class]]) {
cellValue = [[[NSString alloc] initWithData:cellValue encoding:[mySQLConnection stringEncoding]] autorelease];
}
if (![cellValue isNSNull]
&& [columnType isEqualToString:@"string"]
&& [cellValue rangeOfCharacterFromSet:[NSCharacterSet newlineCharacterSet] options:NSLiteralSearch].location != NSNotFound)
{
return YES;
}
// Otherwise, use standard editing
return NO;
}
#pragma mark -
#pragma mark Bundle Command Support
- (IBAction)executeBundleItemForDataTable:(id)sender
{
#ifndef SP_CODA /* executeBundleItemForDataTable: */
NSInteger idx = [sender tag] - 1000000;
NSString *infoPath = nil;
NSArray *bundleItems = [[NSApp delegate] bundleItemsForScope:SPBundleScopeDataTable];
if(idx >=0 && idx < (NSInteger)[bundleItems count]) {
infoPath = [[bundleItems objectAtIndex:idx] objectForKey:SPBundleInternPathToFileKey];
} else {
if([sender tag] == 0 && [[sender toolTip] length]) {
infoPath = [sender toolTip];
}
}
if(!infoPath) {
NSBeep();
return;
}
NSError *readError = nil;
NSString *convError = nil;
NSPropertyListFormat format;
NSDictionary *cmdData = nil;
NSData *pData = [NSData dataWithContentsOfFile:infoPath options:NSUncachedRead error:&readError];
cmdData = [[NSPropertyListSerialization propertyListFromData:pData
mutabilityOption:NSPropertyListImmutable format:&format errorDescription:&convError] retain];
if(!cmdData || readError != nil || [convError length] || !(format == NSPropertyListXMLFormat_v1_0 || format == NSPropertyListBinaryFormat_v1_0)) {
NSLog(@"“%@” file couldn't be read.", infoPath);
NSBeep();
if (cmdData) [cmdData release];
return;
} else {
if([cmdData objectForKey:SPBundleFileCommandKey] && [(NSString *)[cmdData objectForKey:SPBundleFileCommandKey] length]) {
NSString *cmd = [cmdData objectForKey:SPBundleFileCommandKey];
NSString *inputAction = @"";
NSString *inputFallBackAction = @"";
NSError *err = nil;
NSString *uuid = [NSString stringWithNewUUID];
NSString *bundleInputFilePath = [NSString stringWithFormat:@"%@_%@", SPBundleTaskInputFilePath, uuid];
NSString *bundleInputTableMetaDataFilePath = [NSString stringWithFormat:@"%@_%@", SPBundleTaskTableMetaDataFilePath, uuid];
[[NSFileManager defaultManager] removeItemAtPath:bundleInputFilePath error:nil];
if([cmdData objectForKey:SPBundleFileInputSourceKey])
inputAction = [[cmdData objectForKey:SPBundleFileInputSourceKey] lowercaseString];
if([cmdData objectForKey:SPBundleFileInputSourceFallBackKey])
inputFallBackAction = [[cmdData objectForKey:SPBundleFileInputSourceFallBackKey] lowercaseString];
NSMutableDictionary *env = [NSMutableDictionary dictionary];
[env setObject:[infoPath stringByDeletingLastPathComponent] forKey:SPBundleShellVariableBundlePath];
[env setObject:bundleInputFilePath forKey:SPBundleShellVariableInputFilePath];
if ([[self delegate] respondsToSelector:@selector(usedQuery)] && [(id )[self delegate] usedQuery]) {
[env setObject:[(id )[self delegate] usedQuery] forKey:SPBundleShellVariableUsedQueryForTable];
}
[env setObject:bundleInputTableMetaDataFilePath forKey:SPBundleShellVariableInputTableMetaData];
[env setObject:SPBundleScopeDataTable forKey:SPBundleShellVariableBundleScope];
if([self numberOfSelectedRows]) {
NSMutableArray *sel = [NSMutableArray array];
NSIndexSet *selectedRows = [self selectedRowIndexes];
NSUInteger rowIndex = [selectedRows firstIndex];
while ( rowIndex != NSNotFound ) {
[sel addObject:[NSString stringWithFormat:@"%llu", (unsigned long long)rowIndex]];
rowIndex = [selectedRows indexGreaterThanIndex:rowIndex];
}
[env setObject:[sel componentsJoinedByString:@"\t"] forKey:SPBundleShellVariableSelectedRowIndices];
}
NSError *inputFileError = nil;
NSString *input = @"";
NSInteger blobHandling = kBlobExclude;
if([cmdData objectForKey:SPBundleFileWithBlobKey]) {
if([[cmdData objectForKey:SPBundleFileWithBlobKey] isEqualToString:SPBundleInputSourceBlobHandlingExclude])
blobHandling = kBlobExclude;
else if([[cmdData objectForKey:SPBundleFileWithBlobKey] isEqualToString:SPBundleInputSourceBlobHandlingInclude])
blobHandling = kBlobInclude;
else if([[cmdData objectForKey:SPBundleFileWithBlobKey] isEqualToString:SPBundleInputSourceBlobHandlingImageFileReference])
blobHandling = kBlobAsImageFile;
else if([[cmdData objectForKey:SPBundleFileWithBlobKey] isEqualToString:SPBundleInputSourceBlobHandlingFileReference])
blobHandling = kBlobAsFile;
}
if(blobHandling != kBlobExclude) {
NSString *bundleBlobFilePath = [NSString stringWithFormat:@"%@_%@", SPBundleTaskCopyBlobFileDirectory, uuid];
[env setObject:bundleBlobFilePath forKey:SPBundleShellVariableBlobFileDirectory];
[self setTmpBlobFileDirectory:bundleBlobFilePath];
} else {
[self setTmpBlobFileDirectory:@""];
}
if([inputAction isEqualToString:SPBundleInputSourceSelectedTableRowsAsTab]) {
input = [self rowsAsTabStringWithHeaders:YES onlySelectedRows:YES blobHandling:blobHandling];
}
else if([inputAction isEqualToString:SPBundleInputSourceSelectedTableRowsAsCsv]) {
input = [self rowsAsCsvStringWithHeaders:YES onlySelectedRows:YES blobHandling:blobHandling];
}
else if([inputAction isEqualToString:SPBundleInputSourceSelectedTableRowsAsSqlInsert]) {
input = [self rowsAsSqlInsertsOnlySelectedRows:YES];
}
else if([inputAction isEqualToString:SPBundleInputSourceTableRowsAsTab]) {
input = [self rowsAsTabStringWithHeaders:YES onlySelectedRows:NO blobHandling:blobHandling];
}
else if([inputAction isEqualToString:SPBundleInputSourceTableRowsAsCsv]) {
input = [self rowsAsCsvStringWithHeaders:YES onlySelectedRows:NO blobHandling:blobHandling];
}
else if([inputAction isEqualToString:SPBundleInputSourceTableRowsAsSqlInsert]) {
input = [self rowsAsSqlInsertsOnlySelectedRows:NO];
}
if(input == nil) input = @"";
[input writeToFile:bundleInputFilePath
atomically:YES
encoding:NSUTF8StringEncoding
error:&inputFileError];
if(inputFileError != nil) {
NSString *errorMessage = [inputFileError localizedDescription];
SPBeginAlertSheet(NSLocalizedString(@"Bundle Error", @"bundle error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [self window], self, nil, nil,
[NSString stringWithFormat:@"%@ “%@”:\n%@", NSLocalizedString(@"Error for", @"error for message"), [cmdData objectForKey:@"name"], errorMessage]);
if (cmdData) [cmdData release];
return;
}
// Create an array of table column mappings for fast iteration
NSArray *columns = [self tableColumns];
NSUInteger numColumns = [columns count];
NSUInteger *columnMappings = malloc(numColumns * sizeof(NSUInteger));
NSUInteger c;
for ( c = 0; c < numColumns; c++ )
columnMappings[c] = (NSUInteger)[[NSArrayObjectAtIndex(columns, c) identifier] integerValue];
NSMutableString *tableMetaData = [NSMutableString string];
if([[self delegate] isKindOfClass:[SPCustomQuery class]]) {
[env setObject:@"query" forKey:SPBundleShellVariableDataTableSource];
NSArray *defs = [(id )[self delegate] dataColumnDefinitions];
if(defs && [defs count] == numColumns)
for( c = 0; c < numColumns; c++ ) {
NSDictionary *col = NSArrayObjectAtIndex(defs, columnMappings[c]);
[tableMetaData appendFormat:@"%@\t", [col objectForKey:@"type"]];
[tableMetaData appendFormat:@"%@\t", [col objectForKey:@"typegrouping"]];
[tableMetaData appendFormat:@"%@\t", ([col objectForKey:@"char_length"]) ? : @""];
[tableMetaData appendFormat:@"%@\t", [col objectForKey:@"UNSIGNED_FLAG"]];
[tableMetaData appendFormat:@"%@\t", [col objectForKey:@"AUTO_INCREMENT_FLAG"]];
[tableMetaData appendFormat:@"%@\t", [col objectForKey:@"PRI_KEY_FLAG"]];
[tableMetaData appendString:@"\n"];
}
}
else if([[self delegate] isKindOfClass:[SPTableContent class]]) {
[env setObject:@"content" forKey:SPBundleShellVariableDataTableSource];
NSArray *defs = [(id )[self delegate] dataColumnDefinitions];
if(defs && [defs count] == numColumns)
for( c = 0; c < numColumns; c++ ) {
NSDictionary *col = NSArrayObjectAtIndex(defs, columnMappings[c]);
[tableMetaData appendFormat:@"%@\t", [col objectForKey:@"type"]];
[tableMetaData appendFormat:@"%@\t", [col objectForKey:@"typegrouping"]];
[tableMetaData appendFormat:@"%@\t", ([col objectForKey:@"length"]) ? : @""];
[tableMetaData appendFormat:@"%@\t", [col objectForKey:@"unsigned"]];
[tableMetaData appendFormat:@"%@\t", [col objectForKey:@"autoincrement"]];
[tableMetaData appendFormat:@"%@\t", ([col objectForKey:@"isprimarykey"]) ? : @"0"];
[tableMetaData appendFormat:@"%@\n", [col objectForKey:@"comment"]];
}
}
free(columnMappings);
inputFileError = nil;
[tableMetaData writeToFile:bundleInputTableMetaDataFilePath
atomically:YES
encoding:NSUTF8StringEncoding
error:&inputFileError];
if(inputFileError != nil) {
NSString *errorMessage = [inputFileError localizedDescription];
SPBeginAlertSheet(NSLocalizedString(@"Bundle Error", @"bundle error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [self window], self, nil, nil,
[NSString stringWithFormat:@"%@ “%@”:\n%@", NSLocalizedString(@"Error for", @"error for message"), [cmdData objectForKey:@"name"], errorMessage]);
if (cmdData) [cmdData release];
return;
}
NSString *output = [SPBundleCommandRunner runBashCommand:cmd withEnvironment:env
atCurrentDirectoryPath:nil
callerInstance:[[NSApp delegate] frontDocument]
contextInfo:[NSDictionary dictionaryWithObjectsAndKeys:
([cmdData objectForKey:SPBundleFileNameKey])?:@"-", @"name",
NSLocalizedString(@"Data Table", @"data table menu item label"), @"scope",
uuid, SPBundleFileInternalexecutionUUID, nil]
error:&err];
[[NSFileManager defaultManager] removeItemAtPath:bundleInputFilePath error:nil];
NSString *action = SPBundleOutputActionNone;
if([cmdData objectForKey:SPBundleFileOutputActionKey] && [(NSString *)[cmdData objectForKey:SPBundleFileOutputActionKey] length])
action = [[cmdData objectForKey:SPBundleFileOutputActionKey] lowercaseString];
// Redirect due exit code
if(err != nil) {
if([err code] == SPBundleRedirectActionNone) {
action = SPBundleOutputActionNone;
err = nil;
}
else if([err code] == SPBundleRedirectActionReplaceSection) {
action = SPBundleOutputActionReplaceSelection;
err = nil;
}
else if([err code] == SPBundleRedirectActionReplaceContent) {
action = SPBundleOutputActionReplaceContent;
err = nil;
}
else if([err code] == SPBundleRedirectActionInsertAsText) {
action = SPBundleOutputActionInsertAsText;
err = nil;
}
else if([err code] == SPBundleRedirectActionInsertAsSnippet) {
action = SPBundleOutputActionInsertAsSnippet;
err = nil;
}
else if([err code] == SPBundleRedirectActionShowAsHTML) {
action = SPBundleOutputActionShowAsHTML;
err = nil;
}
else if([err code] == SPBundleRedirectActionShowAsTextTooltip) {
action = SPBundleOutputActionShowAsTextTooltip;
err = nil;
}
else if([err code] == SPBundleRedirectActionShowAsHTMLTooltip) {
action = SPBundleOutputActionShowAsHTMLTooltip;
err = nil;
}
}
if(err == nil && output) {
if(![action isEqualToString:SPBundleOutputActionNone]) {
NSPoint pos = [NSEvent mouseLocation];
pos.y -= 16;
if([action isEqualToString:SPBundleOutputActionShowAsTextTooltip]) {
[SPTooltip showWithObject:output atLocation:pos];
}
else if([action isEqualToString:SPBundleOutputActionShowAsHTMLTooltip]) {
[SPTooltip showWithObject:output atLocation:pos ofType:@"html"];
}
else if([action isEqualToString:SPBundleOutputActionShowAsHTML]) {
BOOL correspondingWindowFound = NO;
for(id win in [NSApp windows]) {
if([[win delegate] isKindOfClass:[SPBundleHTMLOutputController class]]) {
if([[[win delegate] windowUUID] isEqualToString:[cmdData objectForKey:SPBundleFileUUIDKey]]) {
correspondingWindowFound = YES;
[[win delegate] displayHTMLContent:output withOptions:nil];
break;
}
}
}
if(!correspondingWindowFound) {
SPBundleHTMLOutputController *bundleController = [[SPBundleHTMLOutputController alloc] init];
[bundleController setWindowUUID:[cmdData objectForKey:SPBundleFileUUIDKey]];
[bundleController displayHTMLContent:output withOptions:nil];
[[NSApp delegate] addHTMLOutputController:bundleController];
}
}
}
} else if([err code] != 9) { // Suppress an error message if command was killed
NSString *errorMessage = [err localizedDescription];
SPBeginAlertSheet(NSLocalizedString(@"BASH Error", @"bash error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [self window], self, nil, nil,
[NSString stringWithFormat:@"%@ “%@”:\n%@", NSLocalizedString(@"Error for", @"error for message"), [cmdData objectForKey:@"name"], errorMessage]);
}
}
if (cmdData) [cmdData release];
}
#endif
}
#pragma mark -
- (void)awakeFromNib
{
columnDefinitions = nil;
prefs = [[NSUserDefaults standardUserDefaults] retain];
if ([NSTableView instancesRespondToSelector:@selector(awakeFromNib)]) {
[super awakeFromNib];
}
}
- (void)dealloc
{
if (columnDefinitions) [columnDefinitions release];
#ifndef SP_CODA
[prefs release];
#endif
[super dealloc];
}
@end