diff options
author | stuconnolly <stuart02@gmail.com> | 2008-12-10 16:52:52 +0000 |
---|---|---|
committer | stuconnolly <stuart02@gmail.com> | 2008-12-10 16:52:52 +0000 |
commit | fab9a6506cd04ec8f840c98772a80c44a79c74a7 (patch) | |
tree | 3cd483487bef381c934717f10df71d306c7eaf97 /Source | |
parent | 4c3b208fad0572d8d1a79bba1bd1b8147fd0f8a6 (diff) | |
download | sequelpro-fab9a6506cd04ec8f840c98772a80c44a79c74a7.tar.gz sequelpro-fab9a6506cd04ec8f840c98772a80c44a79c74a7.tar.bz2 sequelpro-fab9a6506cd04ec8f840c98772a80c44a79c74a7.zip |
MERGED r262:266 from branches/stuart02 to trunk to include new project structure.
Diffstat (limited to 'Source')
42 files changed, 12768 insertions, 0 deletions
diff --git a/Source/CMCopyTable.h b/Source/CMCopyTable.h new file mode 100644 index 00000000..8b4e1485 --- /dev/null +++ b/Source/CMCopyTable.h @@ -0,0 +1,92 @@ +/*! + @header CMCopyTable.h + @abstract sequel-pro + @discussion <pre> + $Id:$ + 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. +</pre> +*/ + +// 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 <AppKit/AppKit.h> + + +/*! + @class copyTable + @abstract subclassed NSTableView to implement copy & drag-n-drop + @discussion Allows copying by creating a string with each table row as + a separate line and each cell then separate via tabs. The drag out + is in similar format. The values for each cell are obtained via the + objects description method +*/ +@interface CMCopyTable : NSTableView +{ + +} + +/*! + @method copy: + @abstract does the work of copying + @discussion gets selected (if any) row(s) as a string setting it + then into th default pasteboard as a string type and tabular text type. + @param sender who asked for this copy? +*/ +- (void)copy:(id)sender; + +/*! + @method validateMenuItem: + @abstract Dynamically enable Copy menu item for the table view + @discussion Will only enable the Copy item when something is selected in + this table view + @param anItem the menu item being validated + @result YES if there is at least one row selected & the menu item is + copy, NO otherwise +*/ +- (BOOL)validateMenuItem:(NSMenuItem*)anItem; + +/*! + @method draggingSourceOperationMaskForLocal: + @discussion Allows for dragging out of the table to other applications + @param isLocal who cares + @result Always calls for a copy type drag operation +*/ +- (unsigned int)draggingSourceOperationMaskForLocal:(BOOL)isLocal; + +/*! + @method selectedRowsAsTabString + @abstract getter of the selected rows of the table for copy + @discussion For the selected rows returns a single string with each row + separated by a newline and then for each column value separated by a + tab. Values are from the objects description method, so make sure it + returns something meaningful. + @result The above described string, or nil if nothing selected +*/ +- (NSString *)selectedRowsAsTabString; + +/*! + @method draggedRowsAsTabString: + @abstract getter of the dragged rows of the table for drag + @discussion For the dragged rows returns a single string with each row + separated by a newline and then for each column value separated by a + tab. Values are from the objects description method, so make sure it + returns something meaningful. + @result The above described string, or nil if nothing selected +*/ +- (NSString *)draggedRowsAsTabString:(NSArray *)rows; + +@end diff --git a/Source/CMCopyTable.m b/Source/CMCopyTable.m new file mode 100644 index 00000000..7eb80c52 --- /dev/null +++ b/Source/CMCopyTable.m @@ -0,0 +1,183 @@ +// +// CMCopyTable.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 "CMCopyTable.h" + + +@implementation CMCopyTable + +- (void)copy:(id)sender +{ + NSString *tmp = [self selectedRowsAsTabString]; + + 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 +- (unsigned int)draggingSourceOperationMaskForLocal:(BOOL)isLocal +{ + return NSDragOperationCopy; +} + +//only have the copy menu item enabled when row(s) are selected +- (BOOL)validateMenuItem:(NSMenuItem*)anItem +{ + int row = [self selectedRow]; + if ([[anItem title] isEqualToString:@"Copy"] ) + { + if (row < 0 ) + { + return NO; + } + } + return YES; +} + +//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 *)selectedRowsAsTabString +{ + if ( [self numberOfSelectedRows] > 0 ) + { + NSArray *columns = [self tableColumns]; + int numColumns = [columns count]; + id dataSource = [self dataSource]; + + NSMutableString *result = [NSMutableString stringWithCapacity:numColumns]; + + //this is really deprecated in 10.3, but the new method is really weird + NSEnumerator *enumerator = [self selectedRowEnumerator]; + + int c; + id row = nil; + id rowData = nil; + NSTableColumn *col = nil; + + while (row = [enumerator nextObject]) + { + rowData = nil; + for ( c = 0; c < numColumns; c++) + { + col = [columns objectAtIndex:c]; + rowData = [dataSource tableView:self + objectValueForTableColumn:col + row:[row intValue] ]; + + if ( nil != rowData ) + { + [result appendString:[NSString stringWithFormat:@"%@\t", [rowData description] ] ]; + } + else + { + [result appendString:@"\t"]; + } + } //end for each column + + if ( [result length] ) + { + [result deleteCharactersInRange:NSMakeRange([result length]-1, 1)]; + } + [result appendString: [ NSString stringWithFormat:@"\n"]]; + } //end for each row + + if ( [result length] ) + { + [result deleteCharactersInRange:NSMakeRange([result length]-1, 1)]; + } + return result; + } + else + { + return nil; + } +} + +//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 *)rows +{ + if ( [rows count] > 0 ) + { + NSArray *columns = [self tableColumns]; + int numColumns = [columns count]; + id dataSource = [self dataSource]; + + NSMutableString *result = [NSMutableString stringWithCapacity:numColumns]; + + //this is really deprecated in 10.3, but the new method is really weird + NSEnumerator *enumerator = [rows objectEnumerator]; + + int c; + id row = nil; + id rowData = nil; + NSTableColumn *col = nil; + + while (row = [enumerator nextObject]) + { + rowData = nil; + for ( c = 0; c < numColumns; c++) + { + col = [columns objectAtIndex:c]; + rowData = [dataSource tableView:self + objectValueForTableColumn:col + row:[row intValue] ]; + + if ( nil != rowData ) + { + [result appendString:[NSString stringWithFormat:@"%@\t", [rowData description] ] ]; + } + else + { + [result appendString:@"\t"]; + } + } //end for each column + + if ( [result length] ) + { + [result deleteCharactersInRange:NSMakeRange([result length]-1, 1)]; + } + [result appendString: [ NSString stringWithFormat:@"\n"]]; + } //end for each row + + if ( [result length] ) + { + [result deleteCharactersInRange:NSMakeRange([result length]-1, 1)]; + } + return result; + } + else + { + return nil; + } +} + +@end diff --git a/Source/CMImageView.h b/Source/CMImageView.h new file mode 100644 index 00000000..f52e8e5b --- /dev/null +++ b/Source/CMImageView.h @@ -0,0 +1,36 @@ +// +// CMImageView.h +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Sat Sep 06 2003. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import <Cocoa/Cocoa.h> + + +@interface CMImageView : NSImageView { + + NSString *draggedFilePath; + +} + +- (NSString *)draggedFilePath; + +@end diff --git a/Source/CMImageView.m b/Source/CMImageView.m new file mode 100644 index 00000000..0eb4c250 --- /dev/null +++ b/Source/CMImageView.m @@ -0,0 +1,48 @@ +// +// CMImageView.m +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Sat Sep 06 2003. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import "CMImageView.h" + + +@implementation CMImageView + +- (NSString *)draggedFilePath +/* +returns the path of the dragged file +*/ +{ + return [NSString stringWithString:draggedFilePath]; +} + +- (void)concludeDragOperation:(id <NSDraggingInfo>)sender +{ + if ( draggedFilePath ) + [draggedFilePath release]; + + draggedFilePath = [[NSString stringWithString:[[[sender draggingPasteboard] propertyListForType:@"NSFilenamesPboardType"] objectAtIndex:0]] retain]; + + [super concludeDragOperation:sender]; +} + +@end diff --git a/Source/CMMCPConnection.h b/Source/CMMCPConnection.h new file mode 100644 index 00000000..d564d96b --- /dev/null +++ b/Source/CMMCPConnection.h @@ -0,0 +1,37 @@ +// +// CMMCPConnection.h +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Wed Sept 21 2005. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import <Cocoa/Cocoa.h> +#import <MCPKit_bundled/MCPKit_bundled.h> +#import "CMMCPResult.h" + +@interface CMMCPConnection : MCPConnection { + id delegate; +} + +- (CMMCPResult *)queryString:(NSString *) query; +- (void)setDelegate:(id)object; +- (NSTimeZone *)timeZone; + +@end diff --git a/Source/CMMCPConnection.m b/Source/CMMCPConnection.m new file mode 100644 index 00000000..8d4e2673 --- /dev/null +++ b/Source/CMMCPConnection.m @@ -0,0 +1,207 @@ +// +// CMMCPConnection.m +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Wed Sept 21 2005. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import "CMMCPConnection.h" + + +@implementation CMMCPConnection + +/* +Gets a proper NSStringEncoding according to the given MySQL charset. + +MySQL 4.0 offers this charsets: +big5 cp1251 cp1257 croat czech danish dec8 dos estonia euc_kr gb2312 gbk german1 greek hebrew hp8 hungarian koi8_ru koi8_ukr latin1 latin1_de latin2 latin5 sjis swe7 tis620 ujis usa7 win1250 win1251ukr + +WARNING : incomplete implementation. Please, send your fixes. + ++ (NSStringEncoding) encodingForMySQLEncoding:(const char *) mysqlEncoding +{ + // unicode + if (!strncmp(mysqlEncoding, "utf8", 4)) { + return NSUTF8StringEncoding; + } + if (!strncmp(mysqlEncoding, "ucs2", 4)) { + return NSUnicodeStringEncoding; + } + // west european + if (!strncmp(mysqlEncoding, "ascii", 5)) { + return NSASCIIStringEncoding; + } + if (!strncmp(mysqlEncoding, "latin1", 6)) { + return NSISOLatin1StringEncoding; + } + if (!strncmp(mysqlEncoding, "macroman", 8)) { + return NSMacOSRomanStringEncoding; + } + // central european + if (!strncmp(mysqlEncoding, "cp1250", 6)) { + return NSWindowsCP1250StringEncoding; + } + if (!strncmp(mysqlEncoding, "latin2", 6)) { + return NSISOLatin2StringEncoding; + } + // south european and middle east + if (!strncmp(mysqlEncoding, "cp1256", 6)) { + return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingWindowsArabic); + } + if (!strncmp(mysqlEncoding, "greek", 5)) { + return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatinGreek); + } + if (!strncmp(mysqlEncoding, "hebrew", 6)) { + CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatinHebrew); + } + if (!strncmp(mysqlEncoding, "latin5", 6)) { + return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatin5); + } + // baltic + if (!strncmp(mysqlEncoding, "cp1257", 6)) { + return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingWindowsBalticRim); + } + // cyrillic + if (!strncmp(mysqlEncoding, "cp1251", 6)) { + return NSWindowsCP1251StringEncoding; + } + // asian + if (!strncmp(mysqlEncoding, "big5", 4)) { + return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingBig5); + } + if (!strncmp(mysqlEncoding, "ujis", 4)) { + return NSJapaneseEUCStringEncoding; + } + if (!strncmp(mysqlEncoding, "sjis", 4)) { + return NSShiftJISStringEncoding; + } + + // default to iso latin 1, even if it is not exact (throw an exception?) + NSLog(@"warning: unknown encoding %s! falling back to latin1.", mysqlEncoding); + return NSISOLatin1StringEncoding; +} +*/ + + +/* + modified version of queryString to be used in sequel-pro + */ +- (CMMCPResult *)queryString:(NSString *) query +{ + CMMCPResult *theResult; + const char *theCQuery = [self cStringFromString:query]; + int theQueryCode; + + // check connection + if (![self checkConnection]) { + NSLog(@"Connection was gone, but should be reestablished now!"); + } + + // inform the delegate about the query + if (delegate && [delegate respondsToSelector:@selector(willQueryString:)]) { + [delegate willQueryString:query]; + } + + if (0 == (theQueryCode = mysql_query(mConnection, theCQuery))) { + if (mysql_field_count(mConnection) != 0) { + // use CMMCPResult instad of MCPResult + theResult = [[CMMCPResult alloc] initWithMySQLPtr:mConnection encoding:mEncoding timeZone:mTimeZone]; + } else { + return nil; + } + } else { +// NSLog (@"Problem in queryString error code is : %d, query is : %s -in ObjC : %@-\n", theQueryCode, theCQuery, query); +// NSLog(@"Error message is : %@\n", [self getLastErrorMessage]); +// theResult = [theResult init]; // Old version... +// theResult = nil; + + // inform the delegate about errors + if (delegate && [delegate respondsToSelector:@selector(queryGaveError:)]) { + [delegate queryGaveError:[self getLastErrorMessage]]; + } + + return nil; + } + return [theResult autorelease]; +} + +- (void)setDelegate:(id)object +{ + delegate = object; +} + +/* Getting the currently used time zone (in communication with the DB server). */ +/* fixes mysql 4.1.14 problem, can be deleted as soon as fixed in the framework */ +- (NSTimeZone *)timeZone +{ + if ([self checkConnection]) { + MCPResult *theSessionTZ = [self queryString:@"SHOW VARIABLES LIKE '%time_zone'"]; + NSArray *theRow; + id theTZName; + NSTimeZone *theTZ; + + [theSessionTZ dataSeek:1ULL]; + theRow = [theSessionTZ fetchRowAsArray]; + theTZName = [theRow objectAtIndex:1]; + + if ( [theTZName isKindOfClass:[NSData class]] ) { + // MySQL 4.1.14 returns the mysql variables as NSData + theTZName = [self stringWithText:theTZName]; + } + + if ([theTZName isEqualToString:@"SYSTEM"]) { + [theSessionTZ dataSeek:0ULL]; + theRow = [theSessionTZ fetchRowAsArray]; + theTZName = [theRow objectAtIndex:1]; + + if ( [theTZName isKindOfClass:[NSData class]] ) { + // MySQL 4.1.14 returns the mysql variables as NSData + theTZName = [self stringWithText:theTZName]; + } + } + + if (theTZName) { // Old versions of the server does not support there own time zone ? + theTZ = [NSTimeZone timeZoneWithName:theTZName]; + } else { + // By default set the time zone to the local one.. + // Try to get the name using the previously available variable: + theSessionTZ = [self queryString:@"SHOW VARIABLES LIKE 'timezone'"]; + [theSessionTZ dataSeek:0ULL]; + theRow = [theSessionTZ fetchRowAsArray]; + theTZName = [theRow objectAtIndex:1]; + if (theTZName) { + // Finally we found one ... + theTZ = [NSTimeZone timeZoneWithName:theTZName]; + } else { + theTZ = [NSTimeZone defaultTimeZone]; + //theTZ = [NSTimeZone systemTimeZone]; + NSLog(@"The time zone is not defined on the server, set it to the default one : %@", theTZ); + } + } + + if (theTZ != mTimeZone) { + [mTimeZone release]; + mTimeZone = [theTZ retain]; + } + } + return mTimeZone; +} + +@end diff --git a/Source/CMMCPResult.h b/Source/CMMCPResult.h new file mode 100644 index 00000000..7f527949 --- /dev/null +++ b/Source/CMMCPResult.h @@ -0,0 +1,35 @@ +// +// CMMCPResult.h +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Wed Sept 21 2005. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import <Cocoa/Cocoa.h> +#import <MCPKit_bundled/MCPKit_bundled.h> + + +@interface CMMCPResult : MCPResult { + +} + +- (id)fetchRowAsType:(MCPReturnType)aType; + +@end diff --git a/Source/CMMCPResult.m b/Source/CMMCPResult.m new file mode 100644 index 00000000..f80ff5c3 --- /dev/null +++ b/Source/CMMCPResult.m @@ -0,0 +1,147 @@ +// +// CMMCPResult.m +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Wed Sept 21 2005. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import "CMMCPResult.h" + + +@implementation CMMCPResult + +/* +modified version for use with sequel-pro +*/ +- (id)fetchRowAsType:(MCPReturnType)aType +{ + MYSQL_ROW theRow; + unsigned long *theLengths; + MYSQL_FIELD *theField; + int i; + id theReturn; + + if (mResult == NULL) { + // If there is no results, returns nil, as after the last row... + return nil; + } + + theRow = mysql_fetch_row(mResult); + if (theRow == NULL) { + return nil; + } + + switch (aType) { + case MCPTypeArray: + theReturn = [NSMutableArray arrayWithCapacity:mNumOfFields]; + break; + case MCPTypeDictionary: + if (mNames == nil) { + [self fetchFieldNames]; + } + theReturn = [NSMutableDictionary dictionaryWithCapacity:mNumOfFields]; + break; + default : + NSLog (@"Unknown type : %d, will return an Array!\n", aType); + theReturn = [NSMutableArray arrayWithCapacity:mNumOfFields]; + break; + } + + theLengths = mysql_fetch_lengths(mResult); + theField = mysql_fetch_fields(mResult); + + for (i=0; i<mNumOfFields; i++) { + id theCurrentObj; + + if (theRow[i] == NULL) { + theCurrentObj = [NSNull null]; + } else { + char *theData = calloc(sizeof(char),theLengths[i]+1); + //char *theUselLess; + memcpy(theData, theRow[i],theLengths[i]); + theData[theLengths[i]] = '\0'; + + switch (theField[i].type) { + case FIELD_TYPE_TINY: + case FIELD_TYPE_SHORT: + case FIELD_TYPE_INT24: + case FIELD_TYPE_LONG: + case FIELD_TYPE_LONGLONG: + case FIELD_TYPE_DECIMAL: + case FIELD_TYPE_FLOAT: + case FIELD_TYPE_DOUBLE: + case FIELD_TYPE_NEW_DECIMAL: + case FIELD_TYPE_TIMESTAMP: + case FIELD_TYPE_DATE: + case FIELD_TYPE_TIME: + case FIELD_TYPE_DATETIME: + case FIELD_TYPE_YEAR: + case FIELD_TYPE_VAR_STRING: + case FIELD_TYPE_STRING: + case FIELD_TYPE_SET: + case FIELD_TYPE_ENUM: + case FIELD_TYPE_NEWDATE: // Don't know what the format for this type is... + theCurrentObj = [self stringWithCString:theData]; + break; + + case FIELD_TYPE_TINY_BLOB: + case FIELD_TYPE_BLOB: + case FIELD_TYPE_MEDIUM_BLOB: + case FIELD_TYPE_LONG_BLOB: + theCurrentObj = [NSData dataWithBytes:theData length:theLengths[i]]; + if (!(theField[i].flags & BINARY_FLAG)) { // It is TEXT and NOT BLOB... + theCurrentObj = [self stringWithText:theCurrentObj]; + } // #warning Should check for TEXT (using theField[i].flag BINARY_FLAG) + break; + + case FIELD_TYPE_NULL: + theCurrentObj = [NSNull null]; + break; + + default: + NSLog (@"in fetchRowAsType : Unknown type : %d for column %d, send back a NSData object", (int)theField[i].type, (int)i); + theCurrentObj = [NSData dataWithBytes:theData length:theLengths[i]]; + break; + } + + free(theData); + + // Some of the creators return nil object... + if (theCurrentObj == nil) { + theCurrentObj = [NSNull null]; + } + } + + switch (aType) { + case MCPTypeDictionary : + [theReturn setObject:theCurrentObj forKey:[mNames objectAtIndex:i]]; + break; + + case MCPTypeArray : + default : + [theReturn addObject:theCurrentObj]; + break; + } + } + + return theReturn; +} + +@end diff --git a/Source/CMTextView.h b/Source/CMTextView.h new file mode 100644 index 00000000..e70c6ea4 --- /dev/null +++ b/Source/CMTextView.h @@ -0,0 +1,32 @@ +// +// CMTextView.h +// sequel-pro +// +// Created by Carsten BlŸm. +// +// 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/> +// Or mail to <lorenz@textor.ch> + +#import <Cocoa/Cocoa.h> + +@interface CMTextView : NSTextView { +} + +-(NSArray *)completionsForPartialWordRange:(NSRange)charRange indexOfSelectedItem:(int *)index; +-(NSArray *)keywords; + +@end diff --git a/Source/CMTextView.m b/Source/CMTextView.m new file mode 100644 index 00000000..0542ffa8 --- /dev/null +++ b/Source/CMTextView.m @@ -0,0 +1,296 @@ +// +// CMTextView.m +// sequel-pro +// +// Created by Carsten BlŸm. +// +// 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/> +// Or mail to <lorenz@textor.ch> + +#import "CMTextView.h" + +@implementation CMTextView + +- (NSArray *)completionsForPartialWordRange:(NSRange)charRange indexOfSelectedItem:(int *)index +{ + + NSString *partialString = [[self string] substringWithRange:charRange]; + unsigned int partialLength = [partialString length]; + unsigned int options = NSCaseInsensitiveSearch | NSAnchoredSearch; + unsigned int i; + NSRange partialRange = NSMakeRange(0, partialLength); + NSMutableArray *compl = [[NSMutableArray alloc] initWithCapacity:32]; + NSArray *keywords = [self keywords]; + + // Get the document +// id tableDocument = [[[self window] windowController] document]; + id tableDocument = [[self window] delegate]; + +//NSLog(@"doc: %@", [[[self window] windowController] document]); + + // Get an array of table names for the current database + id tableNames = [[tableDocument valueForKeyPath:@"tablesListInstance"] valueForKey:@"tables"]; + + // Add matching table names to compl + for (i = 0; i < [tableNames count]; i ++) + { + if ([[tableNames objectAtIndex:i] length] > partialLength) + { + NSRange range = [[tableNames objectAtIndex:i] rangeOfString:partialString + options:options + range:partialRange]; + if (range.location != NSNotFound) + { + [compl addObject:[tableNames objectAtIndex:i]]; + } + } + + } + + // Add matching keywords to compl + for (i = 0; i < [keywords count]; i ++) + { + if ([[keywords objectAtIndex:i] length] > partialLength) + { + NSRange range = [[keywords objectAtIndex:i] rangeOfString:partialString + options:options + range:partialRange]; + if (range.location != NSNotFound) + { + [compl addObject:[keywords objectAtIndex:i]]; + } + } + } + + return [compl autorelease]; +} + + + +-(NSArray *)keywords { + return [NSArray arrayWithObjects: + @"ADD", + @"ALL", + @"ALTER", + @"ANALYZE", + @"AND", + @"ASC", + @"ASENSITIVE", + @"BEFORE", + @"BETWEEN", + @"BIGINT", + @"BINARY", + @"BLOB", + @"BOTH", + @"CALL", + @"CASCADE", + @"CASE", + @"CHANGE", + @"CHAR", + @"CHARACTER", + @"CHECK", + @"COLLATE", + @"COLUMN", + @"COLUMNS", + @"CONDITION", + @"CONNECTION", + @"CONSTRAINT", + @"CONTINUE", + @"CONVERT", + @"CREATE", + @"CROSS", + @"CURRENT_DATE", + @"CURRENT_TIME", + @"CURRENT_TIMESTAMP", + @"CURRENT_USER", + @"CURSOR", + @"DATABASE", + @"DATABASES", + @"DAY_HOUR", + @"DAY_MICROSECOND", + @"DAY_MINUTE", + @"DAY_SECOND", + @"DEC", + @"DECIMAL", + @"DECLARE", + @"DEFAULT", + @"DELAYED", + @"DELETE", + @"DESC", + @"DESCRIBE", + @"DETERMINISTIC", + @"DISTINCT", + @"DISTINCTROW", + @"DIV", + @"DOUBLE", + @"DROP", + @"DUAL", + @"EACH", + @"ELSE", + @"ELSEIF", + @"ENCLOSED", + @"ESCAPED", + @"EXISTS", + @"EXIT", + @"EXPLAIN", + @"FALSE", + @"FETCH", + @"FIELDS", + @"FLOAT", + @"FOR", + @"FORCE", + @"FOREIGN", + @"FOUND", + @"FROM", + @"FULLTEXT", + @"GOTO", + @"GRANT", + @"GROUP", + @"HAVING", + @"HIGH_PRIORITY", + @"HOUR_MICROSECOND", + @"HOUR_MINUTE", + @"HOUR_SECOND", + @"IGNORE", + @"INDEX", + @"INFILE", + @"INNER", + @"INOUT", + @"INSENSITIVE", + @"INSERT", + @"INT", + @"INTEGER", + @"INTERVAL", + @"INTO", + @"ITERATE", + @"JOIN", + @"KEY", + @"KEYS", + @"KILL", + @"LEADING", + @"LEAVE", + @"LEFT", + @"LIKE", + @"LIMIT", + @"LINES", + @"LOAD", + @"LOCALTIME", + @"LOCALTIMESTAMP", + @"LOCK", + @"LONG", + @"LONGBLOB", + @"LONGTEXT", + @"LOOP", + @"LOW_PRIORITY", + @"MATCH", + @"MEDIUMBLOB", + @"MEDIUMINT", + @"MEDIUMTEXT", + @"MIDDLEINT", + @"MINUTE_MICROSECOND", + @"MINUTE_SECOND", + @"MOD", + @"NATURAL", + @"NOT", + @"NO_WRITE_TO_BINLOG", + @"NULL", + @"NUMERIC", + @"ON", + @"OPTIMIZE", + @"OPTION", + @"OPTIONALLY", + @"ORDER", + @"OUT", + @"OUTER", + @"OUTFILE", + @"PRECISION", + @"PRIMARY", + @"PRIVILEGES", + @"PROCEDURE", + @"PURGE", + @"READ", + @"REAL", + @"REFERENCES", + @"REGEXP", + @"RENAME", + @"REPEAT", + @"REPLACE", + @"REQUIRE", + @"RESTRICT", + @"RETURN", + @"REVOKE", + @"RIGHT", + @"RLIKE", + @"SECOND_MICROSECOND", + @"SELECT", + @"SENSITIVE", + @"SEPARATOR", + @"SET", + @"SHOW", + @"SMALLINT", + @"SONAME", + @"SPATIAL", + @"SPECIFIC", + @"SQL", + @"SQLEXCEPTION", + @"SQLSTATE", + @"SQLWARNING", + @"SQL_BIG_RESULT", + @"SQL_CALC_FOUND_ROWS", + @"SQL_SMALL_RESULT", + @"SSL", + @"STARTING", + @"STRAIGHT_JOIN", + @"TABLE", + @"TABLES", + @"TERMINATED", + @"THEN", + @"TINYBLOB", + @"TINYINT", + @"TINYTEXT", + @"TRAILING", + @"TRIGGER", + @"TRUE", + @"UNDO", + @"UNION", + @"UNIQUE", + @"UNLOCK", + @"UNSIGNED", + @"UPDATE", + @"USAGE", + @"USE", + @"USING", + @"UTC_DATE", + @"UTC_TIME", + @"UTC_TIMESTAMP", + @"VALUES", + @"VARBINARY", + @"VARCHAR", + @"VARCHARACTER", + @"VARYING", + @"WHEN", + @"WHERE", + @"WHILE", + @"WITH", + @"WRITE", + @"XOR", + @"YEAR_MONTH", + @"ZEROFILL", + nil]; +} + +@end diff --git a/Source/CustomQuery.h b/Source/CustomQuery.h new file mode 100644 index 00000000..e873d833 --- /dev/null +++ b/Source/CustomQuery.h @@ -0,0 +1,105 @@ +// +// CustomQuery.h +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Wed May 01 2002. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import <Cocoa/Cocoa.h> +#import <MCPKit_bundled/MCPKit_bundled.h> +#import "CMCopyTable.h" +#import "CMMCPConnection.h" +#import "CMMCPResult.h" + + +@interface CustomQuery : NSObject { + + IBOutlet id tableDumpInstance; + + IBOutlet id tableWindow; + IBOutlet id queryFavoritesButton; + IBOutlet id queryHistoryButton; + IBOutlet id textView; + IBOutlet CMCopyTable *customQueryView; + IBOutlet id errorText; + IBOutlet id affectedRowsText; + IBOutlet id valueSheet; + IBOutlet id valueTextField; + IBOutlet id queryFavoritesSheet; + IBOutlet id queryFavoritesView; + + CMMCPConnection *mySQLConnection; + NSArray *queryResult; + NSUserDefaults *prefs; + NSMutableArray *queryFavorites; +} + +//IBAction methods +- (IBAction)performQuery:(id)sender; +- (IBAction)chooseQueryFavorite:(id)sender; +- (IBAction)chooseQueryHistory:(id)sender; +- (IBAction)closeSheet:(id)sender; + +//queryFavoritesSheet methods +- (IBAction)addQueryFavorite:(id)sender; +- (IBAction)removeQueryFavorite:(id)sender; +- (IBAction)copyQueryFavorite:(id)sender; +- (IBAction)closeQueryFavoritesSheet:(id)sender; + +//getter methods +- (NSArray *)currentResult; + +//additional methods +- (void)setConnection:(CMMCPConnection *)theConnection; +- (void)setFavorites; +- (void)doPerformQueryService:(NSString *)query; + +//tableView datasource methods +- (int)numberOfRowsInTableView:(NSTableView *)aTableView; +- (id)tableView:(NSTableView *)aTableView + objectValueForTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex; +- (void)tableView:(NSTableView *)aTableView + setObjectValue:(id)anObject + forTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex; + +//tableView drag&drop datasource methods +- (BOOL)tableView:(NSTableView *)aTableView writeRows:(NSArray*)rows toPasteboard:(NSPasteboard*)pboard; +- (NSDragOperation)tableView:(NSTableView*)aTableView validateDrop:(id <NSDraggingInfo>)info proposedRow:(int)row + proposedDropOperation:(NSTableViewDropOperation)operation; +- (BOOL)tableView:(NSTableView*)aTableView acceptDrop:(id <NSDraggingInfo>)info row:(int)row dropOperation:(NSTableViewDropOperation)operation; + +//tableView delegate methods +- (BOOL)tableView:(NSTableView *)aTableView shouldEditTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex; + +//splitView delegate methods +- (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview; +- (float)splitView:(NSSplitView *)sender constrainMaxCoordinate:(float)proposedMax ofSubviewAt:(int)offset; +- (float)splitView:(NSSplitView *)sender constrainMinCoordinate:(float)proposedMin ofSubviewAt:(int)offset; + +//textView delegate methods +- (BOOL)textView:(NSTextView *)aTextView doCommandBySelector:(SEL)aSelector; + +//last but not least +- (id)init; +- (void)dealloc; + +@end diff --git a/Source/CustomQuery.m b/Source/CustomQuery.m new file mode 100644 index 00000000..e5f48e7b --- /dev/null +++ b/Source/CustomQuery.m @@ -0,0 +1,742 @@ +// +// CustomQuery.m +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Wed May 01 2002. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import "CustomQuery.h" +#import "TableDump.h" +#import "SPGrowlController.h" + +@implementation CustomQuery + +//IBAction methods +- (IBAction)performQuery:(id)sender; +/* +performs the mysql-query given by the user +sets the tableView columns corresponding to the mysql-result +*/ +{ + // Fixes bug in key equivalents. + if ([[NSApp currentEvent] type] == NSKeyUp) + { + return; + } + + NSArray *theColumns; + NSTableColumn *theCol; + CMMCPResult *theResult = nil; + NSArray *queries; +// NSArray *theTypes; + NSMutableArray *menuItems = [NSMutableArray array]; + NSMutableArray *tempResult = [NSMutableArray array]; + NSMutableString *errors = [NSMutableString string]; + int i; + + //query started + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; + + //split queries by ;'s + queries = [tableDumpInstance splitQueries:[textView string]]; + +//perform queries + for ( i = 0 ; i < [queries count] ; i++ ) { + theResult = [mySQLConnection queryString:[queries objectAtIndex:i]]; + if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + //query gave error + if ( [queries count] > 1 ) { + [errors appendString:[NSString stringWithFormat:NSLocalizedString(@"[ERROR in query %d] %@\n", @"error text when multiple custom query failed"), + i+1, + [mySQLConnection getLastErrorMessage]]]; + } else { + [errors setString:[mySQLConnection getLastErrorMessage]]; + } + } +// theTypes = [queryResult fetchTypesAsArray]; + } + + //perform empty query if no query is given + if ( [queries count] == 0 ) { + theResult = [mySQLConnection queryString:@""]; + [errors setString:[mySQLConnection getLastErrorMessage]]; + } + +//put result in array + [queryResult release]; + queryResult = nil; + if ( nil != theResult ) + { + int r = [theResult numOfRows]; + if (r) [theResult dataSeek:0]; + for ( i = 0 ; i < r ; i++ ) { + [tempResult addObject:[theResult fetchRowAsArray]]; + } + queryResult = [[NSArray arrayWithArray:tempResult] retain]; + } + +//add query to history + [queryHistoryButton insertItemWithTitle:[textView string] atIndex:1]; + while ( [queryHistoryButton numberOfItems] > 21 ) { + [queryHistoryButton removeItemAtIndex:[queryHistoryButton numberOfItems]-1]; + } + for ( i = 1 ; i < [queryHistoryButton numberOfItems] ; i++ ) + { + [menuItems addObject:[queryHistoryButton itemTitleAtIndex:i]]; + } + [prefs setObject:menuItems forKey:@"queryHistory"]; + +//select the text of the query textView and set standard font + [textView selectAll:self]; + if ( [errors length] ) { + [errorText setStringValue:errors]; + } else { + [errorText setStringValue:NSLocalizedString(@"There were no errors.", @"text shown when query was successfull")]; + } + if ( [mySQLConnection affectedRows] != -1 ) { + [affectedRowsText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%@ row(s) affected", @"text showing how many rows have been affected"), + [[NSNumber numberWithLongLong:[mySQLConnection affectedRows]] stringValue]]]; + } else { + [affectedRowsText setStringValue:@""]; + } + if ( [prefs boolForKey:@"useMonospacedFonts"] ) { + [textView setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]]; + } else { + [textView setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + } + + if ( !theResult || ![theResult numOfRows] ) { +//no rows in result + //free tableView + theColumns = [customQueryView tableColumns]; + while ([theColumns count]) { + [customQueryView removeTableColumn:[theColumns objectAtIndex:0]]; + } +// theCol = [[NSTableColumn alloc] initWithIdentifier:@""]; +// [[theCol headerCell] setStringValue:@""]; +// [customQueryView addTableColumn:theCol]; +// [customQueryView sizeLastColumnToFit]; + [customQueryView reloadData]; +// [theCol release]; + + //query finished + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; + + // Query finished Growl notification + [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Query Finished" + description:[NSString stringWithFormat:NSLocalizedString(@"%@",@"description for query finished growl notification"), [errorText stringValue]] + notificationName:@"Query Finished"]; + + return; + } + +//set columns +//remove all columns + theColumns = [customQueryView tableColumns]; +// i=0; + while ([theColumns count]) { + [customQueryView removeTableColumn:[theColumns objectAtIndex:0]]; +// i++; + } + +//add columns, corresponding to the query result + theColumns = [theResult fetchFieldNames]; + for ( i = 0 ; i < [theResult numOfFields] ; i++) { + theCol = [[NSTableColumn alloc] initWithIdentifier:[NSNumber numberWithInt:i]]; + [theCol setResizingMask:NSTableColumnUserResizingMask]; + NSTextFieldCell *dataCell = [[[NSTextFieldCell alloc] initTextCell:@""] autorelease]; + [dataCell setEditable:NO]; + if ( [prefs boolForKey:@"useMonospacedFonts"] ) { + [dataCell setFont:[NSFont fontWithName:@"Monaco" size:10]]; + } else { + [dataCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + } + [dataCell setLineBreakMode:NSLineBreakByTruncatingTail]; + [theCol setDataCell:dataCell]; + [[theCol headerCell] setStringValue:[theColumns objectAtIndex:i]]; + + [customQueryView addTableColumn:theCol]; + [theCol release]; + } + + [customQueryView sizeLastColumnToFit]; + //tries to fix problem with last row (otherwise to small) + //sets last column to width of the first if smaller than 30 + //problem not fixed for resizing window + if ( [[customQueryView tableColumnWithIdentifier:[NSNumber numberWithInt:[theColumns count]-1]] width] < 30 ) + [[customQueryView tableColumnWithIdentifier:[NSNumber numberWithInt:[theColumns count]-1]] + setWidth:[[customQueryView tableColumnWithIdentifier:[NSNumber numberWithInt:0]] width]]; + [customQueryView reloadData]; + + //query finished + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; + + // Query finished Growl notification + [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Query Finished" + description:[NSString stringWithFormat:NSLocalizedString(@"%@",@"description for query finished growl notification"), [errorText stringValue]] + notificationName:@"Query Finished"]; +} + +- (IBAction)chooseQueryFavorite:(id)sender +/* +insert the choosen favorite query in the query textView or save query to favorites or opens window to edit favorites +*/ +{ + if ( [queryFavoritesButton indexOfSelectedItem] == 1) { +//save query to favorites + //check if favorite doesn't exist + NSEnumerator *enumerator = [queryFavorites objectEnumerator]; + id favorite; + while ( (favorite = [enumerator nextObject]) ) { + if ( [favorite isEqualToString:[textView string]] ) { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + NSLocalizedString(@"Query already exists in favorites.", @"message of panel when trying to save query which already exists in favorites")); + return; + } + } + if ( [[textView string] isEqualToString:@""] ) { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + NSLocalizedString(@"Query can't be empty.", @"message of panel when trying to save empty query")); + return; + } + [queryFavorites addObject:[NSString stringWithString:[textView string]]]; + [queryFavoritesView reloadData]; + [prefs setObject:queryFavorites forKey:@"queryFavorites"]; + [self setFavorites]; + } else if ( [queryFavoritesButton indexOfSelectedItem] == 2) { +//edit favorites + [NSApp beginSheet:queryFavoritesSheet + modalForWindow:tableWindow modalDelegate:self + didEndSelector:nil contextInfo:nil]; + [NSApp runModalForWindow:queryFavoritesSheet]; + + [NSApp endSheet:queryFavoritesSheet]; + [queryFavoritesSheet orderOut:nil]; + } else if ( [queryFavoritesButton indexOfSelectedItem] != 3) { +//choose favorite + [textView replaceCharactersInRange:[textView selectedRange] withString:[queryFavoritesButton titleOfSelectedItem]]; + } +} + +- (IBAction)chooseQueryHistory:(id)sender +/* +insert the choosen history query in the query textView +*/ +{ + [textView setString:[queryHistoryButton titleOfSelectedItem]]; + [textView selectAll:self]; +} + +- (IBAction)closeSheet:(id)sender +/* +closes the sheet +*/ +{ + [NSApp stopModal]; +} + + +//queryFavoritesSheet methods +- (IBAction)addQueryFavorite:(id)sender +/* +adds a query favorite +*/ +{ + int row = [queryFavoritesView editedRow]; + int column = [queryFavoritesView editedColumn]; + NSTableColumn *tableColumn; + NSCell *cell; + +//end editing + if ( row != -1 ) { + tableColumn = [[queryFavoritesView tableColumns] objectAtIndex:column]; + cell = [tableColumn dataCellForRow:row]; + [cell endEditing:[queryFavoritesView currentEditor]]; + } + + [queryFavorites addObject:[NSString string]]; + [queryFavoritesView reloadData]; + [queryFavoritesView selectRow:[queryFavoritesView numberOfRows]-1 byExtendingSelection:NO]; + [queryFavoritesView editColumn:0 row:[queryFavoritesView numberOfRows]-1 withEvent:nil select:YES]; +} + +- (IBAction)removeQueryFavorite:(id)sender +/* +removes a query favorite +*/ +{ + int row = [queryFavoritesView editedRow]; + int column = [queryFavoritesView editedColumn]; + NSTableColumn *tableColumn; + NSCell *cell; + +//end editing + if ( row != -1 ) { + tableColumn = [[queryFavoritesView tableColumns] objectAtIndex:column]; + cell = [tableColumn dataCellForRow:row]; + [cell endEditing:[queryFavoritesView currentEditor]]; + } + + if ( [queryFavoritesView numberOfSelectedRows] > 0 ) { + [queryFavorites removeObjectAtIndex:[queryFavoritesView selectedRow]]; + [queryFavoritesView reloadData]; + } +} + +- (IBAction)copyQueryFavorite:(id)sender +/* +copies a query favorite +*/ +{ + int row = [queryFavoritesView editedRow]; + int column = [queryFavoritesView editedColumn]; + NSTableColumn *tableColumn; + NSCell *cell; + +//end editing + if ( row != -1 ) { + tableColumn = [[queryFavoritesView tableColumns] objectAtIndex:column]; + cell = [tableColumn dataCellForRow:row]; + [cell endEditing:[queryFavoritesView currentEditor]]; + } + + if ( [queryFavoritesView numberOfSelectedRows] > 0 ) { + [queryFavorites insertObject: + [NSString stringWithString:[queryFavorites objectAtIndex:[queryFavoritesView selectedRow]]] + atIndex:[queryFavoritesView selectedRow]+1]; + [queryFavoritesView reloadData]; + [queryFavoritesView selectRow:[queryFavoritesView selectedRow]+1 byExtendingSelection:NO]; + [queryFavoritesView editColumn:0 row:[queryFavoritesView selectedRow] withEvent:nil select:YES]; + } +} + +- (IBAction)closeQueryFavoritesSheet:(id)sender +/* +closes queryFavoritesSheet and saves favorites to preferences +*/ +{ + int row = [queryFavoritesView editedRow]; + int column = [queryFavoritesView editedColumn]; + NSTableColumn *tableColumn; + NSCell *cell; + +//end editing + if ( row != -1 ) { + tableColumn = [[queryFavoritesView tableColumns] objectAtIndex:column]; + cell = [tableColumn dataCellForRow:row]; + [cell endEditing:[queryFavoritesView currentEditor]]; + } + + [NSApp stopModal]; + [prefs setObject:queryFavorites forKey:@"queryFavorites"]; + [self setFavorites]; +} + + +//getter methods +- (NSArray *)currentResult +/* +returns the current result (as shown in custom result view) as array, the first object containing the field names as array, the following objects containing the rows as array +*/ +{ + NSArray *tableColumns = [customQueryView tableColumns]; + NSEnumerator *enumerator = [tableColumns objectEnumerator]; + id tableColumn; + NSMutableArray *currentResult = [NSMutableArray array]; + NSMutableArray *tempRow = [NSMutableArray array]; + int i; + + //set field names as first line + while ( (tableColumn = [enumerator nextObject]) ) { + [tempRow addObject:[[tableColumn headerCell] stringValue]]; + } + [currentResult addObject:[NSArray arrayWithArray:tempRow]]; + + //add rows + for ( i = 0 ; i < [self numberOfRowsInTableView:customQueryView] ; i++) { + [tempRow removeAllObjects]; + enumerator = [tableColumns objectEnumerator]; + while ( (tableColumn = [enumerator nextObject]) ) { + [tempRow addObject:[self tableView:customQueryView objectValueForTableColumn:tableColumn row:i]]; + } + [currentResult addObject:[NSArray arrayWithArray:tempRow]]; + } + return currentResult; +} + + +//additional methods +- (void)setConnection:(CMMCPConnection *)theConnection +/* +sets the connection (received from TableDocument) and makes things that have to be done only once +*/ +{ + NSArray *tableColumns = [queryFavoritesView tableColumns]; + NSEnumerator *enumerator = [tableColumns objectEnumerator]; + id column; + + mySQLConnection = theConnection; + + prefs = [[NSUserDefaults standardUserDefaults] retain]; + if ( [prefs objectForKey:@"queryFavorites"] ) { + queryFavorites = [[NSMutableArray alloc] initWithArray:[prefs objectForKey:@"queryFavorites"]]; + } else { + queryFavorites = [[NSMutableArray array] retain]; + } + +//set up interface + [customQueryView setVerticalMotionCanBeginDrag:NO]; + if ( [prefs boolForKey:@"useMonospacedFonts"] ) { + [textView setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]]; + } else { + [textView setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + } + [textView setContinuousSpellCheckingEnabled:NO]; + [queryFavoritesView registerForDraggedTypes:[NSArray arrayWithObjects:@"SequelProPasteboard", nil]]; + while ( (column = [enumerator nextObject]) ) + { + if ( [prefs boolForKey:@"useMonospacedFonts"] ) { + [[column dataCell] setFont:[NSFont fontWithName:@"Monaco" size:10]]; + } else { + [[column dataCell] setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + } + } + if ( [prefs objectForKey:@"queryHistory"] ) + { + [queryHistoryButton addItemsWithTitles:[prefs objectForKey:@"queryHistory"]]; + } + [self setFavorites]; +} + +- (void)setFavorites +/* +set up the favorites popUpButton +*/ +{ + int i; + +//remove all menuItems and add favorites from preferences + for ( i = 4 ; i < [queryFavoritesButton numberOfItems] ; i++ ) { + [queryFavoritesButton removeItemAtIndex:i]; + } + [queryFavoritesButton addItemsWithTitles:queryFavorites]; +} + +- (void)doPerformQueryService:(NSString *)query +/* +inserts the query in the textView and performs query +*/ +{ + [textView setString:query]; + [self performQuery:self]; +} + + +//tableView datasource methods +- (int)numberOfRowsInTableView:(NSTableView *)aTableView +{ + if ( aTableView == customQueryView ) { + if ( nil == queryResult ) { + return 0; + } else { + return [queryResult count]; + } + } else if ( aTableView == queryFavoritesView ) { + return [queryFavorites count]; + } else { + return 0; + } +} + +- (id)tableView:(NSTableView *)aTableView + objectValueForTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex +{ + NSArray *theRow; + NSNumber *theIdentifier = [aTableColumn identifier]; + + if ( aTableView == customQueryView ) { + theRow = [queryResult objectAtIndex:rowIndex]; + + if ( [[theRow objectAtIndex:[theIdentifier intValue]] isKindOfClass:[NSData class]] ) { + NSString *tmp = [[NSString alloc] initWithData:[theRow objectAtIndex:[theIdentifier intValue]] + encoding:[mySQLConnection encoding]]; + return [tmp autorelease]; + } + if ( [[theRow objectAtIndex:[theIdentifier intValue]] isMemberOfClass:[NSNull class]] ) + return [prefs objectForKey:@"nullValue"]; + + return [theRow objectAtIndex:[theIdentifier intValue]]; + } else if ( aTableView == queryFavoritesView ) { + return [queryFavorites objectAtIndex:rowIndex]; + } else { + return @""; + } +} + +- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject + forTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex +{ + if ( aTableView == queryFavoritesView ) { + NSEnumerator *enumerator = [queryFavorites objectEnumerator]; + id favorite; + int i = 0; + + if ( [anObject isEqualToString:@""] ) { + [queryFavoritesView deselectAll:self]; + [queryFavorites removeObjectAtIndex:rowIndex]; + [queryFavoritesView reloadData]; + return; + } + + while ( (favorite = [enumerator nextObject]) ) { + if ( [favorite isEqualToString:anObject] && i != rowIndex) { + NSRunAlertPanel(@"Error", @"Query already exists in favorites.", @"OK", nil, nil); + //remove row if it was a (blank) new row or a copied row + if ( [[queryFavorites objectAtIndex:rowIndex] isEqualToString:@""] || + [[queryFavorites objectAtIndex:rowIndex] isEqualToString:anObject] ) { + [queryFavoritesView deselectAll:self]; + [queryFavorites removeObjectAtIndex:rowIndex]; + [queryFavoritesView reloadData]; + } + return; + } + i++; + } + [queryFavorites replaceObjectAtIndex:rowIndex withObject:anObject]; + [queryFavoritesView reloadData]; + } +} + + +//tableView drag&drop datasource methods +- (BOOL)tableView:(NSTableView *)aTableView writeRows:(NSArray*)rows toPasteboard:(NSPasteboard*)pboard +{ + int originalRow; + NSArray *pboardTypes; + + if ( aTableView == queryFavoritesView ) + { + if ( [rows count] == 1 ) + { + pboardTypes = [NSArray arrayWithObjects:@"SequelProPasteboard", nil]; + originalRow = [[rows objectAtIndex:0] intValue]; + + [pboard declareTypes:pboardTypes owner:nil]; + [pboard setString:[[NSNumber numberWithInt:originalRow] stringValue] forType:@"SequelProPasteboard"]; + + return YES; + } + else + { + return NO; + } + } else if ( aTableView == customQueryView ) { + NSString *tmp = [customQueryView draggedRowsAsTabString:rows]; + if ( nil != tmp ) + { + [pboard declareTypes:[NSArray arrayWithObjects: NSTabularTextPboardType, + NSStringPboardType, nil] + owner:nil]; + [pboard setString:tmp forType:NSStringPboardType]; + [pboard setString:tmp forType:NSTabularTextPboardType]; + return YES; + } + return NO; + } else { + return NO; + } +} + +- (NSDragOperation)tableView:(NSTableView*)aTableView validateDrop:(id <NSDraggingInfo>)info proposedRow:(int)row + proposedDropOperation:(NSTableViewDropOperation)operation +{ + NSArray *pboardTypes = [[info draggingPasteboard] types]; + int originalRow; + + if ( aTableView == queryFavoritesView ) { + if ([pboardTypes count] == 1 && row != -1) + { + if ([[pboardTypes objectAtIndex:0] isEqualToString:@"SequelProPasteboard"]==YES && operation==NSTableViewDropAbove) + { + originalRow = [[[info draggingPasteboard] stringForType:@"SequelProPasteboard"] intValue]; + + if (row != originalRow && row != (originalRow+1)) + { + return NSDragOperationMove; + } + } + } + return NSDragOperationNone; + } else { + return NSDragOperationNone; + } +} + +- (BOOL)tableView:(NSTableView*)aTableView acceptDrop:(id <NSDraggingInfo>)info row:(int)row dropOperation:(NSTableViewDropOperation)operation +{ + int originalRow; + int destinationRow; + NSMutableDictionary *draggedRow; + + if ( aTableView == queryFavoritesView ) { + originalRow = [[[info draggingPasteboard] stringForType:@"SequelProPasteboard"] intValue]; + destinationRow = row; + + if ( destinationRow > originalRow ) + destinationRow--; + + draggedRow = [queryFavorites objectAtIndex:originalRow]; + [queryFavorites removeObjectAtIndex:originalRow]; + [queryFavorites insertObject:draggedRow atIndex:destinationRow]; + + [queryFavoritesView reloadData]; + [queryFavoritesView selectRow:destinationRow byExtendingSelection:NO]; + + return YES; + } else { + return NO; + } +} + + +//tableView delegate methods +- (BOOL)tableView:(NSTableView *)aTableView shouldEditTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex +/* +opens sheet with value when double clicking on a field +*/ +{ + if ( aTableView == customQueryView ) { + NSArray *theRow; + NSString *theValue; + NSNumber *theIdentifier = [aTableColumn identifier]; + + //get the value + theRow = [queryResult objectAtIndex:rowIndex]; + + if ( [[theRow objectAtIndex:[theIdentifier intValue]] isKindOfClass:[NSData class]] ) { + theValue = [[NSString alloc] initWithData:[theRow objectAtIndex:[theIdentifier intValue]] + encoding:[mySQLConnection encoding]]; + [theValue autorelease]; + } else if ( [[theRow objectAtIndex:[theIdentifier intValue]] isMemberOfClass:[NSNull class]] ) { + theValue = [prefs objectForKey:@"nullValue"]; + } else { + theValue = [theRow objectAtIndex:[theIdentifier intValue]]; + } + + [valueTextField setString:[theValue description]]; + [valueTextField selectAll:self]; + [NSApp beginSheet:valueSheet + modalForWindow:tableWindow modalDelegate:self + didEndSelector:nil contextInfo:nil]; + [NSApp runModalForWindow:valueSheet]; + + [NSApp endSheet:valueSheet]; + [valueSheet orderOut:nil]; + + return NO; + } else { + return YES; + } +} + + +//splitView delegate methods +- (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview +/* +tells the splitView that it can collapse views +*/ +{ + return YES; +} + +- (float)splitView:(NSSplitView *)sender constrainMaxCoordinate:(float)proposedMax ofSubviewAt:(int)offset +/* +defines max position of splitView +*/ +{ + if ( offset == 0 ) { + return proposedMax - 100; + } else { + return proposedMax - 73; + } +} + +- (float)splitView:(NSSplitView *)sender constrainMinCoordinate:(float)proposedMin ofSubviewAt:(int)offset +/* +defines min position of splitView +*/ +{ + if ( offset == 0 ) { + return proposedMin + 100; + } else { + return proposedMin + 100; + } +} + + +//textView delegate methods +- (BOOL)textView:(NSTextView *)aTextView doCommandBySelector:(SEL)aSelector +/* +traps enter key and + performs query instead of inserting a line break if aTextView == textView + closes valueSheet if aTextView == valueTextField +*/ +{ + if ( aTextView == textView ) { + if ( [aTextView methodForSelector:aSelector] == [aTextView methodForSelector:@selector(insertNewline:)] && + [[[NSApp currentEvent] characters] isEqualToString:@"\003"] ) + { + [self performQuery:self]; + return YES; + } else { + return NO; + } + } else if ( aTextView == valueTextField ) { + if ( [aTextView methodForSelector:aSelector] == [aTextView methodForSelector:@selector(insertNewline:)] ) + { + [self closeSheet:self]; + return YES; + } else { + return NO; + } + } + return NO; +} + + +//last but not least +- (id)init; +{ + self = [super init]; + return self; +} + +- (void)dealloc +{ + [queryResult release]; + [prefs release]; + [queryFavorites release]; + + [super dealloc]; +} + + +@end diff --git a/Source/ImageAndTextCell.h b/Source/ImageAndTextCell.h new file mode 100644 index 00000000..bf0ef753 --- /dev/null +++ b/Source/ImageAndTextCell.h @@ -0,0 +1,59 @@ +// +// ImageAndTextCell.h +// +// Copyright © 2006, Apple. All rights reserved. +// + +/* + IMPORTANT: This Apple software is supplied to you by Apple Computer, Inc. ("Apple") in + consideration of your agreement to the following terms, and your use, installation, + modification or redistribution of this Apple software constitutes acceptance of these + terms. If you do not agree with these terms, please do not use, install, modify or + redistribute this Apple software. + + In consideration of your agreement to abide by the following terms, and subject to these + terms, Apple grants you a personal, non-exclusive license, under Apple’s copyrights in + this original Apple software (the "Apple Software"), to use, reproduce, modify and + redistribute the Apple Software, with or without modifications, in source and/or binary + forms; provided that if you redistribute the Apple Software in its entirety and without + modifications, you must retain this notice and the following text and disclaimers in all + such redistributions of the Apple Software. Neither the name, trademarks, service marks + or logos of Apple Computer, Inc. may be used to endorse or promote products derived from + the Apple Software without specific prior written permission from Apple. Except as expressly + stated in this notice, no other rights or licenses, express or implied, are granted by Apple + herein, including but not limited to any patent rights that may be infringed by your + derivative works or by other works in which the Apple Software may be incorporated. + + The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, + EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS + USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. + + IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, + REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND + WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR + OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#import <Cocoa/Cocoa.h> + +#define INDENT_AMOUNT 17 + +@interface ImageAndTextCell : NSTextFieldCell +{ +@private + NSImage *image; + int _indentationLevel; +} + +- (void)setImage:(NSImage *)anImage; +- (NSImage *)image; + +- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView; +- (NSSize)cellSize; + +- (void)setIndentationLevel:(int)level; +- (int)IndentationLevel; +@end diff --git a/Source/ImageAndTextCell.m b/Source/ImageAndTextCell.m new file mode 100644 index 00000000..a8165f85 --- /dev/null +++ b/Source/ImageAndTextCell.m @@ -0,0 +1,135 @@ +/* + ImageAndTextCell.m + Copyright © 2006, Apple Computer, Inc., all rights reserved. + + Subclass of NSTextFieldCell which can display text and an image simultaneously. +*/ + +#import "ImageAndTextCell.h" + +@implementation ImageAndTextCell + +- (void)dealloc { + [image release]; + image = nil; + [super dealloc]; +} + +- copyWithZone:(NSZone *)zone +{ + ImageAndTextCell *cell = (ImageAndTextCell *)[super copyWithZone:zone]; + cell->image = [image retain]; + return cell; +} + +- (void)setImage:(NSImage *)anImage +{ + if (anImage != image) + { + [image release]; + image = [anImage retain]; + } +} + +- (NSImage *)image +{ + return image; +} + +- (NSRect)imageFrameForCellFrame:(NSRect)cellFrame +{ + if (image != nil) + { + NSRect imageFrame; + imageFrame.size = [image size]; + imageFrame.origin = cellFrame.origin; + imageFrame.origin.x += ((1 - MIN(1,INDENT_AMOUNT)) * 3) + (INDENT_AMOUNT * _indentationLevel); + imageFrame.origin.y += ceil((cellFrame.size.height - imageFrame.size.height) / 2); + return imageFrame; + } + else + return NSZeroRect; +} + +- (void)editWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject event:(NSEvent *)theEvent +{ + if (_indentationLevel != 0) { + NSRect indentationFrame; + NSDivideRect(aRect, &indentationFrame, &aRect, (INDENT_AMOUNT * _indentationLevel), NSMinXEdge); + } + + if (image != nil) { + NSRect imageFrame; + NSDivideRect (aRect, &imageFrame, &aRect, 3 + [image size].width, NSMinXEdge); + } + + [super editWithFrame:aRect inView: controlView editor:textObj delegate:anObject event:theEvent]; +} + +- (void)selectWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject start:(int)selStart length:(int)selLength +{ + if (_indentationLevel != 0) { + NSRect indentationFrame; + NSDivideRect(aRect, &indentationFrame, &aRect, (INDENT_AMOUNT * _indentationLevel), NSMinXEdge); + } + + if (image != nil) { + NSRect imageFrame; + NSDivideRect (aRect, &imageFrame, &aRect, 3 + [image size].width, NSMinXEdge); + } + + [super selectWithFrame:aRect inView: controlView editor:textObj delegate:anObject start:selStart length:selLength]; +} + +- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView +{ + if (_indentationLevel != 0) + { + NSRect indentationFrame; + NSDivideRect(cellFrame, &indentationFrame, &cellFrame, (INDENT_AMOUNT * _indentationLevel), NSMinXEdge); + } + + if (image != nil) + { + NSSize imageSize; + NSRect imageFrame; + + imageSize = [image size]; + NSDivideRect(cellFrame, &imageFrame, &cellFrame, 3 + imageSize.width, NSMinXEdge); + if ([self drawsBackground]) + { + [[self backgroundColor] set]; + NSRectFill(imageFrame); + } + imageFrame.origin.x += 3; + imageFrame.size = imageSize; + + if ([controlView isFlipped]) + imageFrame.origin.y += ceil((cellFrame.size.height + imageFrame.size.height) / 2); + else + imageFrame.origin.y += ceil((cellFrame.size.height - imageFrame.size.height) / 2); + + [image compositeToPoint:imageFrame.origin operation:NSCompositeSourceOver]; + } + [super drawWithFrame:cellFrame inView:controlView]; +} + +- (NSSize)cellSize +{ + NSSize cellSize = [super cellSize]; + cellSize.width += (image ? [image size].width : 0) + ((1 - MIN(1,INDENT_AMOUNT)) * 3) + (INDENT_AMOUNT * _indentationLevel); + return cellSize; +} + +- (void)setIndentationLevel:(int)level +{ + _indentationLevel = MAX(0,level); +} + +- (int)IndentationLevel +{ + return _indentationLevel; +} + +@end + diff --git a/Source/KeyChain.h b/Source/KeyChain.h new file mode 100644 index 00000000..8b4a4e1d --- /dev/null +++ b/Source/KeyChain.h @@ -0,0 +1,35 @@ +// +// KeyChain.h +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Wed Dec 25 2002. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import <Foundation/Foundation.h> +#import <Cocoa/Cocoa.h> + +@interface KeyChain : NSObject + +- (void)addPassword:(NSString *)password forName:(NSString *)name account:(NSString *)account; +- (NSString *)getPasswordForName:(NSString *)name account:(NSString *)account; +- (void)deletePasswordForName:(NSString *)name account:(NSString *)account; +- (BOOL)passwordExistsForName:(NSString *)name account:(NSString *)account; + +@end diff --git a/Source/KeyChain.m b/Source/KeyChain.m new file mode 100644 index 00000000..23ead937 --- /dev/null +++ b/Source/KeyChain.m @@ -0,0 +1,161 @@ +// +// KeyChain.m +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Wed Dec 25 2002. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import "KeyChain.h" +#include <CoreFoundation/CoreFoundation.h> +#include <Security/Security.h> + +@implementation KeyChain + +/** + * Add the supplied password to the user's Keychain using the supplied name and account. + */ +- (void)addPassword:(NSString *)password forName:(NSString *)name account:(NSString *)account +{ + OSStatus status; + + // Check if password already exists before adding + if (![self passwordExistsForName:name account:account]) { + status = SecKeychainAddGenericPassword( + NULL, // default keychain + [name cStringLength], // length of service name + [name cString], // service name + [account cStringLength], // length of account name + [account cString], // account name + [password cStringLength], // length of password + [password cString], // pointer to password data + NULL // the item reference + ); + + if (status != noErr) { + NSLog(@"Error (%i) while trying to add password for name: %@ account: %@", status, name, account); + } + } +} + +/** + * Get a password from the user's Keychain for the supplied name and account. + */ +- (NSString *)getPasswordForName:(NSString *)name account:(NSString *)account +{ + OSStatus status; + + void *passwordData = nil; + UInt32 passwordLength = nil; + SecKeychainItemRef itemRef = nil; + NSString *password = @""; + + status = SecKeychainFindGenericPassword( + NULL, // default keychain + [name cStringLength], // length of service name + [name cString], // service name + [account cStringLength], // length of account name + [account cString], // account name + &passwordLength, // length of password + &passwordData, // pointer to password data + &itemRef // the item reference + ); + + if (status == noErr) { + password = [NSString stringWithCString:passwordData length:passwordLength]; + + // Free the data allocated by SecKeychainFindGenericPassword: + status = SecKeychainItemFreeContent( + NULL, // No attribute data to release + passwordData // Release data + ); + } + + return password; +} + +/** + * Delete a password from the user's Keychain for the supplied name and account. + */ +- (void)deletePasswordForName:(NSString *)name account:(NSString *)account +{ + OSStatus status; + SecKeychainItemRef itemRef = nil; + + // Check if password already exists before deleting + if ([self passwordExistsForName:name account:account]) { + status = SecKeychainFindGenericPassword( + NULL, // default keychain + [name cStringLength], // length of service name + [name cString], // service name + [account cStringLength], // length of account name + [account cString], // account name + nil, // length of password + nil, // pointer to password data + &itemRef // the item reference + ); + + if (status == noErr) { + status = SecKeychainItemDelete(itemRef); + + if (status != noErr) { + NSLog(@"Error (%i) while trying to delete password for name: %@ account: %@", status, name, account); + } + } + + CFRelease(itemRef); + } +} + +/** + * Checks the user's Keychain to see if a password for the supplied name and account exists. + */ +- (BOOL)passwordExistsForName:(NSString *)name account:(NSString *)account +{ + SecKeychainItemRef item; + SecKeychainSearchRef search; + int numberOfItemsFound = 0; + + SecKeychainAttributeList list; + SecKeychainAttribute attributes[2]; + + attributes[0].tag = kSecAccountItemAttr; + attributes[0].data = (void *)[account UTF8String]; + attributes[0].length = [account length]; + + attributes[1].tag = kSecLabelItemAttr; + attributes[1].data = (void *)[name UTF8String]; + attributes[1].length = [name length]; + + list.count = 2; + list.attr = attributes; + + if (SecKeychainSearchCreateFromAttributes(NULL, kSecGenericPasswordItemClass, &list, &search) == noErr) { + while (SecKeychainSearchCopyNext(search, &item) == noErr) { + CFRelease(item); + numberOfItemsFound++; + } + } + + CFRelease(search); + + return (numberOfItemsFound > 0); +} + +@end diff --git a/Source/MainController.h b/Source/MainController.h new file mode 100644 index 00000000..b47bb457 --- /dev/null +++ b/Source/MainController.h @@ -0,0 +1,86 @@ +// +// MainController.h +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Wed May 01 2002. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import <Cocoa/Cocoa.h> + +@interface MainController : NSObject +{ + IBOutlet id keyChainInstance; + + IBOutlet id preferencesWindow; + IBOutlet id favoriteSheet; + IBOutlet id reloadAfterAddingSwitch; + IBOutlet id reloadAfterEditingSwitch; + IBOutlet id reloadAfterRemovingSwitch; + IBOutlet id showErrorSwitch; + IBOutlet id dontShowBlobSwitch; + IBOutlet id useMonospacedFontsSwitch; + IBOutlet id fetchRowCountSwitch; + IBOutlet id limitRowsSwitch; + IBOutlet id limitRowsField; + IBOutlet id nullValueField; + IBOutlet id tableView; + IBOutlet id nameField; + IBOutlet id hostField; + IBOutlet id socketField; + IBOutlet id userField; + IBOutlet id passwordField; + IBOutlet id portField; + IBOutlet id databaseField; + IBOutlet id sshCheckbox; + IBOutlet id sshUserField; + IBOutlet id sshPasswordField; + IBOutlet id sshHostField; + IBOutlet id sshPortField; + IBOutlet id encodingPopUpButton; + + NSMutableArray *favorites; + NSUserDefaults *prefs; + + BOOL isNewFavorite; +} + +//IBAction methods +- (IBAction)openPreferences:(id)sender; +- (IBAction)addFavorite:(id)sender; +- (IBAction)removeFavorite:(id)sender; +- (IBAction)copyFavorite:(id)sender; +- (IBAction)chooseLimitRows:(id)sender; +- (IBAction)closeFavoriteSheet:(id)sender; +- (IBAction)toggleUseSSH:(id)sender; + +//services menu methods +- (void)doPerformQueryService:(NSPasteboard *)pboard userData:(NSString *)data error:(NSString **)error; + +//menu methods +- (IBAction)donate:(id)sender; +- (IBAction)visitWebsite:(id)sender; +- (IBAction)visitHelpWebsite:(id)sender; +- (IBAction)checkForUpdates:(id)sender; + +//SSHTunnel methods +- (id)authenticate:(NSScriptCommand *)command; +- (id)handleQuitScriptCommand:(NSScriptCommand *)command; + +@end diff --git a/Source/MainController.m b/Source/MainController.m new file mode 100644 index 00000000..46996ef6 --- /dev/null +++ b/Source/MainController.m @@ -0,0 +1,802 @@ +// +// MainController.m +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Wed May 01 2002. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import "MainController.h" +#import "KeyChain.h" +#import "TableDocument.h" + +@implementation MainController + +/* +opens the preferences window +*/ +- (IBAction)openPreferences:(id)sender +{ + //get favorites if they exist + [favorites release]; + if ( [prefs objectForKey:@"favorites"] != nil ) { + favorites = [[NSMutableArray alloc] initWithArray:[prefs objectForKey:@"favorites"]]; + } else { + favorites = [[NSMutableArray array] retain]; + } + [tableView reloadData]; + + if ( [prefs boolForKey:@"reloadAfterAdding"] ) { + [reloadAfterAddingSwitch setState:NSOnState]; + } else { + [reloadAfterAddingSwitch setState:NSOffState]; + } + if ( [prefs boolForKey:@"reloadAfterEditing"] ) { + [reloadAfterEditingSwitch setState:NSOnState]; + } else { + [reloadAfterEditingSwitch setState:NSOffState]; + } + if ( [prefs boolForKey:@"reloadAfterRemoving"] ) { + [reloadAfterRemovingSwitch setState:NSOnState]; + } else { + [reloadAfterRemovingSwitch setState:NSOffState]; + } + if ( [prefs boolForKey:@"showError"] ) { + [showErrorSwitch setState:NSOnState]; + } else { + [showErrorSwitch setState:NSOffState]; + } + if ( [prefs boolForKey:@"dontShowBlob"] ) { + [dontShowBlobSwitch setState:NSOnState]; + } else { + [dontShowBlobSwitch setState:NSOffState]; + } + if ( [prefs boolForKey:@"limitRows"] ) { + [limitRowsSwitch setState:NSOnState]; + } else { + [limitRowsSwitch setState:NSOffState]; + } + if ( [prefs boolForKey:@"useMonospacedFonts"] ) { + [useMonospacedFontsSwitch setState:NSOnState]; + } else { + [useMonospacedFontsSwitch setState:NSOffState]; + } + if ( [prefs boolForKey:@"fetchRowCount"] ) { + [fetchRowCountSwitch setState:NSOnState]; + } else { + [fetchRowCountSwitch setState:NSOffState]; + } + [nullValueField setStringValue:[prefs stringForKey:@"nullValue"]]; + [limitRowsField setStringValue:[prefs stringForKey:@"limitRowsValue"]]; + [self chooseLimitRows:self]; + [encodingPopUpButton selectItemWithTitle:[prefs stringForKey:@"encoding"]]; + + [preferencesWindow makeKeyAndOrderFront:self]; +} + +/* +adds a favorite +*/ +- (IBAction)addFavorite:(id)sender +{ + int code; + + isNewFavorite = YES; + + [nameField setStringValue:@""]; + [hostField setStringValue:@""]; + [socketField setStringValue:@""]; + [userField setStringValue:@""]; + [passwordField setStringValue:@""]; + [portField setStringValue:@""]; + [databaseField setStringValue:@""]; + [sshCheckbox setState:NSOffState]; + [sshUserField setEnabled:NO]; + [sshPasswordField setEnabled:NO]; + [sshHostField setEnabled:NO]; + [sshPortField setEnabled:NO]; + [sshHostField setStringValue:@""]; + [sshUserField setStringValue:@""]; + [sshPortField setStringValue:@"8888"]; + [sshPasswordField setStringValue:@""]; + + [NSApp beginSheet:favoriteSheet + modalForWindow:preferencesWindow + modalDelegate:self + didEndSelector:nil + contextInfo:nil]; + + code = [NSApp runModalForWindow:favoriteSheet]; + + [NSApp endSheet:favoriteSheet]; + [favoriteSheet orderOut:nil]; + + if ( code == 1 ) { + if ( ![[socketField stringValue] isEqualToString:@""] ) { + //set host to localhost if socket is used + [hostField setStringValue:@"localhost"]; + } + + // get ssh settings + NSString *sshHost, *sshUser, *sshPassword, *sshPort; + NSNumber *ssh; + if ( [sshCheckbox state] == NSOnState ) { + if ( [[sshHostField stringValue] isEqualToString:@""] ) { + sshHost = [hostField stringValue]; + } else { + sshHost = [sshHostField stringValue]; + } + if ( [[sshUserField stringValue] isEqualToString:@""] ) { + sshUser = [userField stringValue]; + } else { + sshUser = [sshUserField stringValue]; + } + if ( [[sshPasswordField stringValue] isEqualToString:@""] ) { + sshPassword = [passwordField stringValue]; + } else { + sshPassword = [sshPasswordField stringValue]; + } + if ( [[sshPortField stringValue] isEqualToString:@""] ) { + sshPort = [portField stringValue]; + } else { + sshPort = [sshPortField stringValue]; + } + ssh = [NSNumber numberWithInt:1]; + } else { + sshHost = @""; + sshUser = @""; + sshPassword = @""; + sshPort = @""; + ssh = [NSNumber numberWithInt:0]; + } + + NSDictionary *favorite = [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:[nameField stringValue], [hostField stringValue], [socketField stringValue], [userField stringValue], [portField stringValue], [databaseField stringValue], ssh, sshHost, sshUser, sshPort, nil] + forKeys:[NSArray arrayWithObjects:@"name", @"host", @"socket", @"user", @"port", @"database", @"useSSH", @"sshHost", @"sshUser", @"sshPort", nil]]; + [favorites addObject:favorite]; + + if ( ![[passwordField stringValue] isEqualToString:@""] ) + [keyChainInstance addPassword:[passwordField stringValue] + forName:[NSString stringWithFormat:@"Sequel Pro : %@", [nameField stringValue]] + account:[NSString stringWithFormat:@"%@@%@/%@", [userField stringValue], [hostField stringValue], [databaseField stringValue]]]; + + if ( ![sshPassword isEqualToString:@""] ) + [keyChainInstance addPassword:sshPassword + forName:[NSString stringWithFormat:@"Sequel Pro SSHTunnel : %@", [nameField stringValue]] + account:[NSString stringWithFormat:@"%@@%@/%@", [userField stringValue], [hostField stringValue], [databaseField stringValue]]]; + + [tableView reloadData]; + [tableView selectRow:[tableView numberOfRows]-1 byExtendingSelection:NO]; + } + + isNewFavorite = NO; +} + +/* +removes a favorite +*/ +- (IBAction)removeFavorite:(id)sender +{ + if ( ![tableView numberOfSelectedRows] ) + return; + + NSString *name = [[favorites objectAtIndex:[tableView selectedRow]] objectForKey:@"name"]; + NSString *user = [[favorites objectAtIndex:[tableView selectedRow]] objectForKey:@"user"]; + NSString *host = [[favorites objectAtIndex:[tableView selectedRow]] objectForKey:@"host"]; + NSString *database = [[favorites objectAtIndex:[tableView selectedRow]] objectForKey:@"database"]; + + [keyChainInstance deletePasswordForName:[NSString stringWithFormat:@"Sequel Pro : %@", name] + account:[NSString stringWithFormat:@"%@@%@/%@", user, host, database]]; + [keyChainInstance deletePasswordForName:[NSString stringWithFormat:@"Sequel Pro SSHTunnel : %@", name] + account:[NSString stringWithFormat:@"%@@%@/%@", user, host, database]]; + [favorites removeObjectAtIndex:[tableView selectedRow]]; + [tableView reloadData]; +} + +/* +copies a favorite +*/ +- (IBAction)copyFavorite:(id)sender +{ + if ( ![tableView numberOfSelectedRows] ) + return; + + NSMutableDictionary *tempDictionary = [NSMutableDictionary dictionaryWithDictionary:[favorites objectAtIndex:[tableView selectedRow]]]; + [tempDictionary setObject:[NSString stringWithFormat:@"%@Copy", [tempDictionary objectForKey:@"name"]] forKey:@"name"]; +// [tempDictionary setObject:[NSString stringWithFormat:@"%@Copy", [tempDictionary objectForKey:@"user"]] forKey:@"user"]; + + [favorites insertObject:tempDictionary atIndex:[tableView selectedRow]+1]; + [tableView selectRow:[tableView selectedRow]+1 byExtendingSelection:NO]; + + [tableView reloadData]; +} + +/* +enables or disables limitRowsField (depending on the state of limitRowsSwitch) +*/ +- (IBAction)chooseLimitRows:(id)sender +{ + if ( [limitRowsSwitch state] == NSOnState ) { + [limitRowsField setEnabled:YES]; + [limitRowsField selectText:self]; + } else { + [limitRowsField setEnabled:NO]; + } +} + +/* +close the favoriteSheet and save favorite if user hit save +*/ +- (IBAction)closeFavoriteSheet:(id)sender +{ + NSEnumerator *enumerator = [favorites objectEnumerator]; + id favorite; + int count; + + //test if user has entered at least name and host/socket + if ( [sender tag] && + ([[nameField stringValue] isEqualToString:@""] || ([[hostField stringValue] isEqualToString:@""] && [[socketField stringValue] isEqualToString:@""])) ) { + NSRunAlertPanel(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"Please enter at least name and host or socket!", @"message of panel when name/host/socket are missing"), NSLocalizedString(@"OK", @"OK button"), nil, nil); + return; + } + + //test if favorite name isn't used by another favorite + count = 0; + if ( [sender tag] ) { + while ( (favorite = [enumerator nextObject]) ) { + if ( [[favorite objectForKey:@"name"] isEqualToString:[nameField stringValue]] ) + { + if ( isNewFavorite || (!isNewFavorite && (count != [tableView selectedRow])) ) { + NSRunAlertPanel(NSLocalizedString(@"Error", @"error"), [NSString stringWithFormat:NSLocalizedString(@"Favorite %@ has already been saved!\nPlease specify another name.", @"message of panel when favorite name has already been used"), [nameField stringValue]], NSLocalizedString(@"OK", @"OK button"), nil, nil); + return; + } + } +/* + if ( [[favorite objectForKey:@"host"] isEqualToString:[hostField stringValue]] && + [[favorite objectForKey:@"user"] isEqualToString:[userField stringValue]] && + [[favorite objectForKey:@"database"] isEqualToString:[databaseField stringValue]] ) { + if ( isNewFavorite || (!isNewFavorite && (count != [tableView selectedRow])) ) { + NSRunAlertPanel(@"Error", @"There is already a favorite with the same host, user and database!", @"OK", nil, nil); + return; + } + } +*/ + count++; + } + } + + [NSApp stopModalWithCode:[sender tag]]; +} + +/* +enables/disables ssh tunneling +*/ +- (IBAction)toggleUseSSH:(id)sender +{ + if ( [sshCheckbox state] == NSOnState ) { + [sshUserField setEnabled:YES]; + [sshPasswordField setEnabled:YES]; + [sshHostField setEnabled:YES]; + [sshPortField setEnabled:YES]; + } else { + [sshUserField setEnabled:NO]; + [sshPasswordField setEnabled:NO]; + [sshHostField setEnabled:NO]; + [sshPortField setEnabled:NO]; + } +} + +#pragma mark Services menu methods + +/* +passes the query to the last created document +*/ +- (void)doPerformQueryService:(NSPasteboard *)pboard userData:(NSString *)data error:(NSString **)error +{ + NSString *pboardString; + NSArray *types; + + types = [pboard types]; + + if (![types containsObject:NSStringPboardType] || !(pboardString = [pboard stringForType:NSStringPboardType])) { + *error = @"Pasteboard couldn't give string."; + return; + } + + //check if there exists a document + if ( ![[[NSDocumentController sharedDocumentController] documents] count] ) { + *error = @"No Documents open!"; + return; + } + + //pass query to last created document +// [[[NSDocumentController sharedDocumentController] currentDocument] doPerformQueryService:pboardString]; + [[[[NSDocumentController sharedDocumentController] documents] objectAtIndex:[[[NSDocumentController sharedDocumentController] documents] count]-1] doPerformQueryService:pboardString]; + + return; +} + + +#pragma mark Sequel Pro menu methods + +/* +opens donate link in default browser +*/ +- (IBAction)donate:(id)sender +{ + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://code.google.com/p/sequel-pro/wiki/Donations"]]; +} + +/* +opens website link in default browser +*/ +- (IBAction)visitWebsite:(id)sender +{ + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://code.google.com/p/sequel-pro"]]; +} + +/* +opens help link in default browser +*/ +- (IBAction)visitHelpWebsite:(id)sender +{ + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://code.google.com/p/sequel-pro/wiki/FAQ"]]; +} + +/* +checks for updates and opens download page in default browser +*/ +- (IBAction)checkForUpdates:(id)sender +{ + NSLog(@"[MainController checkForUpdates:] is not currently functional."); +} + + +#pragma mark TableView datasource methods + +- (int)numberOfRowsInTableView:(NSTableView *)aTableView +{ + return [favorites count]; +} + +- (id)tableView:(NSTableView *)aTableView + objectValueForTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex +{ + return [[favorites objectAtIndex:rowIndex] objectForKey:[aTableColumn identifier]]; +} + + +#pragma mark TableView drag & drop datasource methods + +- (BOOL)tableView:(NSTableView *)tv writeRows:(NSArray*)rows toPasteboard:(NSPasteboard*)pboard +{ + int originalRow; + NSArray *pboardTypes; + + if ( [rows count] == 1 ) { + pboardTypes=[NSArray arrayWithObjects:@"SequelProPreferencesPasteboard", nil]; + originalRow = [[rows objectAtIndex:0] intValue]; + + [pboard declareTypes:pboardTypes owner:nil]; + [pboard setString:[[NSNumber numberWithInt:originalRow] stringValue] forType:@"SequelProPreferencesPasteboard"]; + + return YES; + } else { + return NO; + } +} + +- (NSDragOperation)tableView:(NSTableView*)tv validateDrop:(id <NSDraggingInfo>)info proposedRow:(int)row + proposedDropOperation:(NSTableViewDropOperation)operation +{ + NSArray *pboardTypes = [[info draggingPasteboard] types]; + int originalRow; + + if ([pboardTypes count] == 1 && row != -1) + { + if ([[pboardTypes objectAtIndex:0] isEqualToString:@"SequelProPreferencesPasteboard"]==YES && operation==NSTableViewDropAbove) + { + originalRow = [[[info draggingPasteboard] stringForType:@"SequelProPreferencesPasteboard"] intValue]; + + if (row != originalRow && row != (originalRow+1)) + { + return NSDragOperationMove; + } + } + } + + return NSDragOperationNone; +} + +- (BOOL)tableView:(NSTableView*)tv acceptDrop:(id <NSDraggingInfo>)info row:(int)row dropOperation:(NSTableViewDropOperation)operation +{ + int originalRow; + int destinationRow; + NSMutableDictionary *draggedRow; + + originalRow = [[[info draggingPasteboard] stringForType:@"SequelProPreferencesPasteboard"] intValue]; + destinationRow = row; + + if ( destinationRow > originalRow ) + destinationRow--; + + draggedRow = [NSMutableDictionary dictionaryWithDictionary:[favorites objectAtIndex:originalRow]]; + [favorites removeObjectAtIndex:originalRow]; + [favorites insertObject:draggedRow atIndex:destinationRow]; + + [tableView reloadData]; + [tableView selectRow:destinationRow byExtendingSelection:NO]; + + return YES; +} + +/* + opens sheet to edit favorite and saves favorite if user hit OK + */ +- (BOOL)tableView:(NSTableView *)aTableView shouldEditTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex +{ + int code; + NSDictionary *favorite = [favorites objectAtIndex:rowIndex]; + + // set up fields + [nameField setStringValue:[favorite objectForKey:@"name"]]; + [hostField setStringValue:[favorite objectForKey:@"host"]]; + [socketField setStringValue:[favorite objectForKey:@"socket"]]; + [userField setStringValue:[favorite objectForKey:@"user"]]; + [portField setStringValue:[favorite objectForKey:@"port"]]; + [databaseField setStringValue:[favorite objectForKey:@"database"]]; + [passwordField setStringValue:[keyChainInstance getPasswordForName:[NSString stringWithFormat:@"Sequel Pro : %@", [nameField stringValue]] + account:[NSString stringWithFormat:@"%@@%@/%@", [userField stringValue], [hostField stringValue], [databaseField stringValue]]]]; + + // set up ssh fields + if ( [[favorite objectForKey:@"useSSH"] intValue] == 1 ) { + [sshCheckbox setState:NSOnState]; + [sshUserField setEnabled:YES]; + [sshPasswordField setEnabled:YES]; + [sshHostField setEnabled:YES]; + [sshPortField setEnabled:YES]; + [sshHostField setStringValue:[favorite objectForKey:@"sshHost"]]; + [sshUserField setStringValue:[favorite objectForKey:@"sshUser"]]; + [sshPortField setStringValue:[favorite objectForKey:@"sshPort"]]; + [sshPasswordField setStringValue:[keyChainInstance getPasswordForName:[NSString stringWithFormat:@"Sequel Pro SSHTunnel : %@", [nameField stringValue]] + account:[NSString stringWithFormat:@"%@@%@/%@", [userField stringValue], [hostField stringValue], [databaseField stringValue]]]]; + } else { + [sshCheckbox setState:NSOffState]; + [sshUserField setEnabled:NO]; + [sshPasswordField setEnabled:NO]; + [sshHostField setEnabled:NO]; + [sshPortField setEnabled:NO]; + [sshHostField setStringValue:@""]; + [sshUserField setStringValue:@""]; + [sshPortField setStringValue:@""]; + [sshPasswordField setStringValue:@""]; + } + + // run sheet + [NSApp beginSheet:favoriteSheet + modalForWindow:preferencesWindow + modalDelegate:self + didEndSelector:nil + contextInfo:nil]; + + code = [NSApp runModalForWindow:favoriteSheet]; + + [NSApp endSheet:favoriteSheet]; + [favoriteSheet orderOut:nil]; + + if ( code == 1 ) { + if ( ![[socketField stringValue] isEqualToString:@""] ) { + //set host to localhost if socket is used + [hostField setStringValue:@"localhost"]; + } + + //get ssh settings + NSString *sshHost, *sshUser, *sshPassword, *sshPort; + NSNumber *ssh; + if ( [sshCheckbox state] == NSOnState ) { + if ( [[sshHostField stringValue] isEqualToString:@""] ) { + sshHost = [hostField stringValue]; + } else { + sshHost = [sshHostField stringValue]; + } + if ( [[sshUserField stringValue] isEqualToString:@""] ) { + sshUser = [userField stringValue]; + } else { + sshUser = [sshUserField stringValue]; + } + if ( [[sshPasswordField stringValue] isEqualToString:@""] ) { + sshPassword = [passwordField stringValue]; + } else { + sshPassword = [sshPasswordField stringValue]; + } + if ( [[sshPortField stringValue] isEqualToString:@""] ) { + sshPort = [portField stringValue]; + } else { + sshPort = [sshPortField stringValue]; + } + ssh = [NSNumber numberWithInt:1]; + } else { + sshHost = @""; + sshUser = @""; + sshPassword = @""; + sshPort = @""; + ssh = [NSNumber numberWithInt:0]; + } + + //replace password + [keyChainInstance deletePasswordForName:[NSString stringWithFormat:@"Sequel Pro : %@", [favorite objectForKey:@"name"]] + account:[NSString stringWithFormat:@"%@@%@/%@", [favorite objectForKey:@"user"], [favorite objectForKey:@"host"], [favorite objectForKey:@"database"]]]; + + if ( ![[passwordField stringValue] isEqualToString:@""] ) + [keyChainInstance addPassword:[passwordField stringValue] + forName:[NSString stringWithFormat:@"Sequel Pro : %@", [nameField stringValue]] + account:[NSString stringWithFormat:@"%@@%@/%@", [userField stringValue], [hostField stringValue], [databaseField stringValue]]]; + + //replace ssh password + [keyChainInstance deletePasswordForName:[NSString stringWithFormat:@"Sequel Pro SSHTunnel : %@", [favorite objectForKey:@"name"]] + account:[NSString stringWithFormat:@"%@@%@/%@", [favorite objectForKey:@"user"], [favorite objectForKey:@"host"], [favorite objectForKey:@"database"]]]; + + if ( ([sshCheckbox state] == NSOnState) && ![sshPassword isEqualToString:@""] ) { + [keyChainInstance addPassword:sshPassword + forName:[NSString stringWithFormat:@"Sequel Pro SSHTunnel : %@", [nameField stringValue]] + account:[NSString stringWithFormat:@"%@@%@/%@", [userField stringValue], [hostField stringValue], + [databaseField stringValue]]]; + } + + //replace favorite + favorite = [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:[nameField stringValue], [hostField stringValue], [socketField stringValue], [userField stringValue], [portField stringValue], [databaseField stringValue], ssh, sshHost, sshUser, sshPort, nil] + forKeys:[NSArray arrayWithObjects:@"name", @"host", @"socket", @"user", @"port", @"database", @"useSSH", @"sshHost", @"sshUser", @"sshPort", nil]]; + [favorites replaceObjectAtIndex:rowIndex withObject:favorite]; + [tableView reloadData]; + } + + return NO; +} + + +#pragma mark Window delegate methods + +/* + saves the preferences + */ +- (BOOL)windowShouldClose:(id)sender +{ + if ( sender == preferencesWindow ) { + if ( [reloadAfterAddingSwitch state] == NSOnState ) { + [prefs setBool:YES forKey:@"reloadAfterAdding"]; + } else { + [prefs setBool:NO forKey:@"reloadAfterAdding"]; + } + if ( [reloadAfterEditingSwitch state] == NSOnState ) { + [prefs setBool:YES forKey:@"reloadAfterEditing"]; + } else { + [prefs setBool:NO forKey:@"reloadAfterEditing"]; + } + if ( [reloadAfterRemovingSwitch state] == NSOnState ) { + [prefs setBool:YES forKey:@"reloadAfterRemoving"]; + } else { + [prefs setBool:NO forKey:@"reloadAfterRemoving"]; + } + if ( [showErrorSwitch state] == NSOnState ) { + [prefs setBool:YES forKey:@"showError"]; + } else { + [prefs setBool:NO forKey:@"showError"]; + } + if ( [dontShowBlobSwitch state] == NSOnState ) { + [prefs setBool:YES forKey:@"dontShowBlob"]; + } else { + [prefs setBool:NO forKey:@"dontShowBlob"]; + } + if ( [limitRowsSwitch state] == NSOnState ) { + [prefs setBool:YES forKey:@"limitRows"]; + } else { + [prefs setBool:NO forKey:@"limitRows"]; + } + if ( [useMonospacedFontsSwitch state] == NSOnState ) { + [prefs setBool:YES forKey:@"useMonospacedFonts"]; + } else { + [prefs setBool:NO forKey:@"useMonospacedFonts"]; + } + if ( [fetchRowCountSwitch state] == NSOnState ) { + [prefs setBool:YES forKey:@"fetchRowCount"]; + } else { + [prefs setBool:NO forKey:@"fetchRowCount"]; + } + [prefs setObject:[nullValueField stringValue] forKey:@"nullValue"]; + if ( [limitRowsField intValue] > 0 ) { + [prefs setInteger:[limitRowsField intValue] forKey:@"limitRowsValue"]; + } else { + [prefs setInteger:1 forKey:@"limitRowsValue"]; + } + [prefs setObject:[encodingPopUpButton titleOfSelectedItem] forKey:@"encoding"]; + + [prefs setObject:favorites forKey:@"favorites"]; + } + return YES; +} + + +#pragma mark Other methods + +- (void)awakeFromNib +{ + NSEnumerator *enumerator; + id favorite; + NSString *name, *host, *user, *database, *password; + //int code; + + //register MainController as services provider + [NSApp setServicesProvider:self]; + + //register MainController for AppleScript events + [[ NSScriptExecutionContext sharedScriptExecutionContext] setTopLevelObject: self ]; + + prefs = [[NSUserDefaults standardUserDefaults] retain]; + isNewFavorite = NO; + [prefs registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithBool:YES], @"limitRows", + [NSNumber numberWithInt:1000], @"limitRowsValue", + nil]]; + + //set standard preferences if no preferences are found + if ( [prefs objectForKey:@"reloadAfterAdding"] == nil ) + { + [prefs setObject:@"0.3" forKey:@"version"]; + [prefs setBool:YES forKey:@"reloadAfterAdding"]; + [prefs setBool:YES forKey:@"reloadAfterEditing"]; + [prefs setBool:NO forKey:@"reloadAfterRemoving"]; + [prefs setObject:@"NULL" forKey:@"nullValue"]; + //[prefs setBool:YES forKey:@"showError"]; + //[prefs setBool:NO forKey:@"dontShowBlob"]; + //[prefs setBool:NO forKey:@"limitRows"]; + //[prefs setInteger:100 forKey:@"limitRowsValue"]; + //[prefs setObject:[NSString stringWithString:NSHomeDirectory()] forKey:@"savePath"]; + //[prefs setObject:[NSString stringWithString:NSHomeDirectory()] forKey:@"openPath"]; + } + + //new preferences and changes in v0.4 + if ( [prefs objectForKey:@"showError"] == nil ) { + [prefs setObject:@"0.4" forKey:@"version"]; + + //set standard values for new preferences + [prefs setBool:YES forKey:@"showError"]; + [prefs setBool:NO forKey:@"dontShowBlob"]; + //[prefs setBool:NO forKey:@"limitRows"]; + //[prefs setInteger:100 forKey:@"limitRowsValue"]; + [prefs setObject:[NSString stringWithString:NSHomeDirectory()] forKey:@"savePath"]; + [prefs setObject:[NSString stringWithString:NSHomeDirectory()] forKey:@"openPath"]; + + //remove old preferences + [prefs removeObjectForKey:@"allowDragAndDropReordering"]; + + //rewrite passwords to keychain (with new format) + if ( [prefs objectForKey:@"favorites"] ) { + NSRunAlertPanel(NSLocalizedString(@"Warning", @"warning"), NSLocalizedString(@"With version 0.4 Sequel Pro has introduced a new format to save passwords in the Keychain.\nPlease allow Sequel Pro to decrypt all passwords of your favorites. Otherwise you have to reenter all passwords of your saved favorites in the Preferences.", @"message of panel when passwords have to be updated for v0.4"), NSLocalizedString(@"OK", @"OK button"), nil, nil); + enumerator = [[prefs objectForKey:@"favorites"] objectEnumerator]; + + while ( (favorite = [enumerator nextObject]) ) { + //replace password + name = [favorite objectForKey:@"name"]; + host = [favorite objectForKey:@"host"]; + user = [favorite objectForKey:@"user"]; + database = [favorite objectForKey:@"database"]; + password = [keyChainInstance getPasswordForName:[NSString stringWithFormat:@"%@/%@", host, database] + account:user]; + [keyChainInstance deletePasswordForName:[NSString stringWithFormat:@"%@/%@", host, database] account:user]; + if ( ![password isEqualToString:@""] ) + [keyChainInstance addPassword:password + forName:[NSString stringWithFormat:@"Sequel Pro : %@", name] + account:[NSString stringWithFormat:@"%@@%@/%@", user, host, database]]; + } + } + } + + //new preferences and changes in v0.5 + if ( [[prefs objectForKey:@"version"] isEqualToString:@"0.4"] ) { + [prefs setObject:@"0.5" forKey:@"version"]; + + //set standard values for new preferences + [prefs setObject:@"ISO Latin 1" forKey:@"encoding"]; + [prefs setBool:NO forKey:@"useMonospacedFonts"]; + + //add socket field to favorites + if ( [prefs objectForKey:@"favorites"] ) { + NSMutableArray *tempFavorites = [NSMutableArray array]; + NSMutableDictionary *tempFavorite; + enumerator = [[prefs objectForKey:@"favorites"] objectEnumerator]; + while ( (favorite = [enumerator nextObject]) ) { + tempFavorite = [NSMutableDictionary dictionaryWithDictionary:favorite]; + [tempFavorite setObject:@"" forKey:@"socket"]; + [tempFavorites addObject:[NSDictionary dictionaryWithDictionary:tempFavorite]]; + } + [prefs setObject:tempFavorites forKey:@"favorites"]; + } + } + + //new preferences and changes in v0.7 + if ( [[prefs objectForKey:@"version"] isEqualToString:@"0.5"] || + [[prefs objectForKey:@"version"] isEqualToString:@"0.6beta"] || + [[prefs objectForKey:@"version"] isEqualToString:@"0.7b2"] ) + { + [prefs setObject:@"0.7b3" forKey:@"version"]; + [prefs setObject:@"Autodetect" forKey:@"encoding"]; + [prefs setBool:YES forKey:@"fetchRowCount"]; + } + +//set up interface +/* + enumerator = [tableColumns objectEnumerator]; + while ( (column = [enumerator nextObject]) ) + { + [[column dataCell] setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + } +*/ + [tableView registerForDraggedTypes:[NSArray arrayWithObjects:@"SequelProPreferencesPasteboard", nil]]; + [tableView reloadData]; +} + + +// SSHTunnel methods +- (id)authenticate:(NSScriptCommand *)command { + NSDictionary *args = [command evaluatedArguments]; + NSString *givenQuery = [ args objectForKey:@"query"]; + NSString *tunnelName = [ args objectForKey:@"tunnelName"]; + NSString *fifo = [ args objectForKey:@"fifo"]; + + NSLog(@"tunnel: %@ / query: %@ / fifo: %@",tunnelName,givenQuery,fifo); + NSFileHandle *fh = [ NSFileHandle fileHandleForWritingAtPath: fifo ]; + [ fh writeData: [ @"xy" dataUsingEncoding: NSASCIIStringEncoding]]; + [ fh closeFile ]; + + NSLog(@"password written"); + return @"OK"; + +/* + [ query setStringValue: givenQuery ]; + [NSApp beginSheet: alertSheet + modalForWindow: mainWindow + modalDelegate: nil + didEndSelector: nil + contextInfo: nil]; + [NSApp runModalForWindow: alertSheet]; + // Sheet is up here. + [NSApp endSheet: alertSheet]; + [alertSheet orderOut: self]; + if ( sheetStatus == 0) + { + password = [ passwd stringValue ]; + [ passwd setStringValue: @"" ]; + return password ; + } + else + { + [[tunnelTask objectForKey: @"task" ] terminate ]; + } + sheetStatus = nil; + return @""; +*/ +} + +- (id)handleQuitScriptCommand:(NSScriptCommand *)command +/* what exactly is this for? */ +{ + [ NSApp terminate: self ]; +} + +@end diff --git a/Source/SPGrowlController.h b/Source/SPGrowlController.h new file mode 100644 index 00000000..2ad73e8b --- /dev/null +++ b/Source/SPGrowlController.h @@ -0,0 +1,34 @@ +// +// SPGrowlController.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on Nov 28, 2008 +// +// 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 <Cocoa/Cocoa.h> +#import <Growl/Growl.h> + +@interface SPGrowlController : NSObject <GrowlApplicationBridgeDelegate> + +// Singleton controller ++ (SPGrowlController *)sharedGrowlController; + +// Post notification +- (void)notifyWithTitle:(NSString *)title description:(NSString *)description notificationName:(NSString *)name; + +@end diff --git a/Source/SPGrowlController.m b/Source/SPGrowlController.m new file mode 100644 index 00000000..3f429067 --- /dev/null +++ b/Source/SPGrowlController.m @@ -0,0 +1,123 @@ +// +// SPGrowlController.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on Nov 28, 2008 +// +// 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 "SPGrowlController.h" + +static SPGrowlController *sharedGrowlController = nil; + +@implementation SPGrowlController + +// ------------------------------------------------------------------------------- +// sharedGrowlController +// +// Returns the shared Growl controller. +// ------------------------------------------------------------------------------- ++ (SPGrowlController *)sharedGrowlController +{ + @synchronized(self) { + if (sharedGrowlController == nil) { + [[self alloc] init]; + } + } + + return sharedGrowlController; +} + +// ------------------------------------------------------------------------------- +// allocWithZone: +// ------------------------------------------------------------------------------- ++ (id)allocWithZone:(NSZone *)zone +{ + @synchronized(self) { + if (sharedGrowlController == nil) { + sharedGrowlController = [super allocWithZone:zone]; + + return sharedGrowlController; + } + } + + return nil; // On subsequent allocation attempts return nil +} + +// ------------------------------------------------------------------------------- +// init +// ------------------------------------------------------------------------------- +- (id)init +{ + if (self = [super init]) { + [GrowlApplicationBridge setGrowlDelegate:self]; + } + + return self; +} + +// ------------------------------------------------------------------------------- +// The following base protocol methods are implemented to ensure the singleton +// status of this class. +// ------------------------------------------------------------------------------- + +- (id)copyWithZone:(NSZone *)zone { return self; } + +- (id)retain { return self; } + +- (unsigned)retainCount { return UINT_MAX; } + +- (id)autorelease { return self; } + +- (void)release { } + +// ------------------------------------------------------------------------------- +// notifyWithTitle:description:notificationName: +// +// Posts a Growl notification using the supplied details and default values. +// ------------------------------------------------------------------------------- +- (void)notifyWithTitle:(NSString *)title description:(NSString *)description notificationName:(NSString *)name +{ + // Post notification + [GrowlApplicationBridge notifyWithTitle:title + description:description + notificationName:name + iconData:nil + priority:0 + isSticky:NO + clickContext:nil]; +} + +// ------------------------------------------------------------------------------- +// notifyWithTitle:description:notificationName: +// +// Posts a Growl notification using the supplied details and effectively ignoring +// the default values. +// ------------------------------------------------------------------------------- +- (void)notifyWithTitle:(NSString *)title description:(NSString *)description notificationName:(NSString *)name iconData:(NSData *)data priority:(int)priority isSticky:(BOOL)sticky clickContext:(id)clickContext +{ + // Post notification + [GrowlApplicationBridge notifyWithTitle:title + description:description + notificationName:name + iconData:data + priority:priority + isSticky:sticky + clickContext:clickContext]; +} + +@end diff --git a/Source/SPTableInfo.h b/Source/SPTableInfo.h new file mode 100644 index 00000000..0613794c --- /dev/null +++ b/Source/SPTableInfo.h @@ -0,0 +1,24 @@ +// +// SPTableInfo.h +// sequel-pro +// +// Created by Ben Perry on 6/05/08. +// Copyright 2008 Ben Perry. All rights reserved. +// + +#import <Cocoa/Cocoa.h> + + +@interface SPTableInfo : NSObject { + IBOutlet id infoTable; + IBOutlet id tableList; + IBOutlet id tableListInstance; + IBOutlet id tableDocumentInstance; + + NSMutableArray *info; +} + +- (NSString *)sizeFromBytes:(int)bytes; + + +@end diff --git a/Source/SPTableInfo.m b/Source/SPTableInfo.m new file mode 100644 index 00000000..b2b81c05 --- /dev/null +++ b/Source/SPTableInfo.m @@ -0,0 +1,190 @@ +// +// SPTableInfo.m +// sequel-pro +// +// Created by Ben Perry on 6/05/08. +// Copyright 2008 Ben Perry. All rights reserved. +// + +#import "SPTableInfo.h" +#import "ImageAndTextCell.h" +#import <MCPKit_bundled/MCPKit_bundled.h> +#import "CMMCPConnection.h" +#import "CMMCPResult.h" +#import "TableDocument.h" + +@implementation SPTableInfo + +- (id)init +{ + self = [super init]; + info = [[NSMutableArray alloc] init]; + return self; +} + +- (void)awakeFromNib +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(tableChanged:) + name:NSTableViewSelectionDidChangeNotification + object:tableList]; + [info addObject:NSLocalizedString(@"TABLE INFORMATION",@"header for table info pane")]; + [infoTable reloadData]; +} + + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + [info release]; + + [super dealloc]; +} + +- (int)numberOfRowsInTableView:(NSTableView *)aTableView +{ + return [info count]; +} + +- (id)tableView:(NSTableView *)aTableView +objectValueForTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex +{ + return [info objectAtIndex:rowIndex]; +} + +- (BOOL)tableView:(NSTableView *)aTableView shouldSelectRow:(int)rowIndex +{ + // row 1 and 6 should be editable - ie be able to rename the table and change the auto_increment value. + return NO;//(rowIndex == 1 || rowIndex == 6 ); +} + + +- (BOOL)tableView:(NSTableView *)aTableView isGroupRow:(int)row +{ + // This makes the top row (TABLE INFORMATION) have the diff styling + return (row == 0); +} + +- (void)tableView:(NSTableView *)aTableView + willDisplayCell:(id)aCell + forTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex +{ + if ((rowIndex > 0) && [[aTableColumn identifier] isEqualToString:@"info"]) { + [(ImageAndTextCell*)aCell setImage:[NSImage imageNamed:@"CodeAssistantProtocol"]]; + [(ImageAndTextCell*)aCell setIndentationLevel:1]; + } else { + [(ImageAndTextCell*)aCell setImage:nil]; + [(ImageAndTextCell*)aCell setIndentationLevel:0]; + } +} + +- (void)tableChanged:(NSNotification *)notification +{ + NSString *query; + CMMCPResult *theResult; + NSDictionary *theRow; + + [info removeAllObjects]; + [info addObject:@"TABLE INFORMATION"]; + + if ([tableListInstance table]) + { + if ([(NSString *)[tableListInstance table] isEqualToString:@""]) { + [info addObject:@"multiple tables"]; + + } else { + // Notify that we are about to perform a query + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; + + // Create the query and get results + query = [NSString stringWithFormat:@"SHOW TABLE STATUS LIKE '%@'", [tableListInstance table]]; + + // This line triggers a bug when opening a new window. but only after having closed a window + theResult = [[tableDocumentInstance sharedConnection] queryString:query]; + + // Check for errors + if (![[[tableDocumentInstance sharedConnection] getLastErrorMessage] isEqualToString:@""]) { + [info addObject:@"error occurred"]; + return; + } + + // Process result + theRow = [[theResult fetch2DResultAsType:MCPTypeDictionary] lastObject]; + + // Check for "Create_time" == NULL + if (![[theRow objectForKey:@"Create_time"] isNSNull]) { + // Setup our data formatter + NSDateFormatter *createDateFormatter = [[[NSDateFormatter alloc] init] autorelease]; + [createDateFormatter setDateStyle:NSDateFormatterShortStyle]; + [createDateFormatter setTimeStyle:NSDateFormatterNoStyle]; + + // Convert our string date from the result to an NSDate. + NSDate *create_date = [NSDate dateWithNaturalLanguageString:[theRow objectForKey:@"Create_time"]]; + + // Add the creation date to the infoTable + [info addObject:[NSString stringWithFormat:@"created: %@", [createDateFormatter stringFromDate:create_date]]]; + } + + // Check for "Update_time" == NULL - InnoDB tables don't have an update time + if (![[theRow objectForKey:@"Update_time"] isNSNull]) { + // Setup our data formatter + NSDateFormatter *updateDateFormatter = [[[NSDateFormatter alloc] init] autorelease]; + [updateDateFormatter setDateStyle:NSDateFormatterShortStyle]; + [updateDateFormatter setTimeStyle:NSDateFormatterNoStyle]; + + // Convert our string date from the result to an NSDate. + NSDate *update_date = [NSDate dateWithNaturalLanguageString:[theRow objectForKey:@"Update_time"]]; + + // Add the update date to the infoTable + [info addObject:[NSString stringWithFormat:@"updated: %@", [updateDateFormatter stringFromDate:update_date]]]; + } + + [info addObject:[NSString stringWithFormat:@"rows: %@", [theRow objectForKey:@"Rows"]]]; + [info addObject:[NSString stringWithFormat:@"size: %@", [self sizeFromBytes:[[theRow objectForKey:@"Data_length"] intValue]]]]; + [info addObject:[NSString stringWithFormat:@"encoding: %@", [[[theRow objectForKey:@"Collation"] componentsSeparatedByString:@"_"] objectAtIndex:0]]]; + [info addObject:[NSString stringWithFormat:@"auto_increment: %@", [theRow objectForKey:@"Auto_increment"]]]; + + // Notify that we've finished performing the query + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; + } + } + + [infoTable reloadData]; +} + +- (NSString *)sizeFromBytes:(int)theSize +{ + NSNumberFormatter *numberFormatter = [[[NSNumberFormatter alloc] init] autorelease]; + float floatSize = theSize; + + [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle]; + + if (theSize < 1023) { + [numberFormatter setFormat:@"#,##0 B"]; + return [numberFormatter stringFromNumber:[NSNumber numberWithInt:theSize]]; + } + + floatSize = floatSize / 1024; + + if (floatSize < 1023) { + [numberFormatter setFormat:@"#,##0.0 KB"]; + return [numberFormatter stringFromNumber:[NSNumber numberWithFloat:floatSize]]; + } + + floatSize = floatSize / 1024; + + if (floatSize < 1023) { + [numberFormatter setFormat:@"#,##0.0 MB"]; + return [numberFormatter stringFromNumber:[NSNumber numberWithFloat:floatSize]]; + } + + floatSize = floatSize / 1024; + + [numberFormatter setFormat:@"#,##0.0 GB"]; + return [numberFormatter stringFromNumber:[NSNumber numberWithFloat:floatSize]]; +} + +@end diff --git a/Source/SSHTunnel.h b/Source/SSHTunnel.h new file mode 100644 index 00000000..ffe29624 --- /dev/null +++ b/Source/SSHTunnel.h @@ -0,0 +1,57 @@ +#import <Cocoa/Cocoa.h> + +@interface SSHTunnel : NSObject +{ + int code; + NSArray *tunnelsLocal; + NSArray *tunnelsRemote; + + BOOL shouldStop; + NSTask *task; + BOOL connAuth; + BOOL autoConnect; + NSPipe *stdErrPipe; + NSString *connName; + NSString *status; + NSString *connPort; + BOOL connRemote; + BOOL compression; + BOOL v1; + NSString * encryption; + BOOL socks4; + NSNumber *socks4p; + NSString *connUser; + NSString *connHost; +} +-(id)initWithName:(NSString*)aName; +-(id)initWithDictionary:(NSDictionary*)aDictionary; ++(id)tunnelWithName:(NSString*)aName; ++(NSArray*)tunnelsFromArray:(NSArray*)anArray; + +-(void)addLocalTunnel:(NSDictionary*)aDictionary; +- (void)removeLocal:(int)index; +-(void)addRemoteTunnel:(NSDictionary*)aDictionary; +- (void)removeRemote:(int)index; +- (void)setLocalValue:(NSString*)aValue ofTunnel:(int)index forKey:(NSString*)key; +- (void)setRemoteValue:(NSString*)aValue ofTunnel:(int)index forKey:(NSString*)key; + +#pragma mark - +#pragma mark Execution related +- (void)startTunnel; +- (void)stopTunnel; +- (void)toggleTunnel; +- (void)launchTunnel:(id)foo; +- (void)stdErr:(NSNotification*)aNotification; +- (BOOL)isRunning; + +#pragma mark - +#pragma mark Getting tunnel informations +- (NSString*)status; +- (NSArray*)arguments; +- (NSDictionary*)dictionary; + +#pragma mark - +#pragma mark Key/Value coding +- (NSImage*)icon; + +@end diff --git a/Source/SSHTunnel.m b/Source/SSHTunnel.m new file mode 100644 index 00000000..617db8a7 --- /dev/null +++ b/Source/SSHTunnel.m @@ -0,0 +1,531 @@ +// +// SSHTunnel.m +// SSH Tunnel Manager 2 +// +// Created by Yann Bizeul on Wed Nov 19 2003. +// Copyright (c) 2003 __MyCompanyName__. All rights reserved. +// + +#import "SSHTunnel.h" +#include <unistd.h> + +// start diff lorenz textor +/* +#define T_START NSLocalizedString(@"T_START",@"") +#define T_STOP NSLocalizedString(@"T_STOP",@"") +#define S_IDLE NSLocalizedString(@"S_IDLE",@"") +#define S_CONNECTING NSLocalizedString(@"S_CONNECTING",@"") +#define S_CONNECTED NSLocalizedString(@"S_CONNECTED",@"") +#define S_AUTH NSLocalizedString(@"S_AUTH",@"") +#define S_PORT NSLocalizedString(@"S_PORT",@"") +*/ +#define T_START @"START: %@" +#define T_STOP @"STOP: %@" +#define S_IDLE @"Idle" +#define S_CONNECTING @"Connecting..." +#define S_CONNECTED @"Connected" +#define S_AUTH @"Authenticated" +#define S_PORT "Port %@ forwarded" +// end diff lorenz textor + +@implementation SSHTunnel + +#pragma mark - +#pragma mark Initialization +-(id)init +{ + return [ self initWithName:@"New Tunnel"]; +} +-(id)initWithName:(NSString*)aName +{ + NSDictionary *dictionary = [ NSDictionary dictionaryWithObjectsAndKeys: + [ NSNumber numberWithBool: NO ],@"compression", + [ NSNumber numberWithBool: YES ],@"connAuth", + @"", @"connHost", + aName, @"connName", + @"", @"connPort", + [ NSNumber numberWithBool: NO ],@"connRemote", + @"", @"connUser", + @"3des", @"encryption", + [ NSNumber numberWithBool: NO ],@"socks4", + [ NSNumber numberWithInt: 1080 ], @"socks4p", + [ NSArray array ], @"tunnelsLocal", + [ NSArray array ], @"tunnelsRemote", + [ NSNumber numberWithBool: NO ],@"v1", nil + ]; + return [ self initWithDictionary: dictionary ]; +} +-(id)initWithDictionary:(NSDictionary*)aDictionary +{ + NSEnumerator *e; + NSString *key; + + self = [ super init ]; + e = [[ aDictionary allKeys ] objectEnumerator ]; + while (key = [ e nextObject ]) + { + [ self setValue: [ aDictionary objectForKey: key ] forKey: key ]; + } + code = 0; + if ([[ self valueForKey: @"autoConnect" ] boolValue ]) + [ self startTunnel ]; + return self; +} ++(id)tunnelWithName:(NSString*)aName +{ + return [[ SSHTunnel alloc ] initWithName: aName ]; +} ++(SSHTunnel*)tunnelFromDictionary:(NSDictionary*)aDictionary +{ + return [[ SSHTunnel alloc ] initWithDictionary: aDictionary ]; +} ++(NSArray*)tunnelsFromArray:(NSArray*)anArray +{ + NSMutableArray *newArray; + SSHTunnel *currentTunnel; + NSEnumerator *e; + NSDictionary *currentTunnelDictionary; + + newArray = [ NSMutableArray array ]; + e = [ anArray objectEnumerator ]; + while (currentTunnelDictionary = [ e nextObject ]) + { + currentTunnel = [ SSHTunnel tunnelFromDictionary: currentTunnelDictionary ]; + [ newArray addObject: currentTunnel ]; + } + return [[ newArray copy ] autorelease ]; +} + +#pragma mark - +#pragma mark Adding and removing port redir. +-(void)addLocalTunnel:(NSDictionary*)aDictionary; +{ + NSMutableArray *tempTunnelsLocal = [ NSMutableArray arrayWithArray: tunnelsLocal ]; + [ tempTunnelsLocal addObject: aDictionary ]; + [ tunnelsLocal release ]; + tunnelsLocal = [ tempTunnelsLocal copy ]; +} +- (void)removeLocal:(int)index +{ + NSMutableArray *tempLocalTunnels = [ tunnelsLocal mutableCopy ]; + [ tempLocalTunnels removeObjectAtIndex: index ]; + [ tunnelsLocal release ]; + tunnelsLocal = [ tempLocalTunnels copy ]; + [ tempLocalTunnels release ]; +} +-(void)addRemoteTunnel:(NSDictionary*)aDictionary; +{ + NSMutableArray *tempTunnelsRemote = [ NSMutableArray arrayWithArray: tunnelsRemote ]; + [ tempTunnelsRemote addObject: aDictionary ]; + [ tunnelsRemote release ]; + tunnelsRemote = [ tempTunnelsRemote copy ]; +} +- (void)removeRemote:(int)index +{ + NSMutableArray *tempRemoteTunnels = [ tunnelsRemote mutableCopy ]; + [ tempRemoteTunnels removeObjectAtIndex: index ]; + [ tunnelsRemote release ]; + tunnelsRemote = [ tempRemoteTunnels copy ]; + [ tempRemoteTunnels release ]; +} +- (void)setLocalValue:(NSString*)aValue ofTunnel:(int)index forKey:(NSString*)key +{ + NSMutableArray *tempLocalTunnel; + NSMutableDictionary *tempCurrentTunnel; + + tempLocalTunnel = [tunnelsLocal mutableCopy]; + tempCurrentTunnel = [[ tempLocalTunnel objectAtIndex: index ] mutableCopy ]; + + [ tempCurrentTunnel setObject: aValue forKey: key ]; + [ tempLocalTunnel replaceObjectAtIndex:index withObject:[tempCurrentTunnel copy ]]; + [ tempCurrentTunnel release ]; + [ tunnelsLocal release ]; + tunnelsLocal = [ tempLocalTunnel copy ]; +} +- (void)setRemoteValue:(NSString*)aValue ofTunnel:(int)index forKey:(NSString*)key +{ + NSMutableArray *tempRemoteTunnel; + NSMutableDictionary *tempCurrentTunnel; + + tempRemoteTunnel = [tunnelsRemote mutableCopy]; + tempCurrentTunnel = [[ tempRemoteTunnel objectAtIndex: index ] mutableCopy ]; + + [ tempCurrentTunnel setObject: aValue forKey: key ]; + [ tempRemoteTunnel replaceObjectAtIndex:index withObject:[tempCurrentTunnel copy ]]; + [ tempCurrentTunnel release ]; + [ tunnelsRemote release ]; + tunnelsRemote = [ tempRemoteTunnel copy ]; +} + +#pragma mark - +#pragma mark Execution related +- (void)startTunnel +{ +// NSDictionary *t; +// NSEnumerator *e; +// BOOL asRoot = NO; + + if ([ self isRunning ]) + return; + + shouldStop = NO; + /* + [ arguments addObject: @"-N" ]; + [ arguments addObject: @"-v" ]; + [ arguments addObject: @"-p" ]; + if ([ connPort length ]) + [ arguments addObject: connPort]; + else + [ arguments addObject: @"22" ]; + + if (connRemote) + [ arguments addObject: @"-g" ]; + if (compression) + [ arguments addObject: @"-C" ]; + if (v1) + [ arguments addObject: @"-1" ]; + + [ arguments addObject: @"-c"]; + if (encryption) + [ arguments addObject: encryption]; + else + [ arguments addObject: @"3des"]; + + if (socks4 && socks4p != nil) + { + [ arguments addObject: @"-D" ]; + [ arguments addObject: [ socks4p stringValue ]]; + } + [ arguments addObject: [ NSString stringWithFormat: @"%@@%@", + connUser, connHost ] + ]; + + NSString *hostPort; + e = [ tunnelsLocal objectEnumerator ]; + while (t = [ e nextObject ]) + { + [ arguments addObject: @"-L" ]; + if ([[ t objectForKey:@"hostport"] isEqualTo: @"" ]) + hostPort = [ t objectForKey:@"port" ]; + else + hostPort = [ t objectForKey:@"hostport" ]; + [ arguments addObject: [NSString stringWithFormat:@"%@/%@/%@", + [ t objectForKey:@"port"], + [ t objectForKey:@"host"], + hostPort + ] ]; + if ([[ t objectForKey:@"port"] intValue] < 1024) + asRoot=YES; + } + + e = [ tunnelsRemote objectEnumerator ]; + while (t = [ e nextObject ]) + { + [ arguments addObject: @"-R" ]; + if ([[ t objectForKey:@"hostport"] isEqualTo: @"" ]) + hostPort = [ t objectForKey:@"port" ]; + else + hostPort = [ t objectForKey:@"hostport" ]; + [ arguments addObject: [NSString stringWithFormat:@"%@/%@/%@", + [ t objectForKey:@"port"], + [ t objectForKey:@"host"], + hostPort + ]]; + } + args = [ NSMutableDictionary dictionary ]; + [ args setObject: arguments forKey:@"arguments" ]; + [ args setObject: [ NSNumber numberWithBool: connAuth ] forKey: @"handleAuth" ]; + [ args setObject: connName forKey:@"tunnelName" ]; + [ args setObject: [ NSNumber numberWithBool: asRoot ] forKey: @"asRoot" ]; + + + [ NSThread detachNewThreadSelector:@selector(launchTunnel:) + toTarget: self + withObject: args ]; + */ + [ NSThread detachNewThreadSelector:@selector(launchTunnel:) + toTarget: self + withObject: nil ]; +// [ arguments release ]; + +} +- (void)stopTunnel +{ + if (! [ self isRunning ]) + return; + shouldStop=YES; + [ self setValue: nil forKey: @"status" ]; + [ task terminate ]; + code = 0; + [[ NSNotificationCenter defaultCenter] postNotificationName:@"STMStatusChanged" object:self ]; +} + +- (void)toggleTunnel +{ + if ([ self isRunning ]) + [ self stopTunnel ]; + else + [ self startTunnel ]; +} + +- (void)launchTunnel:(id)foo; +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + if (task) + [ task release ]; + task = [[ NSTask alloc ] init ]; + NSMutableDictionary *environment = [ NSMutableDictionary dictionaryWithDictionary: [[ NSProcessInfo processInfo ] environment ]]; + NSString *pathToAuthentifier = [[ NSBundle mainBundle ] pathForResource: @"askForPass" ofType: @"sh" ]; + if (socks4) + [ task setLaunchPath: [[ NSBundle mainBundle ] pathForResource: @"ssh" ofType: @"" ]]; + else + [ task setLaunchPath: @"/usr/bin/ssh" ]; + [ task setArguments: [ self arguments ]]; + if (connAuth) + { + [ environment removeObjectForKey: @"SSH_AGENT_PID" ]; + [ environment removeObjectForKey: @"SSH_AUTH_SOCK" ]; + [ environment setObject: pathToAuthentifier forKey: @"SSH_ASKPASS" ]; + [ environment setObject:@":0" forKey:@"DISPLAY" ]; + } + [ environment setObject: connName forKey: @"TUNNEL_NAME" ]; + [ task setEnvironment: environment ]; + + stdErrPipe = [[ NSPipe alloc ] init ]; + [ task setStandardError: stdErrPipe ]; + + [[ NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(stdErr:) + name: @"NSFileHandleDataAvailableNotification" + object:[ stdErrPipe fileHandleForReading]]; + + [[ stdErrPipe fileHandleForReading] waitForDataInBackgroundAndNotify ]; + + NSLog(T_START,connName); + [ self setValue: S_CONNECTING forKey: @"status" ]; + code = 1; + [ task launch ]; + [[ NSNotificationCenter defaultCenter] postNotificationName:@"STMStatusChanged" object:self ]; + [ task waitUntilExit ]; + sleep(1); + code = 0; + [ self setValue: S_IDLE forKey: @"status" ]; + NSLog(T_STOP,connName); + [[ NSNotificationCenter defaultCenter] removeObserver:self + name: @"NSFileHandleDataAvailableNotification" + object:[ stdErrPipe fileHandleForReading]]; + [ task release ]; + task = nil; + [ stdErrPipe release ]; + stdErrPipe = nil; + [[ NSNotificationCenter defaultCenter] postNotificationName:@"STMStatusChanged" object:self ]; + if (! shouldStop) + [ self startTunnel ]; + [ pool release ]; +} + +- (void)stdErr:(NSNotification*)aNotification +{ + NSData *data = [[ aNotification object ] availableData ]; + NSString *log = [[ NSString alloc ] initWithData: data encoding: NSASCIIStringEncoding ]; + BOOL wait = YES; + if ([ log length ]) + { + //NSLog(log); + NSArray *lines = [ log componentsSeparatedByString:@"\n" ]; + NSEnumerator *e = [ lines objectEnumerator ]; + NSString *line; + while (line = [ e nextObject ]) + { + if ([ line rangeOfString:@"Entering interactive session." ].location != NSNotFound) + { + code = 2; + [ self setValue: S_CONNECTED forKey: @"status"]; + } + if ([ line rangeOfString:@"Authentication succeeded" ].location != NSNotFound) + [ self setValue: S_AUTH forKey: @"status"]; + if ([ line rangeOfString:@"Connections to local port" ].location != NSNotFound) + { + NSScanner *s; + NSString *port; + s = [ NSScanner scannerWithString:log]; + [ s scanUpToString: @"Connections to local port " intoString: nil ]; + [ s scanString: @"Connections to local port " intoString: nil ]; + [ s scanUpToString: @"forwarded" intoString:&port]; + [ self setValue: [ NSString stringWithFormat: @"Port %@ forwarded", port ] forKey: @"status"]; + } + if ([ line rangeOfString:@"closed by remote host." ].location != NSNotFound) + { + [ task terminate]; + [ self setValue: @"Connection closed" forKey: @"status"]; + wait = NO; + } + [[ NSNotificationCenter defaultCenter] postNotificationName:@"STMStatusChanged" object:self ]; + } + if (wait) + [[ stdErrPipe fileHandleForReading ] waitForDataInBackgroundAndNotify ]; + } + [ log release] ; +} + +- (BOOL)isRunning +{ + if ([ task isRunning ]) + return YES; + return NO; +} + +#pragma mark - +#pragma mark Getting tunnel informations +- (NSString *)status +{ + if (status) + return status; + return S_IDLE; +} +- (NSArray*)arguments +{ + NSMutableArray *arguments; + NSEnumerator *e; + NSDictionary *t; + BOOL asRoot; + + arguments = [ NSMutableArray array ]; + [ arguments addObject: @"-N" ]; + [ arguments addObject: @"-v" ]; + [ arguments addObject: @"-p" ]; + if ([ connPort length ]) + [ arguments addObject: connPort]; + else + [ arguments addObject: @"22" ]; + + if (connRemote) + [ arguments addObject: @"-g" ]; + if (compression) + [ arguments addObject: @"-C" ]; + if (v1) + [ arguments addObject: @"-1" ]; + + [ arguments addObject: @"-c"]; + if (encryption) + [ arguments addObject: encryption]; + else + [ arguments addObject: @"3des"]; + + if (socks4 && socks4p != nil) + { + [ arguments addObject: @"-D" ]; + [ arguments addObject: [ socks4p stringValue ]]; + } + [ arguments addObject: [ NSString stringWithFormat: @"%@@%@", + connUser, connHost ] + ]; + + NSString *hostPort; + e = [ tunnelsLocal objectEnumerator ]; + while (t = [ e nextObject ]) + { + [ arguments addObject: @"-L" ]; + if ([[ t objectForKey:@"hostport"] isEqualTo: @"" ]) + hostPort = [ t objectForKey:@"port" ]; + else + hostPort = [ t objectForKey:@"hostport" ]; + [ arguments addObject: [NSString stringWithFormat:@"%@/%@/%@", + [ t objectForKey:@"port"], + [ t objectForKey:@"host"], + hostPort + ] ]; + if ([[ t objectForKey:@"port"] intValue] < 1024) + asRoot=YES; + } + + e = [ tunnelsRemote objectEnumerator ]; + while (t = [ e nextObject ]) + { + [ arguments addObject: @"-R" ]; + if ([[ t objectForKey:@"hostport"] isEqualTo: @"" ]) + hostPort = [ t objectForKey:@"port" ]; + else + hostPort = [ t objectForKey:@"hostport" ]; + [ arguments addObject: [NSString stringWithFormat:@"%@/%@/%@", + [ t objectForKey:@"port"], + [ t objectForKey:@"host"], + hostPort + ]]; + } + + return [[ arguments copy ] autorelease ]; +} + +- (NSDictionary*)dictionary +{ + return [ NSDictionary dictionaryWithObjectsAndKeys: + [ NSNumber numberWithBool: compression ],@"compression", + [ NSNumber numberWithBool: connAuth ],@"connAuth", + [ NSNumber numberWithBool: autoConnect ],@"autoConnect", + connHost, @"connHost", + connName, @"connName", + connPort, @"connPort", + [ NSNumber numberWithBool: connRemote ],@"connRemote", + connUser, @"connUser", + encryption, @"encryption", + [ NSNumber numberWithBool: socks4 ],@"socks4", + socks4p, @"socks4p", + tunnelsLocal, @"tunnelsLocal", + tunnelsRemote, @"tunnelsRemote", + [ NSNumber numberWithBool: v1 ],@"v1", nil + ]; +} + + +#pragma mark - +#pragma mark Key/Value coding +- (NSImage*)icon +{ + switch (code) + { + case 0: + return [ NSImage imageNamed: @"offState" ]; + break; + case 1: + return [ NSImage imageNamed: @"middleState" ]; + break; + case 2: + return [ NSImage imageNamed: @"onState" ]; + break; + } + return [ NSImage imageNamed: @"offState" ]; +} +- (void)setValue:(id)value forUndefinedKey:(NSString *)key +{ + NSLog(@"key %@ undefined",key); +} +- (id)valueForUndefinedKey:(NSString *)key +{ + return nil; +} + +#pragma mark - +#pragma mark Misc. +-(void)dealloc +{ + [ self stopTunnel ]; + [ tunnelsLocal release ]; + [ tunnelsRemote release ]; + + [ task release ]; + [ stdErrPipe release ]; + [ connName release ]; + [ status release ]; + [ connPort release ]; + [ encryption release ]; + [ socks4p release ]; + [ connUser release ]; + [ connHost release ]; + + // start diff lorenz textor + [super dealloc]; + // end diff lorenz textor +} +@end diff --git a/Source/SSHTunnel_old.h b/Source/SSHTunnel_old.h new file mode 100644 index 00000000..b1daa595 --- /dev/null +++ b/Source/SSHTunnel_old.h @@ -0,0 +1,41 @@ +/* + +SSHTunnel.h + +Original code by tynsoe.org, Copyright 2002 +Modified by Lorenz Textor for use with Sequel Pro + +*/ + +#import <Cocoa/Cocoa.h> + +@interface SSHTunnel : NSObject +{ + BOOL shouldStop; + NSTask *task; + NSPipe *stdErrPipe; + NSString *status; + + NSDictionary *tunnelArguments; +} + +// initialization +- (id)init; + +// Getting tunnels informations +- (BOOL)isRunning; +- (NSString *)status; + +// starting & stopping the tunnel +- (void)startTunnel; +- (void)startTunnelWithArguments:(NSDictionary *)args; +- (void)stopTunnel; +- (void)launchTunnel:(NSArray*)arguments; +- (void)stdErr:(NSNotification*)aNotification; +- (id)authenticate:(NSScriptCommand *)command; +- (id)handleQuitScriptCommand:(NSScriptCommand *)command; + +// deallocation +- (void) dealloc; + +@end diff --git a/Source/SSHTunnel_old.m b/Source/SSHTunnel_old.m new file mode 100644 index 00000000..8080a9bf --- /dev/null +++ b/Source/SSHTunnel_old.m @@ -0,0 +1,225 @@ +/* + +SSHTunnel.m + +Original code by tynsoe.org, Copyright 2002 +Modified by Lorenz Textor for use with Sequel Pro + +*/ + +#import "SSHTunnel.h" +#include <unistd.h> + +// start diff lorenz textor +/* +#define T_START @"START: %@" +#define T_STOP @"STOP: %@" +#define S_IDLE @"Idle" +#define S_CONNECTING @"Connecting..." +#define S_CONNECTED @"Connected" +#define S_AUTH @"Authenticated" +#define S_PORT "Port %@ forwarded" +*/ +#define T_START @"START: %@" +#define T_STOP @"STOP: %@" +#define S_IDLE @"Idle" +#define S_CONNECTING @"Connecting..." +#define S_CONNECTED @"Connected" +#define S_AUTH @"Authenticated" +#define S_PORT "Port %@ forwarded" +// end diff lorenz textor + +@implementation SSHTunnel + +// initialization +- (id)init +{ + self = [super init]; + + // Make this class the root one for AppleEvent calls +// [[ NSScriptExecutionContext sharedScriptExecutionContext] setTopLevelObject: self ]; + + return self; +} + +// Getting tunnels informations +- (BOOL)isRunning +/* returns YES if tunnel is running */ +{ + return [ task isRunning ]; +} + +- (NSString*)status +{ + if (status) + return status; + return S_IDLE; +} + +// starting & stopping the tunnel +- (void)startTunnel +/* starts tunnel with saved arguments */ +{ + [self startTunnelWithArguments:tunnelArguments]; +} + +- (void)startTunnelWithArguments:(NSDictionary *)args +/* starts the tunnel */ +{ + NSMutableArray *arguments = [[ NSMutableArray alloc] init ]; + + if (tunnelArguments ) + [tunnelArguments release]; + tunnelArguments = [args retain]; + + // stop tunnel if already running + if ( [self isRunning] ) +// [self stopTunnel]; + return; + + shouldStop = NO; + +// get arguments + [ arguments addObject: @"-N" ]; + [ arguments addObject: @"-v" ]; + +// [ arguments addObject: @"-p" ]; +// [ arguments addObject: @"-p" ]; +// [ arguments addObject: @"22" ]; + +// [ arguments addObject: @"-c"]; +// [ arguments addObject: @"3des"]; + + [ arguments addObject: [ NSString stringWithFormat: @"%@@%@", [args objectForKey:@"connUser"], [args objectForKey:@"connHost"] ]]; + + [ arguments addObject: @"-L" ]; + [ arguments addObject: [NSString stringWithFormat:@"%@/%@/%@", [args objectForKey:@"localPort"], [args objectForKey:@"host"], [args objectForKey:@"remotePort"]] ]; + + [ NSThread detachNewThreadSelector:@selector(launchTunnel:) + toTarget: self + withObject: arguments ]; + + [ arguments release ]; +} + +- (void)stopTunnel +/* stops the tunnel */ +{ + if (! [ self isRunning ]) + return; + shouldStop=YES; + [ self setValue: nil forKey: @"status" ]; + [ task terminate ]; + [[ NSNotificationCenter defaultCenter] postNotificationName:@"STMStatusChanged" object:self ]; +} + +- (void)launchTunnel:(NSArray*)arguments +/* launches the tunnel in a separate thread */ +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + if (task) + [ task release ]; + task = [[ NSTask alloc ] init ]; + NSMutableDictionary *environment = [ NSMutableDictionary dictionaryWithDictionary: [[ NSProcessInfo processInfo ] environment ]]; + NSString *pathToAuthentifier = [[ NSBundle mainBundle ] pathForResource: @"askForPass" ofType: @"sh" ]; + + [ task setLaunchPath: @"/usr/bin/ssh" ]; + [ task setArguments: arguments ]; + +// really necessary??? + [ environment removeObjectForKey: @"SSH_AGENT_PID" ]; + [ environment removeObjectForKey: @"SSH_AUTH_SOCK" ]; + [ environment setObject: pathToAuthentifier forKey: @"SSH_ASKPASS" ]; + [ environment setObject:@":0" forKey:@"DISPLAY" ]; + + [ environment setObject: @"Sequel Pro Tunnel" forKey: @"TUNNEL_NAME" ]; + [ task setEnvironment: environment ]; + + stdErrPipe = [[ NSPipe alloc ] init ]; + [ task setStandardError: stdErrPipe ]; + + [[ NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(stdErr:) + name: @"NSFileHandleDataAvailableNotification" + object:[ stdErrPipe fileHandleForReading]]; + + [[ stdErrPipe fileHandleForReading] waitForDataInBackgroundAndNotify ]; + + NSLog(T_START,@"Sequel Pro Tunnel"); + [ self setValue: S_CONNECTING forKey: @"status" ]; + [ task launch ]; + [[ NSNotificationCenter defaultCenter] postNotificationName:@"STMStatusChanged" object:self ]; + [ task waitUntilExit ]; + sleep(1); + [ self setValue: S_IDLE forKey: @"status" ]; + NSLog(T_STOP,@"Sequel Pro Tunnel"); + [[ NSNotificationCenter defaultCenter] removeObserver:self + name: @"NSFileHandleDataAvailableNotification" + object:[ stdErrPipe fileHandleForReading]]; + [ task release ]; + task = nil; + [ stdErrPipe release ]; + stdErrPipe = nil; + [[ NSNotificationCenter defaultCenter] postNotificationName:@"STMStatusChanged" object:self ]; + if (! shouldStop) + [ self startTunnel ]; + [ pool release ]; +} + +- (void)stdErr:(NSNotification*)aNotification +{ + NSData *data = [[ aNotification object ] availableData ]; + NSString *log = [[ NSString alloc ] initWithData: data encoding: NSASCIIStringEncoding ]; + BOOL wait = YES; + if ([ log length ]) + { + NSLog(log); + NSArray *lines = [ log componentsSeparatedByString:@"\n" ]; + NSEnumerator *e = [ lines objectEnumerator ]; + NSString *line; + while (line = [ e nextObject ]) + { + if ([ line rangeOfString:@"Entering interactive session." ].location != NSNotFound) + { + [ self setValue: S_CONNECTED forKey: @"status"]; + } + if ([ line rangeOfString:@"Authentication succeeded" ].location != NSNotFound) + [ self setValue: S_AUTH forKey: @"status"]; + if ([ line rangeOfString:@"Connections to local port" ].location != NSNotFound) + { + NSScanner *s; + NSString *port; + s = [ NSScanner scannerWithString:log]; + [ s scanUpToString: @"Connections to local port " intoString: nil ]; + [ s scanString: @"Connections to local port " intoString: nil ]; + [ s scanUpToString: @"forwarded" intoString:&port]; + [ self setValue: [ NSString stringWithFormat: @"Port %@ forwarded", port ] forKey: @"status"]; + } + if ([ line rangeOfString:@"closed by remote host." ].location != NSNotFound) + { + [ task terminate]; + [ self setValue: @"Connection closed" forKey: @"status"]; + wait = NO; + } + [[ NSNotificationCenter defaultCenter] postNotificationName:@"STMStatusChanged" object:self ]; + } + if (wait) + [[ stdErrPipe fileHandleForReading ] waitForDataInBackgroundAndNotify ]; + } + [ log release] ; +} + +// deallocation +- (void) dealloc +{ + [self stopTunnel]; + + [task release]; + [stdErrPipe release]; + [status release]; + [tunnelArguments release]; + + [super dealloc]; +} + +@end diff --git a/Source/TableContent.h b/Source/TableContent.h new file mode 100644 index 00000000..54ad2481 --- /dev/null +++ b/Source/TableContent.h @@ -0,0 +1,135 @@ +// +// 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 <Cocoa/Cocoa.h> +#import <MCPKit_bundled/MCPKit_bundled.h> +#import "CMCopyTable.h" +#import "CMMCPConnection.h" +#import "CMMCPResult.h" + + +@interface TableContent : NSObject { + + IBOutlet id tableDocumentInstance; + IBOutlet id tablesListInstance; + IBOutlet id tableSourceInstance; + + IBOutlet id tableWindow; + IBOutlet CMCopyTable *tableContentView; + IBOutlet id editSheet; + IBOutlet id editImage; + IBOutlet id editTextView; + IBOutlet id hexTextView; + IBOutlet id fieldField; + IBOutlet id compareField; + IBOutlet id argumentField; + IBOutlet id filterButton; + IBOutlet id addButton; + IBOutlet id copyButton; + IBOutlet id removeButton; + IBOutlet id multipleLineEditingButton; + IBOutlet id countText; + IBOutlet id limitRowsField; + IBOutlet id limitRowsButton; + IBOutlet id limitRowsStepper; + IBOutlet id limitRowsText; + + CMMCPConnection *mySQLConnection; + + id editData; + NSString *selectedTable; + NSMutableArray *fullResult, *filteredResult, *keys; + NSMutableDictionary *oldRow; + NSArray *fieldNames, *fieldTypes; + NSString *compareType, *sortField; + BOOL isEditingRow, isEditingNewRow, isDesc, setLimit; + NSUserDefaults *prefs; + int numRows; + bool areShowingAllRows; + +} + +//table methods +- (void)loadTable:(NSString *)aTable; +- (IBAction)reloadTable:(id)sender; +- (IBAction)reloadTableValues:(id)sender; +- (IBAction)filterTable:(id)sender; +- (IBAction)showAll:(id)sender; +- (IBAction)toggleFilterField:(id)sender; + +//edit methods +- (IBAction)addRow:(id)sender; +- (IBAction)copyRow:(id)sender; +- (IBAction)removeRow:(id)sender; + +//editSheet methods +- (IBAction)closeEditSheet:(id)sender; +- (IBAction)openEditSheet:(id)sender; +- (IBAction)saveEditSheet:(id)sender; +- (IBAction)dropImage:(id)sender; +- (void)textDidChange:(NSNotification *)notification; +- (NSString *)dataToHex:(NSData *)data; + +//getter methods +- (NSArray *)currentResult; + +//additional methods +- (void)setConnection:(CMMCPConnection *)theConnection; +- (IBAction)setCompareTypes:(id)sender; +- (IBAction)stepLimitRows:(id)sender; +- (NSArray *)fetchResultAsArray:(CMMCPResult *)theResult; +- (BOOL)addRowToDB; +- (NSString *)argumentForRow:(int)row; +- (BOOL)isBlobOrText:(NSString *)fieldType; +- (NSString *)fieldListForQuery; +- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo; +- (int)getNumberOfRows; +- (int)fetchNumberOfRows; + +//tableView datasource methods +- (int)numberOfRowsInTableView:(NSTableView *)aTableView; +- (id)tableView:(NSTableView *)aTableView +objectValueForTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex; +- (void)tableView:(NSTableView *)aTableView + setObjectValue:(id)anObject + forTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex; + +//tableView delegate methods +- (void)tableView:(NSTableView*)tableView didClickTableColumn:(NSTableColumn *)tableColumn; +- (BOOL)selectionShouldChangeInTableView:(NSTableView *)aTableView; +- (void)tableViewSelectionDidChange:(NSNotification *)aNotification; +- (void)tableViewSelectionIsChanging:(NSNotification *)aNotification; +- (void)tableViewColumnDidResize:(NSNotification *)aNotification; +- (BOOL)tableView:(NSTableView *)aTableView shouldEditTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex; +- (BOOL)tableView:(NSTableView *)tableView writeRows:(NSArray*)rows toPasteboard:(NSPasteboard*)pboard; +- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command; + +//textView delegate methods +- (BOOL)textView:(NSTextView *)aTextView doCommandBySelector:(SEL)aSelector; + +@end diff --git a/Source/TableContent.m b/Source/TableContent.m new file mode 100644 index 00000000..1aa939f2 --- /dev/null +++ b/Source/TableContent.m @@ -0,0 +1,2018 @@ +// +// 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 "TableContent.h" +#import "TableDocument.h" +#import "TablesList.h" +#import "TableSource.h" +#import "CMImageView.h" + + +@implementation TableContent + +- (id)init +{ + if (![super init]) + return nil; + + fullResult = [[NSMutableArray alloc] init]; + filteredResult = [[NSMutableArray alloc] init]; + oldRow = [[NSMutableDictionary alloc] init]; + selectedTable = nil; + sortField = nil; + areShowingAllRows = false; + + return self; +} + +- (void)awakeFromNib +{ +} + +/* + Loads aTable, retrieving column information and updating the tableViewColumns before + reloading table data into the fullResults array and redrawing the table. + */ +- (void)loadTable:(NSString *)aTable +{ + int i; + NSNumber *colWidth; + NSArray *theColumns; + NSTableColumn *theCol; + NSString *query; + CMMCPResult *queryResult; + BOOL preserveCurrentView = [aTable isEqualToString:selectedTable]; + NSString *preservedFilterField = nil, *preservedFilterComparison, *preservedFilterValue; + + // Clear the selection, and abort the reload if the user is still editing a row + [tableContentView deselectAll:self]; + if ( isEditingRow ) + return; + + // Store the newly selected table name + selectedTable = aTable; + + // Reset table key store for use in argumentForRow: + if ( keys ) + keys = nil; + + // 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 ( [aTable isEqualToString:@""] || !aTable ) + { + + // Empty the table and stored data arrays + theColumns = [tableContentView tableColumns]; + while ([theColumns count]) { + [tableContentView removeTableColumn:[theColumns objectAtIndex:0]]; + } + [fullResult removeAllObjects]; + [filteredResult removeAllObjects]; + [tableContentView reloadData]; + areShowingAllRows = YES; + [countText setStringValue:@""]; + + // 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 setEnabled:NO]; + [argumentField setStringValue:@""]; + [filterButton setEnabled:NO]; + + // Empty and disable the limit field + [limitRowsField setStringValue:@""]; + [limitRowsText setStringValue:NSLocalizedString(@"No limit", @"text showing that the result isn't limited")]; + [limitRowsField setEnabled:NO]; + [limitRowsButton setEnabled:NO]; + [limitRowsStepper setEnabled:NO]; + + // Disable table action buttons + [addButton setEnabled:NO]; + [copyButton setEnabled:NO]; + [removeButton setEnabled:NO]; + + return; + } + + // Post a notification that a query will be performed + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; + + + // Make a fast query to get fieldNames and fieldTypes for this table. This is used to decide whether to preserve the + // current filter/sort settings, and also used when grabbing all the data as part of the fieldListForQuery method. + queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SELECT * FROM `%@` LIMIT 0", selectedTable]]; + if ( queryResult == nil ) { + NSLog(@"Loading table columns for %@ failed", aTable); + return; + } + fieldNames = [[queryResult fetchFieldNames] retain]; + fieldTypes = [[queryResult fetchTypesAsArray] retain]; + + // Retrieve the number of rows in the table and initially mark all as being visible. + numRows = [self getNumberOfRows]; + areShowingAllRows = YES; + + // Remove existing columns from the table + theColumns = [tableContentView tableColumns]; + while ([theColumns count]) { + [tableContentView removeTableColumn:[theColumns objectAtIndex:0]]; + } + + // Add the new columns to the table + for ( i = 0 ; i < [fieldNames count] ; i++ ) { + + // Set up the column + theCol = [[NSTableColumn alloc] initWithIdentifier:[fieldNames objectAtIndex:i]]; + [theCol setEditable:YES]; + if ( [theCol respondsToSelector:@selector(setResizingMask:)] ) { + // Mac OS X 10.4+ + [theCol setResizingMask:NSTableColumnUserResizingMask]; + } else { + // Mac OS X pre-10.4 + [theCol setResizable:YES]; + } + [[theCol headerCell] setStringValue:[fieldNames objectAtIndex:i]]; + + // Set up the data cell depending on the column type + NSComboBoxCell *dataCell; + if ( [[tableSourceInstance enumFields] objectForKey:[fieldNames objectAtIndex:i]] ) + { + dataCell = [[[NSComboBoxCell alloc] initTextCell:@""] autorelease]; + [dataCell setButtonBordered:NO]; + [dataCell setBezeled:NO]; + [dataCell setDrawsBackground:NO]; + [dataCell setCompletes:YES]; + [dataCell setControlSize:NSSmallControlSize]; + [dataCell addItemWithObjectValue:@"NULL"]; + [dataCell addItemsWithObjectValues:[[tableSourceInstance enumFields] objectForKey:[fieldNames objectAtIndex:i]]]; + } + else + { + dataCell = [[[NSTextFieldCell alloc] initTextCell:@""] autorelease]; + } + [dataCell setEditable:YES]; + + if ( [dataCell respondsToSelector:@selector(setLineBreakMode:)] ) { + // Mac OS X 10.4+ + [dataCell setLineBreakMode:NSLineBreakByTruncatingTail]; + } + + // Set the data cell font according to the preferences + if ( [prefs boolForKey:@"useMonospacedFonts"] ) + { + [dataCell setFont:[NSFont fontWithName:@"Monaco" size:10]]; + } + else + { + [dataCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + } + + // Assign the data cell + [theCol setDataCell:dataCell]; + + // Set the width of this column to saved value if exists + colWidth = [[[[prefs objectForKey:@"tableColumnWidths"] objectForKey:[NSString stringWithFormat:@"%@@%@", [tableDocumentInstance database], [tableDocumentInstance host]]] objectForKey:[tablesListInstance table]] objectForKey:[fieldNames objectAtIndex:i]]; + if ( colWidth ) + { + [theCol setWidth:[colWidth floatValue]]; + } + + // 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 (preserveCurrentView && [fieldNames containsObject:sortField]) + { + theCol = [tableContentView tableColumnWithIdentifier:sortField]; + [tableContentView setHighlightedTableColumn:theCol]; + if ( isDesc ) { + [tableContentView setIndicatorImage:[NSImage imageNamed:@"NSDescendingSortIndicator"] inTableColumn:theCol]; + } else { + [tableContentView setIndicatorImage:[NSImage imageNamed:@"NSAscendingSortIndicator"] inTableColumn:theCol]; + } + } + + // Otherwise, clear sorting + else + { + sortField = nil; + isDesc = NO; + } + + // Preserve the stored filter settings if appropriate + if (preserveCurrentView && [fieldField isEnabled]) + { + preservedFilterField = [NSString stringWithString:[[fieldField selectedItem] title]]; + preservedFilterComparison = [NSString stringWithString:[[compareField selectedItem] title]]; + preservedFilterValue = [NSString stringWithString:[argumentField stringValue]]; + } + + // Enable and initialize filter fields (with tags for position of menu item and field position) + [fieldField setEnabled:YES]; + [fieldField removeAllItems]; + [fieldField addItemsWithTitles:fieldNames]; + 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:YES]; + + // Restore preserved filter settings if appropriate and valid + if (preserveCurrentView && preservedFilterField != nil && [fieldField itemWithTitle:preservedFilterField]) + { + [fieldField selectItemWithTitle:preservedFilterField]; + [self setCompareTypes:self]; + } + if (preserveCurrentView && preservedFilterField != nil + && [fieldField itemWithTitle:preservedFilterField] + && [compareField itemWithTitle:preservedFilterComparison]) + { + [compareField selectItemWithTitle:preservedFilterComparison]; + [argumentField setStringValue:preservedFilterValue]; + areShowingAllRows = NO; + } + + // Enable or disable the limit fields according to preference setting + if ( [prefs boolForKey:@"limitRows"] ) + { + + // Attempt to preserve the limit value if it's still valid + if (!preserveCurrentView || [limitRowsField intValue] < 1 || [limitRowsField intValue] >= numRows) { + [limitRowsField setStringValue:@"1"]; + } + [limitRowsField setEnabled:YES]; + [limitRowsButton setEnabled:YES]; + [limitRowsStepper setEnabled:YES]; + [limitRowsText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Limited to %d rows starting with row", @"text showing the number of rows the result is limited to"), + [prefs integerForKey:@"limitRowsValue"]]]; + if ([prefs integerForKey:@"limitRowsValue"] < numRows) + areShowingAllRows = NO; + } + else + { + [limitRowsField setEnabled:NO]; + [limitRowsButton setEnabled:NO]; + [limitRowsStepper setEnabled:NO]; + [limitRowsField setStringValue:@""]; + [limitRowsText setStringValue:NSLocalizedString(@"No limit", @"text showing that the result isn't limited")]; + } + + // Enable the table buttons + [addButton setEnabled:YES]; + [copyButton setEnabled:YES]; + [removeButton setEnabled:YES]; + + + // Perform the data query and store the result as an array containing a dictionary per result row + query = [NSString stringWithFormat:@"SELECT %@ FROM `%@`", [self fieldListForQuery], selectedTable]; + if ( sortField ) + { + query = [NSString stringWithFormat:@"%@ ORDER BY `%@`", query, sortField]; + if ( isDesc ) + query = [query stringByAppendingString:@" DESC"]; + } + if ( [prefs boolForKey:@"limitRows"] ) + { + if ( [limitRowsField intValue] <= 0 ) + { + [limitRowsField setStringValue:@"1"]; + } + query = [query stringByAppendingString: + [NSString stringWithFormat:@" LIMIT %d,%d", + [limitRowsField intValue]-1, [prefs integerForKey:@"limitRowsValue"]]]; + } + queryResult = [mySQLConnection queryString:query]; + if ( queryResult == nil ) { + NSLog(@"Loading table data for %@ failed, query string was: %@", aTable, query); + return; + } + [fullResult setArray:[self fetchResultAsArray:queryResult]]; + + // Apply any filtering and update the row count + if ( !areShowingAllRows ) { + [self filterTable:self]; + [countText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%d rows of %d selected", @"text showing how many rows are in the filtered result"), [filteredResult count], numRows]]; + } else { + [filteredResult setArray:fullResult]; + [countText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%d rows in table", @"text showing how many rows are in the result"), numRows]]; + } + + // Reload the table data. + [tableContentView reloadData]; + + // Post the notification that the query is finished + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; +} + +/* + Reloads the current table data, performing a new SQL query. Now attempts to preserve sort order, filters, and viewport. + */ +- (IBAction)reloadTable:(id)sender +{ + // Store the current viewport location + NSRect viewRect = [tableContentView visibleRect]; + + [self loadTable:selectedTable]; + + // Restore the viewport + [tableContentView scrollRectToVisible:viewRect]; +} + +- (IBAction)reloadTableValues:(id)sender +/* + reload the table values without reconfiguring the tableView (with filter and limit if set) + */ +{ + NSString *queryString; + CMMCPResult *queryResult; + + //query started + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; + + //enable or disable limit fields + if ( [prefs boolForKey:@"limitRows"] ) { + [limitRowsField setEnabled:YES]; + [limitRowsButton setEnabled:YES]; + [limitRowsStepper setEnabled:YES]; + [limitRowsText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Limited to %d rows starting with row", @"text showing the number of rows the result is limited to"), + [prefs integerForKey:@"limitRowsValue"]]]; + } else { + [limitRowsField setEnabled:NO]; + [limitRowsButton setEnabled:NO]; + [limitRowsStepper setEnabled:NO]; + [limitRowsText setStringValue:NSLocalizedString(@"No limit", @"text showing that the result isn't limited")]; + [limitRowsField setStringValue:@""]; + } + + // queryString = [@"SELECT * FROM " stringByAppendingString:selectedTable]; + queryString = [NSString stringWithFormat:@"SELECT %@ FROM `%@`", [self fieldListForQuery], selectedTable]; + if ( sortField ) { + queryString = [NSString stringWithFormat:@"%@ ORDER BY `%@`", queryString, sortField]; + // queryString = [queryString stringByAppendingString:[NSString stringWithFormat:@" ORDER BY `%@`", sortField]]; + if ( isDesc ) + queryString = [queryString stringByAppendingString:@" DESC"]; + } + if ( [prefs boolForKey:@"limitRows"] ) { + if ( [limitRowsField intValue] <= 0 ) { + [limitRowsField setStringValue:@"1"]; + } + queryString = [queryString stringByAppendingString: + [NSString stringWithFormat:@" LIMIT %d,%d", + [limitRowsField intValue]-1, [prefs integerForKey:@"limitRowsValue"]]]; + [limitRowsField selectText:self]; + } + queryResult = [mySQLConnection queryString:queryString]; + // [fullResult setArray:[[self fetchResultAsArray:queryResult] retain]]; + [fullResult setArray:[self fetchResultAsArray:queryResult]]; + numRows = [self getNumberOfRows]; + if ( !areShowingAllRows ) { + [self filterTable:self]; + [countText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%d rows of %d selected", @"text showing how many rows are in the filtered result"), [filteredResult count], numRows]]; + } else { + [filteredResult setArray:fullResult]; + [countText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%d rows in table", @"text showing how many rows are in the result"), numRows]]; + } + [tableContentView reloadData]; + + //query finished + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; +} + +/** + * filter the table with arguments given by the user + */ +- (IBAction)filterTable:(id)sender +{ + CMMCPResult *theResult; + int tag = [[compareField selectedItem] tag]; + NSString *compareOperator = @""; + NSMutableString *argument = [[NSMutableString alloc] initWithString:[argumentField stringValue]]; + NSString *queryString; + int i; + + // Update negative limits + if ( [limitRowsField intValue] <= 0 ) { + [limitRowsField setStringValue:@"1"]; + } + + // If the filter field is empty, the limit field is at 1, and the selected filter is not looking + // for NULLs or NOT NULLs, then don't allow filtering. + if (([argument length] == 0) && (![[[compareField selectedItem] title] hasSuffix:@"NULL"]) && (![prefs boolForKey:@"limitRows"] || [limitRowsField intValue] == 1)) { + [argument release]; + [self showAll:sender]; + return; + } + + //query started + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; + + BOOL doQuote = YES; + BOOL ignoreArgument = NO; + + // Start building the query string + queryString = [NSString stringWithFormat:@"SELECT %@ FROM `%@`", + [self fieldListForQuery], selectedTable]; + + // Add filter if appropriate + if (([argument length] > 0) || [[[compareField selectedItem] title] hasSuffix:@"NULL"]) { + if ( ![compareType isEqualToString:@""] ) { + if ( [compareType isEqualToString:@"string"] ) { + //string comparision + switch ( tag ) { + case 0: + compareOperator = @"LIKE"; + break; + case 1: + compareOperator = @"NOT LIKE"; + break; + case 2: + compareOperator = @"LIKE"; + [argument setString:[[@"%" stringByAppendingString:argument] stringByAppendingString:@"%"]]; + break; + case 3: + compareOperator = @"NOT LIKE"; + [argument setString:[[@"%" stringByAppendingString:argument] stringByAppendingString:@"%"]]; + break; + case 4: + compareOperator = @"IN"; + doQuote = NO; + [argument setString:[[@"(" stringByAppendingString:argument] stringByAppendingString:@")"]]; + break; + case 5: + compareOperator = @"IS NULL"; + doQuote = NO; + ignoreArgument = YES; + break; + case 6: + compareOperator = @"IS NOT NULL"; + doQuote = NO; + ignoreArgument = YES; + break; + } + } else if ( [compareType isEqualToString:@"number"] ) { + //number comparision + switch ( tag ) { + case 0: + compareOperator = @"="; + break; + case 1: + compareOperator = @"!="; + break; + case 2: + compareOperator = @">"; + break; + case 3: + compareOperator = @"<"; + break; + case 4: + compareOperator = @">="; + break; + case 5: + compareOperator = @"<="; + break; + case 6: + compareOperator = @"IN"; + doQuote = NO; + [argument setString:[[@"(" stringByAppendingString:argument] stringByAppendingString:@")"]]; + break; + case 7: + compareOperator = @"IS NULL"; + doQuote = NO; + ignoreArgument = YES; + break; + case 8: + compareOperator = @"IS NOT NULL"; + doQuote = NO; + ignoreArgument = YES; + break; + } + } else if ( [compareType isEqualToString:@"date"] ) { + //date comparision + switch ( tag ) { + case 0: + compareOperator = @"="; + break; + case 1: + compareOperator = @"!="; + break; + case 2: + compareOperator = @"<"; + break; + case 3: + compareOperator = @">"; + break; + case 4: + compareOperator = @"<="; + break; + case 5: + compareOperator = @">="; + break; + case 6: + compareOperator = @"IS NULL"; + doQuote = NO; + ignoreArgument = YES; + break; + case 7: + compareOperator = @"IS NOT NULL"; + doQuote = NO; + ignoreArgument = YES; + break; + } + } else { + doQuote = NO; + ignoreArgument = YES; + NSLog(@"ERROR: unknown compare type %@", compareType); + } + + if (doQuote) { + //escape special characters + for ( i = 0 ; i < [argument length] ; i++ ) { + if ( [argument characterAtIndex:i] == '\\' ) { + [argument insertString:@"\\" atIndex:i]; + i++; + } + } + [argument setString:[mySQLConnection prepareString:argument]]; + queryString = [NSString stringWithFormat:@"%@ WHERE `%@` %@ \"%@\"", + queryString, [fieldField titleOfSelectedItem], compareOperator, argument]; + } else { + queryString = [NSString stringWithFormat:@"%@ WHERE `%@` %@ %@", + queryString, [fieldField titleOfSelectedItem], + compareOperator, (ignoreArgument) ? @"" : argument]; + } + } + } + + // Add sorting details if appropriate + if ( sortField ) { + queryString = [NSString stringWithFormat:@"%@ ORDER BY `%@`", queryString, sortField]; + if ( isDesc ) + queryString = [queryString stringByAppendingString:@" DESC"]; + } + + // LIMIT if appropriate + if ( [prefs boolForKey:@"limitRows"] ) { + queryString = [NSString stringWithFormat:@"%@ LIMIT %d,%d", queryString, + [limitRowsField intValue]-1, [prefs integerForKey:@"limitRowsValue"]]; + } + + theResult = [mySQLConnection queryString:queryString]; + [filteredResult setArray:[self fetchResultAsArray:theResult]]; + + [countText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%d rows of %d selected", @"text showing how many rows are in the filtered result"), [filteredResult count], numRows]]; + + // Reset the table view + [tableContentView scrollPoint:NSMakePoint(0.0, 0.0)]; + [tableContentView reloadData]; + areShowingAllRows = NO; + + //query finished + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; + [argument release]; +} + +/** + * reload tableView with all results shown (no new mysql-query, it uses simply the fullResult array) + */ +- (IBAction)showAll:(id)sender +{ + [filteredResult setArray:fullResult]; + [tableContentView reloadData]; + areShowingAllRows = YES; + + [countText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%d rows in table", @"text showing how many rows are in the result"), numRows]]; +} + +/** + * Enables or disables the filter input field based on the selected filter type. + */ +- (IBAction)toggleFilterField:(id)sender +{ + // If the user is filtering for NULLs then disabled the filter field, otherwise enable it. + [argumentField setEnabled:(![[[compareField selectedItem] title] hasSuffix:@"NULL"])]; +} + + +//edit methods +- (IBAction)addRow:(id)sender +/* + adds an empty row to the table-array and goes into edit mode + */ +{ + NSMutableDictionary *newRow = [NSMutableDictionary dictionary]; + int i; + + if ( ![self selectionShouldChangeInTableView:nil] ) + return; + + for ( i = 0 ; i < [fieldNames count] ; i++ ) { + // [newRow setObject:[prefs stringForKey:@"nullValue"] forKey:[fieldNames objectAtIndex:i]]; + [newRow setObject:[tableSourceInstance defaultValueForField:[fieldNames objectAtIndex:i]] + forKey:[fieldNames objectAtIndex:i]]; + } + [filteredResult addObject:newRow]; + + isEditingRow = YES; + isEditingNewRow = YES; + [tableContentView reloadData]; + [tableContentView selectRow:[tableContentView numberOfRows]-1 byExtendingSelection:NO]; + if ( [multipleLineEditingButton state] == NSOffState ) + [tableContentView editColumn:0 row:[tableContentView numberOfRows]-1 withEvent:nil select:YES]; +} + +- (IBAction)copyRow:(id)sender +/* + copies a row of the table-array and goes into edit mode + */ +{ + NSMutableDictionary *tempRow; + CMMCPResult *queryResult; + NSDictionary *row; + int i; + + if ( ![self selectionShouldChangeInTableView:nil] ) + return; + if ( [tableContentView numberOfSelectedRows] < 1 ) + return; + if ( [tableContentView numberOfSelectedRows] > 1 ) { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, NSLocalizedString(@"You can only copy single rows.", @"message of panel when trying to copy multiple rows")); + return; + } + + //copy row + tempRow = [NSMutableDictionary dictionaryWithDictionary:[filteredResult objectAtIndex:[tableContentView selectedRow]]]; + [filteredResult insertObject:tempRow atIndex:[tableContentView selectedRow]+1]; + isEditingRow = YES; + isEditingNewRow = YES; + //set autoincrement fields to NULL + queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM `%@`", selectedTable]]; + if ([queryResult numOfRows]) [queryResult dataSeek:0]; + for ( i = 0 ; i < [queryResult numOfRows] ; i++ ) { + row = [queryResult fetchRowAsDictionary]; + if ( [[row objectForKey:@"Extra"] isEqualToString:@"auto_increment"] ) { + [tempRow setObject:[prefs stringForKey:@"nullValue"] forKey:[row objectForKey:@"Field"]]; + } + } + //select row and go in edit mode + [tableContentView reloadData]; + [tableContentView selectRow:[tableContentView selectedRow]+1 byExtendingSelection:NO]; + if ( [multipleLineEditingButton state] == NSOffState ) + [tableContentView editColumn:0 row:[tableContentView selectedRow] withEvent:nil select:YES]; +} + +- (IBAction)removeRow:(id)sender +/* + asks user if he really wants to delete the selected rows + */ +{ + if ( ![self selectionShouldChangeInTableView:nil] ) + return; + if ( ![tableContentView numberOfSelectedRows] ) + return; + /* + if ( ([tableContentView numberOfSelectedRows] == [self numberOfRowsInTableView:tableContentView]) && + areShowingAllRows && + (![prefs boolForKey:@"limitRows"] || ([tableContentView numberOfSelectedRows] < [prefs integerForKey:@"limitRowsValue"])) ) { + */ + if ( ([tableContentView numberOfSelectedRows] == [tableContentView numberOfRows]) && + (([prefs boolForKey:@"limitRows"] && [tableContentView numberOfSelectedRows] == [self fetchNumberOfRows]) || + (![prefs boolForKey:@"limitRows"] && [tableContentView numberOfSelectedRows] == [self getNumberOfRows])) ) { + NSBeginAlertSheet(NSLocalizedString(@"Warning", @"warning"), NSLocalizedString(@"Delete", @"delete button"), NSLocalizedString(@"Cancel", @"cancel button"), nil, tableWindow, self, @selector(sheetDidEnd:returnCode:contextInfo:), + nil, @"removeallrows", NSLocalizedString(@"Do you really want to delete all rows?", @"message of panel asking for confirmation for deleting all rows")); + } else if ( [tableContentView numberOfSelectedRows] == 1 ) { + NSBeginAlertSheet(NSLocalizedString(@"Warning", @"warning"), NSLocalizedString(@"Delete", @"delete button"), NSLocalizedString(@"Cancel", @"cancel button"), nil, tableWindow, self, @selector(sheetDidEnd:returnCode:contextInfo:), + nil, @"removerow", NSLocalizedString(@"Do you really want to delete the selected row?", @"message of panel asking for confirmation for deleting the selected row")); + } else { + NSBeginAlertSheet(NSLocalizedString(@"Warning", @"warning"), NSLocalizedString(@"Delete", @"delete button"), NSLocalizedString(@"Cancel", @"cancel button"), nil, tableWindow, self, @selector(sheetDidEnd:returnCode:contextInfo:), + nil, @"removerow", + [NSString stringWithFormat:NSLocalizedString(@"Do you really want to delete the selected %d rows?", @"message of panel asking for confirmation for deleting the selected rows"), [tableContentView numberOfSelectedRows]]); + } +} + + +//editSheet methods +- (IBAction)closeEditSheet:(id)sender +{ + [NSApp stopModalWithCode:[sender tag]]; +} + +- (IBAction)openEditSheet:(id)sender +/* + loads a file into the editSheet + */ +{ + NSOpenPanel *panel = [NSOpenPanel openPanel]; + + if ( [panel runModal] == NSOKButton ) { + NSString *fileName = [panel filename]; + + // free old data + if ( editData != nil ) { + [editData release]; + } + + // load new data/images + editData = [[NSData alloc] initWithContentsOfFile:fileName]; + NSImage *image = [[[NSImage alloc] initByReferencingFile:fileName] autorelease]; + NSString *contents = [[NSString stringWithContentsOfFile:fileName] autorelease]; + + // set the image preview, string contents and hex representation + [editImage setImage:image]; + [editTextView setString:contents]; + [hexTextView setString:[self dataToHex:editData]]; + } +} + +- (IBAction)saveEditSheet:(id)sender +/* + saves a file containing the content of the editSheet + */ +{ + NSSavePanel *panel = [NSSavePanel savePanel]; + + if ( [panel runModal] == NSOKButton ) { + NSString *fileName = [panel filename]; + NSString *data; + + if ( [editData isKindOfClass:[NSData class]] ) { + data = editData; + } else { + data = [editData description]; + } + if ( [editData respondsToSelector:@selector(writeToFile:atomically:encoding:error:)] ) { + // mac os 10.4 or later + [editData writeToFile:fileName atomically:YES encoding:[CMMCPConnection encodingForMySQLEncoding:[(NSString *)[tableDocumentInstance encoding] UTF8String]] error:NULL]; + } else { + // mac os pre 10.4 + [editData writeToFile:fileName atomically:YES]; + } + } +} + +- (IBAction)dropImage:(id)sender +/* + invoked when user drag&drops image on imageView + */ +{ + // load new data/images + if (nil != editData) + { + [editData release]; + } + editData = [[[NSData alloc] initWithContentsOfFile:[sender draggedFilePath]] retain]; + NSString *contents = [NSString stringWithContentsOfFile:[sender draggedFilePath]]; + + // set the string contents and hex representation + [editTextView setString:contents]; + [hexTextView setString:[self dataToHex:editData]]; +} + +- (void)textDidChange:(NSNotification *)notification +/* + invoked when the user changes the string in the editSheet + */ +{ + // clear the image and hex (since i doubt someone can "type" a gif) + [editImage setImage:nil]; + [hexTextView setString:@""]; + + // free old data + if ( editData != nil ) { + [editData release]; + } + + // set edit data to text + editData = [[editTextView string] retain]; +} + +- (NSString *)dataToHex:(NSData *)data +/* + returns the hex representation of the given data + */ +{ + unsigned i; + unsigned totalLength = [data length]; + int bytesPerLine = 16; + NSMutableString *retVal = [NSMutableString string]; + unsigned char *nodisplay = "\t\n\r\f"; + + // get the length of the longest location + int longest = [(NSString *)[NSString stringWithFormat:@"%X", totalLength - ( totalLength % bytesPerLine )] length]; + + for ( i = 0; i < totalLength; i += bytesPerLine ) { + int j; + NSMutableString *hex = [[NSMutableString alloc] initWithCapacity:(3 * bytesPerLine - 1)]; + NSMutableString *location = [[NSMutableString alloc] initWithCapacity:(longest + 2)]; + NSMutableString *chars = [[NSMutableString alloc] init]; + unsigned char *buffer; + int buffLength = bytesPerLine; + + // add hex value of location + [location appendString:[NSString stringWithFormat:@"%X", i]]; + + // pad it + while( longest > [location length] ) { + [location insertString:@"0" atIndex:0]; + } + + // get the chars from the NSData obj + if ( i + buffLength >= totalLength ) { + buffLength = totalLength - i; + } + buffer = (unsigned char*) malloc( sizeof( unsigned char ) * buffLength ); + NSRange range = { i, buffLength }; + [data getBytes:buffer range:range]; + + // build the hex string + for ( j = 0; j < buffLength; j++ ) { + unsigned char byte = *(buffer + j); + if ( byte < 16 ) { + [hex appendString:@"0"]; + } + [hex appendString:[NSString stringWithFormat:@"%X", byte]]; + [hex appendString:@" "]; + + // if the char is undisplayable, replace it with "." + unsigned char current; + int count = 0; + while ( ( current = *(nodisplay + count++) ) > 0 ) { + if ( current == byte ) { + *(buffer + j) = '.'; + break; + } + } + } + + // add padding to missing hex values. + for ( j = 0; j < bytesPerLine - buffLength; j++ ) { + [hex appendString:@" "]; + } + + // remove extra ghost characters + [chars appendString:[NSString stringWithCString:buffer]]; + if ( [chars length] > bytesPerLine ) { + [chars deleteCharactersInRange:NSMakeRange( bytesPerLine, [chars length] - bytesPerLine )]; + } + + // build line + [retVal appendString:location]; + [retVal appendString:@" "]; + [retVal appendString:hex]; + [retVal appendString:@" "]; + [retVal appendString:chars]; + [retVal appendString:@"\n"]; + + // clean up + [hex release]; + [chars release]; + [location release]; + free( buffer ); + } + + return retVal; +} + +//getter methods +- (NSArray *)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 *tableColumns; + NSEnumerator *enumerator; + id tableColumn; + NSMutableArray *currentResult = [NSMutableArray array]; + NSMutableArray *tempRow = [NSMutableArray array]; + int i; + + //load table if not already done + if ( ![tablesListInstance contentLoaded] ) { + [self loadTable:(NSString *)[tablesListInstance table]]; + } + + 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 +- (void)setConnection:(CMMCPConnection *)theConnection +/* + sets the connection (received from TableDocument) and makes things that have to be done only once + */ +{ + mySQLConnection = theConnection; + + [tableContentView setVerticalMotionCanBeginDrag:NO]; + + prefs = [[NSUserDefaults standardUserDefaults] retain]; + if ( [prefs boolForKey:@"useMonospacedFonts"] ) { + [argumentField setFont:[NSFont fontWithName:@"Monaco" size:10]]; + [limitRowsField setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]]; + [editTextView setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]]; + } else { + [editTextView setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + [limitRowsField setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + [argumentField setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + } + [hexTextView setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]]; + [limitRowsStepper setEnabled:NO]; + if ( [prefs boolForKey:@"limitRows"] ) { + [limitRowsText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Limited to %d rows starting with row", @"text showing the number of rows the result is limited to"), + [prefs integerForKey:@"limitRowsValue"]]]; + } else { + [limitRowsText setStringValue:NSLocalizedString(@"No limit", @"text showing that the result isn't limited")]; + [limitRowsField setStringValue:@""]; + } +} + +/** + * Sets the compare types for the filter and the appropriate formatter for the textField + */ +- (IBAction)setCompareTypes:(id)sender +{ + NSArray *stringFields = [NSArray arrayWithObjects:@"varstring", @"string", @"tinyblob", @"blob", @"mediumblob", @"longblob", @"set", @"enum", nil]; + NSArray *stringTypes = [NSArray arrayWithObjects:NSLocalizedString(@"is", @"popup menuitem for field IS value"), NSLocalizedString(@"is not", @"popup menuitem for field IS NOT value"), NSLocalizedString(@"contains", @"popup menuitem for field CONTAINS value"), NSLocalizedString(@"contains not", @"popup menuitem for field CONTAINS NOT value"), @"IN", nil]; + NSArray *numberFields = [NSArray arrayWithObjects:@"tiny", @"short", @"long", @"int24", @"longlong", @"decimal", @"float", @"double", nil]; + NSArray *numberTypes = [NSArray arrayWithObjects:@"=", @"≠", @">", @"<", @"≥", @"≤", @"IN", nil]; + NSArray *dateFields = [NSArray arrayWithObjects:@"timestamp", @"date", @"time", @"datetime", @"year", nil]; + NSArray *dateTypes = [NSArray arrayWithObjects:NSLocalizedString(@"is", @"popup menuitem for field IS value"), NSLocalizedString(@"is not", @"popup menuitem for field IS NOT value"), NSLocalizedString(@"older than", @"popup menuitem for field OLDER THAN value"), NSLocalizedString(@"younger than", @"popup menuitem for field YOUNGER THAN value"), NSLocalizedString(@"older than or equal to", @"popup menuitem for field OLDER THAN OR EQUAL TO value"), NSLocalizedString(@"younger than or equal to", @"popup menuitem for field YOUNGER THAN OR EQUAL TO value"), nil]; + NSString *fieldType = [NSString stringWithString:[fieldTypes objectAtIndex:[[fieldField selectedItem] tag]]]; + + int i; + + [compareField removeAllItems]; + + // Why do we get "string" for enum fields? (error in framework?) + if ( [stringFields containsObject:fieldType] ) { + [compareField addItemsWithTitles:stringTypes]; + compareType = @"string"; + // [argumentField setFormatter:nil]; + } else if ( [numberFields containsObject:fieldType] ) { + [compareField addItemsWithTitles:numberTypes]; + compareType = @"number"; + // [argumentField setFormatter:numberFormatter]; + } else if ( [dateFields containsObject:fieldType] ) { + [compareField addItemsWithTitles:dateTypes]; + 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]]; + } + */ + } else { + NSLog(@"ERROR: unknown type for comparision: %@", fieldType); + } + + // Add IS NULL and IS NOT NULL as they should always be available + [compareField addItemWithTitle:@"IS NULL"]; + [compareField addItemWithTitle:@"IS NOT NULL"]; + + for ( i = 0 ; i < [compareField numberOfItems] ; i++ ) { + [[compareField itemAtIndex:i] setTag:i]; + } + + // Update the argumentField enabled state + [self toggleFilterField:self]; + + // set focus on argumentField + [argumentField selectText:self]; +} + +- (IBAction)stepLimitRows:(id)sender +/* + steps the start row up or down (+/- limitRowsValue) + */ +{ + if ( [limitRowsStepper intValue] > 0 ) { + [limitRowsField setIntValue:[limitRowsField intValue]+[prefs integerForKey:@"limitRowsValue"]]; + } else { + if ( ([limitRowsField intValue]-[prefs integerForKey:@"limitRowsValue"]) < 1 ) { + [limitRowsField setIntValue:1]; + } else { + [limitRowsField setIntValue:[limitRowsField intValue]-[prefs integerForKey:@"limitRowsValue"]]; + } + } + [limitRowsStepper setIntValue:0]; +} + +- (NSArray *)fetchResultAsArray:(CMMCPResult *)theResult +/* + fetches the result as an array with a dictionary for each row in it + */ +{ + NSMutableArray *tempResult = [NSMutableArray array]; + NSDictionary *tempRow; + NSMutableDictionary *modifiedRow = [NSMutableDictionary dictionary]; + NSEnumerator *enumerator; + id key; + int i,j; + + if ([theResult numOfRows]) [theResult dataSeek:0]; + for ( i = 0 ; i < [theResult numOfRows] ; i++ ) { + tempRow = [theResult fetchRowAsDictionary]; + enumerator = [tempRow keyEnumerator]; + while ( key = [enumerator nextObject] ) { + if ( [[tempRow objectForKey:key] isMemberOfClass:[NSNull class]] ) { + [modifiedRow setObject:[prefs stringForKey:@"nullValue"] forKey:key]; + /* + //NSData objects now decoded in tableView:objectValueForTableColumn:row + //object in result remains a NSData object + } else if ( [[tempRow objectForKey:key] isKindOfClass:[NSData class]] ) { + [modifiedRow setObject:[[NSString alloc] initWithData:[tempRow objectForKey:key] encoding:[mySQLConnection encoding]] + forKey:key]; + */ + } else { + [modifiedRow setObject:[tempRow objectForKey:key] forKey:key]; + } + //add values for hidden blob and text fields + if ( [prefs boolForKey:@"dontShowBlob"] ) { + for ( j = 0 ; j < [fieldTypes count] ; j++ ) { + if ( [self isBlobOrText:[fieldTypes objectAtIndex:j]] ) { + [modifiedRow setObject:NSLocalizedString(@"- blob or text -", @"value shown for hidden blob and text fields") forKey:[fieldNames objectAtIndex:j]]; + } + } + } + } + [tempResult addObject:[NSMutableDictionary dictionaryWithDictionary:modifiedRow]]; + } + return tempResult; +} + +- (BOOL)addRowToDB +/* + tries to write row to mysql-db + returns YES if row written to db, otherwies NO + returns YES if no row is beeing edited and nothing has to be written to db + */ +{ + int rowIndex = [tableContentView selectedRow]; + NSMutableArray *fieldValues = [[NSMutableArray alloc] init]; + NSMutableString *queryString; + NSString *query; + CMMCPResult *queryResult; + id rowObject; + NSMutableString *rowValue = [NSMutableString string]; + NSString *currentTime = [[NSDate date] descriptionWithCalendarFormat:@"%H:%M:%S" timeZone:nil locale:nil]; + int i; + + if ( !isEditingRow || rowIndex == -1) { + [fieldValues release]; + return YES; + } + + //get field values + for ( i=0 ; i < [fieldNames count] ; i++) { + rowObject = [[filteredResult objectAtIndex:rowIndex] objectForKey:[fieldNames objectAtIndex:i]]; + //convert the object to a string (here we can add special treatment for date-, number- and data-fields) + if ( [[rowObject description] isEqualToString:[prefs stringForKey:@"nullValue"]] || + ([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 { + if ( [rowObject isKindOfClass:[NSCalendarDate class]] ) { + // [rowValue setString:[NSString stringWithFormat:@"\"%@\"", [mySQLConnection prepareString:[rowObject description]]]]; + [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 { + // [rowValue setString:[NSString stringWithFormat:@"\"%@\"", [mySQLConnection prepareString:[rowObject description]]]]; + if ( [[rowObject description] isEqualToString:@"CURRENT_TIMESTAMP"] ) { + [rowValue setString:@"CURRENT_TIMESTAMP"]; + } else { + [rowValue setString:[NSString stringWithFormat:@"'%@'", [mySQLConnection prepareString:[rowObject description]]]]; + } + } + } + //escape special characters -> now escaped by framework + /* + for ( j = 0 ; j < [rowValue length] ; j++ ) { + if ( [rowValue characterAtIndex:j] == '\\' ) { + [rowValue insertString:@"\\" atIndex:j]; + j++; + } else if ( [rowValue characterAtIndex:j] == '"' ) { + [rowValue insertString:@"\\" atIndex:j]; + j++; + } + } + */ + [fieldValues addObject:[NSString stringWithString:rowValue]]; + } + + if ( isEditingNewRow ) { + //INSERT syntax + queryString = [NSString stringWithFormat:@"INSERT INTO `%@` (`%@`) VALUES (%@)", + selectedTable, [fieldNames componentsJoinedByString:@"`,`"], [fieldValues componentsJoinedByString:@","]]; + } else { + //UPDATE syntax + queryString = [NSMutableString stringWithFormat:@"UPDATE `%@` SET ", selectedTable]; + for ( i = 0 ; i < [fieldNames count] ; i++ ) { + if ( i > 0 ) { + [queryString appendString:@", "]; + } + [queryString appendString:[NSString stringWithFormat:@"`%@`=%@", + [fieldNames objectAtIndex:i], [fieldValues objectAtIndex:i]]]; + } + [fieldValues release]; + [queryString appendString:[NSString stringWithFormat:@" WHERE %@", [self argumentForRow:-2]]]; + } + [mySQLConnection queryString:queryString]; + + //NSLog( @"%@", queryString ); + + if ( ![mySQLConnection affectedRows] ) { + //no rows changed + if ( [prefs boolForKey:@"showError"] ) { + NSBeginAlertSheet(NSLocalizedString(@"Warning", @"warning"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, 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(); + } + [filteredResult replaceObjectAtIndex:rowIndex withObject:[NSMutableDictionary dictionaryWithDictionary:oldRow]]; + isEditingRow = NO; + isEditingNewRow = NO; + [tableDocumentInstance 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]]; + return YES; + } else if ( [[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + //added new row with success + isEditingRow = NO; + if ( isEditingNewRow ) { + if ( [prefs boolForKey:@"reloadAfterAdding"] ) { + [self reloadTableValues:self]; + // if ( sortField ) + [tableContentView deselectAll:self]; + } else { + //set insertId for fields with auto_increment + queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM `%@`", selectedTable]]; + if ([queryResult numOfRows]) [queryResult dataSeek:0]; + for ( i = 0 ; i < [queryResult numOfRows] ; i++ ) { + rowObject = [queryResult fetchRowAsDictionary]; + if ( [[rowObject objectForKey:@"Extra"] isEqualToString:@"auto_increment"] ) { + [[filteredResult objectAtIndex:rowIndex] setObject:[NSNumber numberWithLong:[mySQLConnection insertId]] + forKey:[rowObject objectForKey:@"Field"]]; + } + } + [fullResult addObject:[filteredResult objectAtIndex:rowIndex]]; + } + isEditingNewRow = NO; + } else { + //updated row with success + if ( [prefs boolForKey:@"reloadAfterEditing"] ) { + [self reloadTableValues:self]; + // if ( sortField ) + [tableContentView deselectAll:self]; + } else { + // query = [@"SELECT * FROM " stringByAppendingString:selectedTable]; + query = [NSString stringWithFormat:@"SELECT %@ FROM `%@`", [self fieldListForQuery], selectedTable]; + if ( sortField ) { + // query = [query stringByAppendingString:[NSString stringWithFormat:@" ORDER BY `%@`", sortField]]; + query = [NSString stringWithFormat:@"%@ ORDER BY `%@`", query, sortField]; + if ( isDesc ) + query = [query stringByAppendingString:@" DESC"]; + } + if ( [prefs boolForKey:@"limitRows"] ) { + if ( [limitRowsField intValue] <= 0 ) { + [limitRowsField setStringValue:@"1"]; + } + query = [query stringByAppendingString: + [NSString stringWithFormat:@" LIMIT %d,%d", + [limitRowsField intValue]-1, [prefs integerForKey:@"limitRowsValue"]]]; + } + queryResult = [mySQLConnection queryString:query]; + // [fullResult setArray:[[self fetchResultAsArray:queryResult] retain]]; + [fullResult setArray:[self fetchResultAsArray:queryResult]]; + } + } + return YES; + } else { + //error in mysql-query + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), NSLocalizedString(@"Cancel", @"cancel button"), nil, tableWindow, self, @selector(sheetDidEnd:returnCode:contextInfo:), nil, @"addrow", + [NSString stringWithFormat:NSLocalizedString(@"Couldn't write row.\nMySQL said: %@", @"message of panel when error while adding row to db"), [mySQLConnection getLastErrorMessage]]); + return NO; + } +} + +- (NSString *)argumentForRow:(int)row +/* + returns the WHERE argument to identify a row + if row is -2, it uses the oldRow + if there is one, it uses the primary key, otherwise uses all fields as argument and sets LIMIT to 1 + */ +{ + CMMCPResult *theResult; + NSDictionary *theRow; + id tempValue; + NSMutableString *value = [NSMutableString string]; + NSMutableString *argument = [NSMutableString string]; + int i,j; + NSEnumerator *enumerator; + id type; + BOOL blob = NO; + NSArray *numberFields = [NSArray arrayWithObjects:@"tiny", @"short", @"long", @"int24", @"longlong", @"decimal", @"float", @"double", nil]; + + if ( row == -1 ) + return @""; + + //get primary key if there is one + /* + theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW INDEX FROM `%@`", selectedTable]]; + if ([theResult numOfRows]) [theResult dataSeek:0]; + for ( i = 0 ; i < [theResult numOfRows] ; i++ ) { + theRow = [theResult fetchRowAsDictionary]; + if ( [[theRow objectForKey:@"Key_name"] isEqualToString:@"PRIMARY"] ) { + [keys addObject:[theRow objectForKey:@"Column_name"]]; + } + } + */ + if ( !keys ) { + setLimit = NO; + keys = [[NSMutableArray alloc] init]; + theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM `%@`", selectedTable]]; + 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 ( ![keys count] ) { + //if there is no primary key, take all fields as argument + //here we have a problem when dontShowBlob == YES (we don't have the right values to use in the WHERE statement) + [keys setArray:fieldNames]; + setLimit = YES; + enumerator = [fieldTypes objectEnumerator]; + while ( (type = [enumerator nextObject]) ) { + if ( [self isBlobOrText:type] ) { + blob = YES; + } + } + if ( [prefs boolForKey:@"dontShowBlob"] && blob ) { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, 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 @""; + } + } + for ( i = 0 ; i < [keys count] ; i++ ) { + if ( i ) + [argument appendString:@" AND "]; + if ( row >= 0 ) { + //use selected row + tempValue = [[filteredResult objectAtIndex:row] objectForKey:[keys objectAtIndex:i]]; + } else { + //use oldRow + tempValue = [oldRow objectForKey:[keys objectAtIndex:i]]; + } + if ( [tempValue isKindOfClass:[NSData class]] ) { + [value setString:[[NSString alloc] initWithData:tempValue encoding:[mySQLConnection encoding]]]; + } else { + [value setString:[tempValue description]]; + } + + if ( [value isEqualToString:[prefs stringForKey:@"nullValue"]] ) { + [value setString:@"NULL"]; + } else { + //escape special characters (in WHERE statement!) + for ( j = 0 ; j < [value length] ; j++ ) { + if ( [value characterAtIndex:j] == '\\' ) { + [value insertString:@"\\" atIndex:j]; + j++; + } + } + [value setString:[mySQLConnection prepareString:value]]; + for ( j = 0 ; j < [value length] ; j++ ) { + if ( [value characterAtIndex:j] == '%' || + [value characterAtIndex:j] == '_' ) { + [value insertString:@"\\" atIndex:j]; + j++; + } + } + // [value setString:[NSString stringWithFormat:@"\"%@\"", value]]; + [value setString:[NSString stringWithFormat:@"'%@'", value]]; + } + if ( [value isEqualToString:@"NULL"] ) { + [argument appendString:[NSString stringWithFormat:@"`%@` IS NULL", [keys objectAtIndex:i]]]; + } else { + if ( [numberFields containsObject:[fieldTypes objectAtIndex:[fieldNames indexOfObject:[keys objectAtIndex:i]]]] ) { + [argument appendString:[NSString stringWithFormat:@"`%@` = %@", [keys objectAtIndex:i], value]]; + } else { + [argument appendString:[NSString stringWithFormat:@"`%@` LIKE %@", [keys objectAtIndex:i], value]]; + } + } + } + if ( setLimit ) + [argument appendString:@" LIMIT 1"]; + return argument; +} + +- (BOOL)isBlobOrText:(NSString *)fieldType +/* + returns YES if fieldType is some kind of blob or text. afaik the type of this fields is always blob, but better we test it... + it would be nice to know if it is blob or text, but mysql doesn't want to tell it... + */ +{ + if ( [fieldType isEqualToString:@"tinyblob"] || [fieldType isEqualToString:@"blob"] || + [fieldType isEqualToString:@"mediumblob"] || [fieldType isEqualToString:@"longblob"] ) { + return YES; + } else { + return NO; + } +} + +- (NSString *)fieldListForQuery +/* + returns * if dontShowBlob == NO + returns a comma-separated list of all fields which aren't of type blob or text if dontShowBlob == YES + */ +{ + int i; + NSMutableArray *fields = [NSMutableArray array]; + + if ( [prefs boolForKey:@"dontShowBlob"] ) { + for ( i = 0 ; i < [fieldTypes count] ; i++ ) { + if ( ![self isBlobOrText:[fieldTypes objectAtIndex:i]] ) { + [fields addObject:[fieldNames objectAtIndex:i]]; + } + } + if ( [fields count] == 0 ) { + return [NSString stringWithFormat:@"`%@`", [fieldNames objectAtIndex:0]]; + } else { + return [NSString stringWithFormat:@"`%@`", [fields componentsJoinedByString:@"`,`"]]; + } + } else { + return @"*"; + } +} + +- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo +/* + if contextInfo == addrow: remain in edit-mode if user hits OK, otherwise cancel editing + if contextInfo == removerow: removes row if user hits OK + */ +{ + NSEnumerator *enumerator = [tableContentView selectedRowEnumerator]; + NSNumber *index; + NSMutableArray *tempArray = [NSMutableArray array]; + NSMutableArray *tempResult = [NSMutableArray array]; + NSString *queryString; + CMMCPResult *queryResult; + int i, errors; + + [sheet orderOut:self]; + + if ( [contextInfo isEqualToString:@"addrow"] ) { + if ( returnCode == NSAlertDefaultReturn ) { + //problem: reenter edit mode doesn't function + [tableContentView editColumn:0 row:[tableContentView selectedRow] withEvent:nil select:YES]; + } else { + if ( !isEditingNewRow ) { + [filteredResult replaceObjectAtIndex:[tableContentView selectedRow] + withObject:[NSMutableDictionary dictionaryWithDictionary:oldRow]]; + isEditingRow = NO; + } else { + [filteredResult removeObjectAtIndex:[tableContentView selectedRow]]; + isEditingRow = NO; + isEditingNewRow = NO; + } + } + [tableContentView reloadData]; + } else if ( [contextInfo isEqualToString:@"removeallrows"] ) { + if ( returnCode == NSAlertDefaultReturn ) { + /* + if ( ([tableContentView numberOfSelectedRows] == [self numberOfRowsInTableView:tableContentView]) && + areShowingAllRows && + ([tableContentView numberOfSelectedRows] < [prefs integerForKey:@"limitRowsValue"]) ) { + */ + [mySQLConnection queryString:[NSString stringWithFormat:@"DELETE FROM `%@`", selectedTable]]; + if ( [[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + [self reloadTable:self]; + } else { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + [NSString stringWithFormat:NSLocalizedString(@"Couldn't remove rows.\nMySQL said: %@", @"message of panel when field cannot be removed"), + [mySQLConnection getLastErrorMessage]]); + } + } + } else if ( [contextInfo isEqualToString:@"removerow"] ) { + if ( returnCode == NSAlertDefaultReturn ) { + errors = 0; + + while ( (index = [enumerator nextObject]) ) { + [mySQLConnection queryString:[NSString stringWithFormat:@"DELETE FROM `%@` WHERE %@", + selectedTable, [self argumentForRow:[index intValue]]]]; + if ( ![mySQLConnection affectedRows] ) { + //no rows deleted + errors++; + } else if ( [[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + //rows deleted with success + [tempArray addObject:index]; + } else { + //error in mysql-query + errors++; + } + } + + if ( errors ) { + NSBeginAlertSheet(NSLocalizedString(@"Warning", @"warning"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, [NSString stringWithFormat:NSLocalizedString(@"%d rows have not been removed. 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"), errors]); + } + + //do deleting (after enumerating) + if ( [prefs boolForKey:@"reloadAfterRemoving"] ) { + [self reloadTableValues:self]; + } else { + for ( i = 0 ; i < [filteredResult count] ; i++ ) { + if ( ![tempArray containsObject:[NSNumber numberWithInt:i]] ) + [tempResult addObject:[filteredResult objectAtIndex:i]]; + } + [filteredResult setArray:tempResult]; + numRows = [self getNumberOfRows]; + if ( !areShowingAllRows ) { + // queryString = [@"SELECT * FROM " stringByAppendingString:selectedTable]; + queryString = [NSString stringWithFormat:@"SELECT %@ FROM `%@`", [self fieldListForQuery], selectedTable]; + if ( sortField ) { + // queryString = [queryString stringByAppendingString:[NSString stringWithFormat:@" ORDER BY `%@`", sortField]]; + queryString = [NSString stringWithFormat:@"%@ ORDER BY `%@`", queryString, sortField]; + if ( isDesc ) + queryString = [queryString stringByAppendingString:@" DESC"]; + } + if ( [prefs boolForKey:@"limitRows"] ) { + if ( [limitRowsField intValue] <= 0 ) { + [limitRowsField setStringValue:@"1"]; + } + queryString = [queryString stringByAppendingString: + [NSString stringWithFormat:@" LIMIT %d,%d", + [limitRowsField intValue]-1, [prefs integerForKey:@"limitRowsValue"]]]; + } + queryResult = [mySQLConnection queryString:queryString]; + // [fullResult setArray:[[self fetchResultAsArray:queryResult] retain]]; + [fullResult setArray:[self fetchResultAsArray:queryResult]]; + [tableContentView reloadData]; + [countText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%d rows of %d selected", @"text showing how many rows are in the filtered result"), + [filteredResult count], numRows]]; + } else { + [fullResult setArray:filteredResult]; + [tableContentView reloadData]; + [countText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%d rows in table", @"text showing how many rows are in the result"), numRows]]; + } + } + [tableContentView deselectAll:self]; + } + } +} + +- (int)getNumberOfRows +/* + returns the number of rows in the selected table + queries the number from mysql if enabled in prefs and result is limited, otherwise just return the fullResult count + */ +{ + if ( [prefs boolForKey:@"limitRows"] && [prefs boolForKey:@"fetchRowCount"] ) { + numRows = [self fetchNumberOfRows]; + } else { + numRows = [fullResult count]; + } + return numRows; +} + +- (int)fetchNumberOfRows +/* + fetches the number of rows in the selected table using a "SELECT COUNT(*)" query and return it + */ +{ + return [[[[mySQLConnection queryString:[NSString stringWithFormat:@"SELECT COUNT(*) FROM `%@`", selectedTable]] fetchRowAsArray] objectAtIndex:0] intValue]; +} + + +//tableView datasource methods +- (int)numberOfRowsInTableView:(NSTableView *)aTableView +{ + return [filteredResult count]; +} + +- (id)tableView:(NSTableView *)aTableView +objectValueForTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex +{ + id theRow, theValue; + + theRow = [filteredResult objectAtIndex:rowIndex]; + theValue = [theRow objectForKey:[aTableColumn identifier]]; + + if ( [theValue isKindOfClass:[NSData class]] ) { + theValue = [[NSString alloc] initWithData:theValue encoding:[mySQLConnection encoding]]; + //show only first 50 characters to speed up interface (but return everything when this method is used to return the current result) + // if ( ([theValue length] > 100) && aTableView ) { + } + +// if ( ([(NSString *)theValue length] > 100) && aTableView ) { +// theValue = [NSString stringWithFormat:@"%@(...)", [theValue substringToIndex:100]]; +// } + + return theValue; +} + +- (void)tableView:(NSTableView *)aTableView + setObjectValue:(id)anObject + forTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex +{ + if ( !isEditingRow ) { + [oldRow setDictionary:[filteredResult objectAtIndex:rowIndex]]; + isEditingRow = YES; + } + if ( anObject ) { + [[filteredResult objectAtIndex:rowIndex] setObject:anObject forKey:[aTableColumn identifier]]; + } else { + [[filteredResult objectAtIndex:rowIndex] setObject:@"" forKey:[aTableColumn identifier]]; + } +} + +#pragma mark - +#pragma mark tableView delegate methods + +- (void)tableView:(NSTableView*)tableView didClickTableColumn:(NSTableColumn *)tableColumn +/* + sorts the tableView by the clicked column + if clicked twice, order is descending + */ +{ + NSString *queryString; + CMMCPResult *queryResult; + + if ( [selectedTable isEqualToString:@""] || !selectedTable ) + return; + if ( ![self selectionShouldChangeInTableView:nil] ) + return; + + //query started + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; + + //sets order descending if a header is clicked twice + if ( [[tableColumn identifier] isEqualTo:sortField] ) { + if ( isDesc ) { + isDesc = NO; + } else { + isDesc = YES; + } + } else { + isDesc = NO; + [tableContentView setIndicatorImage:nil inTableColumn:[tableContentView tableColumnWithIdentifier:sortField]]; + } + sortField = [tableColumn identifier]; + + //make queryString and perform query + queryString = [NSString stringWithFormat:@"SELECT %@ FROM `%@` ORDER BY `%@`", [self fieldListForQuery], + selectedTable, sortField]; + if ( isDesc ) + queryString = [queryString stringByAppendingString:@" DESC"]; + if ( [prefs boolForKey:@"limitRows"] ) { + if ( [limitRowsField intValue] <= 0 ) { + [limitRowsField setStringValue:@"1"]; + } + queryString = [queryString stringByAppendingString: + [NSString stringWithFormat:@" LIMIT %d,%d", + [limitRowsField intValue]-1, [prefs integerForKey:@"limitRowsValue"]]]; + } + queryResult = [mySQLConnection queryString:queryString]; + + // [fullResult setArray:[[self fetchResultAsArray:queryResult] retain]]; + [fullResult setArray:[self fetchResultAsArray:queryResult]]; + + if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + [NSString stringWithFormat:NSLocalizedString(@"Couldn't sort table. MySQL said: %@", @"message of panel when sorting of table failed"), [mySQLConnection getLastErrorMessage]]); + return; + } + + //sets highlight and indicatorImage + [tableContentView setHighlightedTableColumn:tableColumn]; + if ( isDesc ) { + [tableContentView setIndicatorImage:[NSImage imageNamed:@"NSDescendingSortIndicator"] inTableColumn:tableColumn]; + } else { + [tableContentView setIndicatorImage:[NSImage imageNamed:@"NSAscendingSortIndicator"] inTableColumn:tableColumn]; + } + + //query finished + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; + + //if filter is activated filters the result, otherwise shows fullResult + if ( !areShowingAllRows ) { + [self filterTable:self]; + } else { + [filteredResult setArray:fullResult]; + [tableContentView reloadData]; + } +} + +- (BOOL)selectionShouldChangeInTableView:(NSTableView *)aTableView +{ + /* + int row = [tableContentView editedRow]; + int column = [tableContentView editedColumn]; + NSTableColumn *tableColumn; + NSCell *cell; + + if ( row != -1 ) { + tableColumn = [[tableContentView tableColumns] objectAtIndex:column]; + cell = [tableColumn dataCellForRow:row]; + [cell endEditing:[tableContentView currentEditor]]; + } + */ + //end editing (otherwise problems when user hits reload button) + [tableWindow endEditingFor:nil]; + + return [self addRowToDB]; +} + +- (void)tableViewSelectionDidChange:(NSNotification *)aNotification +{ + // Check our notification object is our table content view + if ([aNotification object] != tableContentView) + return; + + if ( [tableContentView numberOfSelectedRows] > 0 ) { + [countText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%d of %d rows selected", @"Text showing how many rows are selected"), [tableContentView numberOfSelectedRows], [tableContentView numberOfRows]]]; + } else { + [countText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%d rows", @"Text showing how many rows are in the result"), [tableContentView numberOfRows]]]; + } +} + +- (void)tableViewSelectionIsChanging:(NSNotification *)aNotification +{ + // Check our notification object is our table content view + if ([aNotification object] != tableContentView) + return; + + if ( [tableContentView numberOfSelectedRows] > 0 ) { + [countText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%d of %d rows selected", @"Text showing how many rows are selected"), [tableContentView numberOfSelectedRows], [tableContentView numberOfRows]]]; + } else { + [countText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%d rows", @"Text showing how many rows are in the result"), [tableContentView numberOfRows]]]; + } +} + + +- (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 = (NSString *)[tablesListInstance table]; + + // get tableColumnWidths object + if ( [prefs objectForKey:@"tableColumnWidths"] != nil ) { + tableColumnWidths = [NSMutableDictionary dictionaryWithDictionary:[prefs objectForKey:@"tableColumnWidths"]]; + } else { + tableColumnWidths = [NSMutableDictionary dictionary]; + } + // get database object + if ( [tableColumnWidths objectForKey:database] == nil ) { + [tableColumnWidths setObject:[NSMutableDictionary dictionary] forKey:database]; + } else { + [tableColumnWidths setObject:[[tableColumnWidths objectForKey:database] mutableCopy] 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:[[[tableColumnWidths objectForKey:database] objectForKey:table] mutableCopy] forKey:table]; + } + // save column size + [[[tableColumnWidths objectForKey:database] objectForKey:table] setObject:[NSNumber numberWithFloat:[[[aNotification userInfo] objectForKey:@"NSTableColumn"] width]] forKey:[[[aNotification userInfo] objectForKey:@"NSTableColumn"] identifier]]; + [prefs setObject:tableColumnWidths forKey:@"tableColumnWidths"]; +} + +- (BOOL)tableView:(NSTableView *)aTableView shouldEditTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex +/* + opens sheet if multipleLineEditingButton is clicked or field is a hidden blob or text field + */ +{ + NSEnumerator *enumerator; + id type; + BOOL blob = NO; + NSDictionary *tempRow; + NSMutableDictionary *modifiedRow = [NSMutableDictionary dictionary]; + id key; + int code; + NSString *query; + CMMCPResult *tempResult; + id theValue; + BOOL columnIsBlob = NO; + // int i; + // NSArray *columns = [aTableView tableColumns]; + + if ( [prefs boolForKey:@"dontShowBlob"] && !isEditingRow ) { + //get all row values if dontShowBlob == YES and table contains blob or text field and isEditingRow = NO + enumerator = [fieldTypes objectEnumerator]; + while ( (type = [enumerator nextObject]) ) { + if ( [self isBlobOrText:type] ) { + blob = YES; + } + } + + if ( blob ) { + query = [NSString stringWithFormat:@"SELECT * FROM `%@` WHERE %@", + selectedTable, [self argumentForRow:[tableContentView selectedRow]]]; + tempResult = [mySQLConnection queryString:query]; + if ( ![tempResult numOfRows] ) { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, 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; + } + tempRow = [tempResult fetchRowAsDictionary]; + enumerator = [tempRow keyEnumerator]; + while ( key = [enumerator nextObject] ) { + if ( [[tempRow objectForKey:key] isMemberOfClass:[NSNull class]] ) { + [modifiedRow setObject:[prefs stringForKey:@"nullValue"] forKey:key]; + } else { + [modifiedRow setObject:[tempRow objectForKey:key] forKey:key]; + } + } + [filteredResult replaceObjectAtIndex:rowIndex + withObject:[NSMutableDictionary dictionaryWithDictionary:modifiedRow]]; + [tableContentView reloadData]; + } + } + + /* + // find the column we're trying to edit + for ( i = 0; i < [columns count]; i++ ) { + if ( [columns objectAtIndex:i] == aTableColumn ) { + // this flag will let us determine if we should "force" multi-line edit. + columnIsBlob = [self isBlobOrText:[fieldTypes objectAtIndex:i]]; + break; + } + } + */ + //is the column a blob field -> if YES force sheet editing + if ( [self isBlobOrText:[fieldTypes objectAtIndex:[fieldNames indexOfObject:[aTableColumn identifier]]]] ) { + columnIsBlob = YES; + } + + if ( [multipleLineEditingButton state] == NSOnState || columnIsBlob ) { + theValue = [[filteredResult objectAtIndex:rowIndex] objectForKey:[aTableColumn identifier]]; + NSImage *image = nil; + editData = [theValue retain]; + + if ( [theValue isKindOfClass:[NSData class]] ) { + image = [[NSImage alloc] initWithData:theValue]; + [hexTextView setString:[self dataToHex:theValue]]; + /* + // update displayed font to monospace + NSFont *font = [NSFont fontWithName:@"Courier" size:12.0f]; + NSRange hexRange = { 0, [[hexTextView string] length] - 1 }; + [hexTextView setFont:font range:hexRange]; + */ + theValue = [[NSString alloc] initWithData:theValue encoding:[mySQLConnection encoding]]; + } else { + [hexTextView setString:@""]; + theValue = [theValue description]; + } + + [editImage setImage:image]; + [editTextView setString:theValue]; + [editTextView setSelectedRange:NSMakeRange(0,[[editTextView string] length])]; + //different sheets for date (with up/down arrows), number and text + [NSApp beginSheet:editSheet + modalForWindow:tableWindow modalDelegate:self + didEndSelector:nil contextInfo:nil]; + code = [NSApp runModalForWindow:editSheet]; + + [NSApp endSheet:editSheet]; + [editSheet orderOut:nil]; + + if ( code ) { + if ( !isEditingRow ) { + [oldRow setDictionary:[filteredResult objectAtIndex:rowIndex]]; + isEditingRow = YES; + } + + [[filteredResult objectAtIndex:rowIndex] setObject:[editData copy] + forKey:[aTableColumn identifier]]; + + // clean up + [editImage setImage:nil]; + [editTextView setString:@""]; + [hexTextView setString:@""]; + if ( editData ) { + [editData release]; + } + } + return NO; + } else { + return YES; + } +} + +- (BOOL)tableView:(NSTableView *)tableView writeRows:(NSArray*)rows + toPasteboard:(NSPasteboard*)pboard +/* + enable drag from tableview + */ +{ + if ( tableView == tableContentView ) + { + NSString *tmp = [tableContentView draggedRowsAsTabString:rows]; + + if ( nil != tmp ) + { + [pboard declareTypes:[NSArray arrayWithObjects: NSTabularTextPboardType, + NSStringPboardType, nil] + owner:nil]; + + [pboard setString:tmp forType:NSStringPboardType]; + [pboard setString:tmp forType:NSTabularTextPboardType]; + return YES; + } + } + return NO; +} + +#pragma mark - + +- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command +/* + traps enter and esc an make/cancel editing without entering next row + */ +{ + int row, column, i; + + row = [tableContentView editedRow]; + column = [tableContentView editedColumn]; + + if ( [textView methodForSelector:command] == [textView methodForSelector:@selector(insertNewline:)] || + [textView methodForSelector:command] == [textView methodForSelector:@selector(insertTab:)] ) //trap enter and tab + { + //save current line + [[control window] makeFirstResponder:control]; + if ( column == ( [tableContentView numberOfColumns] - 1 ) ) { + [self addRowToDB]; + /* + if ( [self addRowToDB] && + ( [textView methodForSelector:command] == [textView methodForSelector:@selector(insertTab:)] ) && + !(sortField && ([prefs boolForKey:@"reloadAfterAdding"] || [prefs boolForKey:@"reloadAfterEditing"])) ) { + //get in edit-mode of next row if user hit tab (and result isn't sorted and reloaded) + if ( row < ([tableContentView numberOfRows] - 1) ) { + [tableContentView selectRow:row+1 byExtendingSelection:NO]; + [tableContentView editColumn:0 row:row+1 withEvent:nil select:YES]; + } else { + [tableContentView selectRow:0 byExtendingSelection:NO]; + [tableContentView editColumn:0 row:0 withEvent:nil select:YES]; + } + } + */ + } else { + //check if next column is a blob column + i = 1; + while ( [self isBlobOrText:[fieldTypes objectAtIndex:[fieldNames indexOfObject:[[[tableContentView tableColumns] objectAtIndex:column+i] identifier]]]] ) { + i++; + if ( (column+i) >= [tableContentView numberOfColumns] ) { + //there is no other column after the blob column + [self addRowToDB]; + return TRUE; + } + } + //edit the column after the blob column + [tableContentView editColumn:column+i row:row withEvent:nil select:YES]; + } + return TRUE; + } + else if ( [[control window] methodForSelector:command] == [[control window] methodForSelector:@selector(_cancelKey:)] || + [textView methodForSelector:command] == [textView methodForSelector:@selector(complete:)] ) //trap esc + { + //abort editing + [control abortEditing]; + if ( isEditingRow && !isEditingNewRow ) { + isEditingRow = NO; + [filteredResult replaceObjectAtIndex:row withObject:[NSMutableDictionary dictionaryWithDictionary:oldRow]]; + } else if ( isEditingNewRow ) { + isEditingRow = NO; + isEditingNewRow = NO; + [filteredResult removeObjectAtIndex:row]; + [tableContentView reloadData]; + } + return TRUE; + } + else + { + return FALSE; + } +} + + +//textView delegate methods +- (BOOL)textView:(NSTextView *)aTextView doCommandBySelector:(SEL)aSelector +/* + traps enter and return key and closes editSheet instead of inserting a linebreak when user hits return + */ +{ + if ( aTextView == editTextView ) { + if ( [aTextView methodForSelector:aSelector] == [aTextView methodForSelector:@selector(insertNewline:)] && + [[[NSApp currentEvent] characters] isEqualToString:@"\003"] ) + { + [NSApp stopModalWithCode:1]; + return YES; + } else { + return NO; + } + } + return NO; +} + + +//last but not least + +- (void)dealloc +{ + // NSLog(@"TableContent dealloc"); + + [editData release]; + [fullResult release]; + [filteredResult release]; + [keys release]; + [oldRow release]; + [fieldNames release]; + [fieldTypes release]; + [compareType release]; + [sortField release]; + [prefs release]; + + [super dealloc]; +} + +@end diff --git a/Source/TableDocument.h b/Source/TableDocument.h new file mode 100644 index 00000000..62798b2d --- /dev/null +++ b/Source/TableDocument.h @@ -0,0 +1,221 @@ +// +// 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 <Cocoa/Cocoa.h> +#import <MCPKit_bundled/MCPKit_bundled.h> +#import "CMMCPConnection.h" +#import "CMMCPResult.h" + +/** + * The TableDocument class controls the primary database view window. + */ + +@interface TableDocument : NSDocument +{ + //IBOutlets + IBOutlet id keyChainInstance; + IBOutlet id tablesListInstance; + IBOutlet id tableSourceInstance; + IBOutlet id tableContentInstance; + IBOutlet id customQueryInstance; + IBOutlet id tableDumpInstance; + IBOutlet id tableStatusInstance; + + IBOutlet id tableWindow; + IBOutlet id connectSheet; + IBOutlet id databaseSheet; + IBOutlet id variablesSheet; + IBOutlet id consoleDrawer; + + IBOutlet id queryProgressBar; + IBOutlet id favoritesButton; + IBOutlet NSTableView *connectFavoritesTableView; + IBOutlet NSArrayController *favoritesController; + IBOutlet id hostField; + IBOutlet id socketField; + IBOutlet id userField; + IBOutlet id passwordField; + IBOutlet id portField; + IBOutlet id databaseField; + + IBOutlet id connectProgressBar; + IBOutlet id connectProgressStatusText; + IBOutlet id databaseNameField; + IBOutlet id chooseDatabaseButton; + IBOutlet id consoleTextView; + IBOutlet id variablesTableView; + IBOutlet NSTabView *tableTabView; + + IBOutlet id sidebarGrabber; + + IBOutlet NSTableView *dbTablesTableView; + + IBOutlet id syntaxView; + IBOutlet id syntaxViewContent; + IBOutlet NSWindow *createTableSyntaxWindow; + + CMMCPConnection *mySQLConnection; + + NSMutableArray *favorites; + NSArray *variables; + NSString *selectedDatabase; + NSString *mySQLVersion; + NSUserDefaults *prefs; + + NSMenu *selectEncodingMenu; + BOOL _supportsEncoding; + NSString *_encoding; + + NSToolbar *mainToolbar; + NSToolbarItem *chooseDatabaseToolbarItem; +} + +//start sheet +- (IBAction)connectToDB:(id)sender; +- (IBAction)connect:(id)sender; +- (IBAction)cancelConnectSheet:(id)sender; +- (IBAction)closeSheet:(id)sender; +- (IBAction)chooseFavorite:(id)sender; +- (IBAction)removeFavorite:(id)sender; +- (id)selectedFavorite; +- (NSString *)selectedFavoritePassword; +- (void)connectSheetAddToFavorites:(id)sender; +- (void)addToFavoritesHost:(NSString *)host socket:(NSString *)socket + user:(NSString *)user password:(NSString *)password + port:(NSString *)port database:(NSString *)database + useSSH:(BOOL)useSSH // no-longer in use + sshHost:(NSString *)sshHost // no-longer in use + sshUser:(NSString *)sshUser // no-longer in use + sshPassword:(NSString *)sshPassword // no-longer in use + sshPort:(NSString *)sshPort; // no-longer in use +- (NSMutableArray *)favorites; + +//alert sheets method +- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo; + +//connection getter +- (CMMCPConnection *)sharedConnection; + +//database methods +- (IBAction)setDatabases:(id)sender; +- (IBAction)chooseDatabase:(id)sender; +- (IBAction)addDatabase:(id)sender; +- (IBAction)closeDatabaseSheet:(id)sender; +- (IBAction)removeDatabase:(id)sender; + +//console methods +- (void)toggleConsole:(id)sender; +- (void)clearConsole:(id)sender; +- (BOOL)consoleIsOpened; +- (void)showMessageInConsole:(NSString *)message; +- (void)showErrorInConsole:(NSString *)error; + +//encoding methods +- (void)setEncoding:(NSString *)encoding; +- (void)detectDatabaseEncoding; +- (void)detectTableEncodingForTable:(NSString *)table; +- (IBAction)chooseEncoding:(id)sender; +- (BOOL)supportsEncoding; +- (void)updateEncodingMenuWithSelectedEncoding:(NSString *)encoding; +- (NSString *)encodingNameFromMySQLEncoding:(NSString *)mysqlEncoding; +- (NSString *)mysqlEncodingFromDisplayEncoding:(NSString *)encodingName; + +//table methods +- (IBAction)showCreateTableSyntax:(id)sender; +- (IBAction)copyCreateTableSyntax:(id)sender; +- (IBAction)checkTable:(id)sender; +- (IBAction)analyzeTable:(id)sender; +- (IBAction)optimizeTable:(id)sender; +- (IBAction)repairTable:(id)sender; +- (IBAction)flushTable:(id)sender; +- (IBAction)checksumTable:(id)sender; + +//other methods +- (NSString *)host; +- (void)doPerformQueryService:(NSString *)query; +- (void)flushPrivileges:(id)sender; +- (void)showVariables:(id)sender; +- (void)closeConnection; + +//getter methods +- (NSString *)database; +- (NSString *)table; +- (NSString *)mySQLVersion; +- (NSString *)user; + +//notification center methods +- (void)willPerformQuery:(NSNotification *)notification; +- (void)hasPerformedQuery:(NSNotification *)notification; +- (void)applicationWillTerminate:(NSNotification *)notification; +- (void)tunnelStatusChanged:(NSNotification *)notification; + +//menu methods +- (BOOL)validateMenuItem:(NSMenuItem *)anItem; +- (IBAction)import:(id)sender; +- (IBAction)export:(id)sender; +- (IBAction)exportTable:(id)sender; +- (IBAction)exportMultipleTables:(id)sender; +- (IBAction)viewStructure:(id)sender; +- (IBAction)viewContent:(id)sender; +- (IBAction)viewQuery:(id)sender; +- (IBAction)viewStatus:(id)sender; + +//toolbar methods +- (void)setupToolbar; +- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag; +- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar; +- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar; +- (BOOL)validateToolbarItem:(NSToolbarItem *)toolbarItem; +- (void)updateChooseDatabaseToolbarItemWidth; + +//NSDocument methods +- (NSString *)windowNibName; +- (void)windowControllerDidLoadNib:(NSWindowController *)aController; +- (void)windowWillClose:(NSNotification *)aNotification; + +//NSWindow delegate methods +- (BOOL)windowShouldClose:(id)sender; + +//SMySQL delegate methods +- (void)willQueryString:(NSString *)query; +- (void)queryGaveError:(NSString *)error; + +//splitView delegate methods +- (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview; +- (float)splitView:(NSSplitView *)sender constrainMaxCoordinate:(float)proposedMax ofSubviewAt:(int)offset; +- (float)splitView:(NSSplitView *)sender constrainMinCoordinate:(float)proposedMin ofSubviewAt:(int)offset; +- (NSRect)splitView:(NSSplitView *)splitView additionalEffectiveRectOfDividerAtIndex:(int)dividerIndex; + + +//tableView datasource methods +- (int)numberOfRowsInTableView:(NSTableView *)aTableView; +- (id)tableView:(NSTableView *)aTableView + objectValueForTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex; + +@end + +extern NSString *TableDocumentFavoritesControllerSelectionIndexDidChange; +extern NSString *TableDocumentFavoritesControllerFavoritesDidChange; diff --git a/Source/TableDocument.m b/Source/TableDocument.m new file mode 100644 index 00000000..2dcb5f1a --- /dev/null +++ b/Source/TableDocument.m @@ -0,0 +1,1633 @@ +// +// TableDocument.m +// 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 "TableDocument.h" +#import "KeyChain.h" +#import "TablesList.h" +#import "TableSource.h" +#import "TableContent.h" +#import "CustomQuery.h" +#import "TableDump.h" +#import "TableStatus.h" +#import "ImageAndTextCell.h" +#import "SPGrowlController.h" + +NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocumentFavoritesControllerSelectionIndexDidChange"; +NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFavoritesControllerFavoritesDidChange"; + +@implementation TableDocument + +- (id)init +{ + if (![super init]) + return nil; + + _encoding = [@"utf8" retain]; + chooseDatabaseButton = nil; + chooseDatabaseToolbarItem = nil; + + return self; +} + +- (void)awakeFromNib +{ + // register selection did change handler for favorites controller (used in connect sheet) + [favoritesController addObserver:self forKeyPath:@"selectionIndex" options:NSKeyValueChangeInsertion context:TableDocumentFavoritesControllerSelectionIndexDidChange]; + + // register value change handler for favourites, so we can save them to preferences + [self addObserver:self forKeyPath:@"favorites" options:0 context:TableDocumentFavoritesControllerFavoritesDidChange]; + + // register double click for the favorites view (double click favorite to connect) + [connectFavoritesTableView setTarget:self]; + + // find the Database -> Database Encoding menu (it's not in our nib, so we can't use interface builder) + selectEncodingMenu = [[[[[NSApp mainMenu] itemWithTag:1] submenu] itemWithTag:1] submenu]; + + // hide the tabs in the tab view (we only show them to allow switching tabs in interface builder) + [tableTabView setTabViewType:NSNoTabsNoBorder]; +} + +- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if (context == TableDocumentFavoritesControllerSelectionIndexDidChange) { + [self chooseFavorite:self]; + return; + } + + if (context == TableDocumentFavoritesControllerFavoritesDidChange) { + [prefs setObject:[self favorites] forKey:@"favorites"]; + return; + } + + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; +} + + +- (CMMCPConnection *)sharedConnection +{ + return mySQLConnection; +} + + +//start sheet + +/** + * tries to connect to a database server, shows connect sheet prompting user to + * enter details/select favorite and shoows alert sheets on failure. + */ +- (IBAction)connectToDB:(id)sender +{ + + // load the details of the curretnly selected favorite into the text boxes in connect sheet + [self chooseFavorite:self]; + + // run the connect sheet (modal) + [NSApp beginSheet:connectSheet + modalForWindow:tableWindow + modalDelegate:self + didEndSelector:@selector(connectSheetDidEnd:returnCode:contextInfo:) + contextInfo:nil]; +} + + +/* + invoked when user hits the connect-button of the connectSheet + stops modal session with code: + 1 when connected with success + 2 when no connection to host + 3 when no connection to db + 4 when hostField and socketField are empty + */ +- (IBAction)connect:(id)sender +{ + int code; + + [connectProgressBar startAnimation:self]; + [connectProgressStatusText setHidden:NO]; + [connectProgressStatusText display]; + + [selectedDatabase autorelease]; + selectedDatabase = nil; + + code = 0; + if ( [[hostField stringValue] isEqualToString:@""] && [[socketField stringValue] isEqualToString:@""] ) { + code = 4; + } else { + if ( ![[socketField stringValue] isEqualToString:@""] ) { + //connect to socket + mySQLConnection = [[CMMCPConnection alloc] initToSocket:[socketField stringValue] + withLogin:[userField stringValue] + password:[passwordField stringValue]]; + [hostField setStringValue:@"localhost"]; + } else { + //connect to host + mySQLConnection = [[CMMCPConnection alloc] initToHost:[hostField stringValue] + withLogin:[userField stringValue] + password:[passwordField stringValue] + usingPort:[portField intValue]]; + } + if ( ![mySQLConnection isConnected] ) + code = 2; + if ( !code && ![[databaseField stringValue] isEqualToString:@""] ) { + if ([mySQLConnection selectDB:[databaseField stringValue]]) { + selectedDatabase = [[databaseField stringValue] retain]; + } else { + code = 3; + } + } + if ( !code ) + code = 1; + } + + // close sheet + [connectSheet orderOut:nil]; + [NSApp endSheet:connectSheet returnCode:code]; + [connectProgressBar stopAnimation:self]; + [connectProgressStatusText setHidden:YES]; +} + +-(void)connectSheetDidEnd:(NSWindow*)sheet returnCode:(int)code contextInfo:(void*)contextInfo +{ + [sheet orderOut:self]; + + CMMCPResult *theResult; + id version; + + if ( code == 1) { + //connected with success + //register as delegate + [mySQLConnection setDelegate:self]; + // set encoding + NSString *encodingName = [prefs objectForKey:@"encoding"]; + if ( [encodingName isEqualToString:@"Autodetect"] ) { + [self detectDatabaseEncoding]; + } else { + [self setEncoding:[self mysqlEncodingFromDisplayEncoding:encodingName]]; + } + //get mysql version + theResult = [mySQLConnection queryString:@"SHOW VARIABLES LIKE 'version'"]; + version = [[theResult fetchRowAsArray] objectAtIndex:1]; + if ( [version isKindOfClass:[NSData class]] ) { + // starting with MySQL 4.1.14 the mysql variables are returned as nsdata + mySQLVersion = [[NSString alloc] initWithData:version encoding:[mySQLConnection encoding]]; + } else { + mySQLVersion = [[NSString stringWithString:version] retain]; + } + [self setDatabases:self]; + [tablesListInstance setConnection:mySQLConnection]; + [tableSourceInstance setConnection:mySQLConnection]; + [tableContentInstance setConnection:mySQLConnection]; + [customQueryInstance setConnection:mySQLConnection]; + [tableDumpInstance setConnection:mySQLConnection]; + [tableStatusInstance setConnection:mySQLConnection]; + [self setFileName:[NSString stringWithFormat:@"(MySQL %@) %@@%@ %@", mySQLVersion, [userField stringValue], + [hostField stringValue], [databaseField stringValue]]]; + [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@@%@/%@", mySQLVersion, [userField stringValue], + [hostField stringValue], [databaseField stringValue]]]; + + // Connected Growl notification + [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Connected" + description:[NSString stringWithFormat:NSLocalizedString(@"Connected to %@",@"description for connected growl notification"), [tableWindow title]] + notificationName:@"Connected"]; + + } else if (code == 2) { + //can't connect to host + NSBeginAlertSheet(NSLocalizedString(@"Connection failed!", @"connection failed"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, + @selector(sheetDidEnd:returnCode:contextInfo:), @"connect", + [NSString stringWithFormat:NSLocalizedString(@"Unable to connect to host %@.\nBe sure that the address is correct and that you have the necessary privileges.\nMySQL said: %@", @"message of panel when connection to host failed"), [hostField stringValue], [mySQLConnection getLastErrorMessage]]); + } else if (code == 3) { + //can't connect to db + NSBeginAlertSheet(NSLocalizedString(@"Connection failed!", @"connection failed"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, + @selector(sheetDidEnd:returnCode:contextInfo:), @"connect", + [NSString stringWithFormat:NSLocalizedString(@"Unable to connect to database %@.\nBe sure that the database exists and that you have the necessary privileges.\nMySQL said: %@", @"message of panel when connection to db failed"), [databaseField stringValue], [mySQLConnection getLastErrorMessage]]); + } else if (code == 4) { + //no host is given + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, + @selector(sheetDidEnd:returnCode:contextInfo:), @"connect", NSLocalizedString(@"Please enter at least a host or socket.", @"message of panel when host/socket are missing")); + } + +} + +- (IBAction)cancelConnectSheet:(id)sender +{ + [NSApp endSheet:connectSheet]; + [tableWindow close]; +} + +- (IBAction)closeSheet:(id)sender +/* + invoked when user hits the cancel button of the connectSheet + stops modal session with code 0 + reused when user hits the close button of the variablseSheet or of the createTableSyntaxSheet + */ +{ + [NSApp stopModalWithCode:0]; +} + +/** + * sets fields for the chosen favorite. + */ +- (IBAction)chooseFavorite:(id)sender +{ + if (![self selectedFavorite]) + return; + + [hostField setStringValue:[self valueForKeyPath:@"selectedFavorite.host"]]; + [socketField setStringValue:[self valueForKeyPath:@"selectedFavorite.socket"]]; + [userField setStringValue:[self valueForKeyPath:@"selectedFavorite.user"]]; + [portField setStringValue:[self valueForKeyPath:@"selectedFavorite.port"]]; + [databaseField setStringValue:[self valueForKeyPath:@"selectedFavorite.database"]]; + [passwordField setStringValue:[self selectedFavoritePassword]]; +} + +/** + * Remove the selected favourite. Instead of calling the remove: method of the Favorites NSArrayController + * directly in the XIB we do it here because we also need to remove the keychain password. + */ +- (IBAction)removeFavorite:(id)sender +{ + if (![self selectedFavorite]) { + return; + } + + NSString *name = [self valueForKeyPath:@"selectedFavorite.name"]; + NSString *user = [self valueForKeyPath:@"selectedFavorite.user"]; + NSString *host = [self valueForKeyPath:@"selectedFavorite.host"]; + NSString *database = [self valueForKeyPath:@"selectedFavorite.database"]; + + [keyChainInstance deletePasswordForName:[NSString stringWithFormat:@"Sequel Pro : %@", name] + account:[NSString stringWithFormat:@"%@@%@/%@", user, host, database]]; + [keyChainInstance deletePasswordForName:[NSString stringWithFormat:@"Sequel Pro SSHTunnel : %@", name] + account:[NSString stringWithFormat:@"%@@%@/%@", user, host, database]]; + + // Remove from favorites array controller + [favoritesController remove:[self selectedFavorite]]; + +} + +/** + * Return the favorites array. + */ +- (NSMutableArray *)favorites +{ + // if no favorites, load from user defaults + if (!favorites) { + favorites = [[NSMutableArray alloc] initWithArray:[[NSUserDefaults standardUserDefaults] objectForKey:@"favorites"]]; + } + + // if no favorites in user defaults, load empty ones + if (!favorites) { + favorites = [[NSMutableArray array] retain]; + } + + return favorites; +} + +/** + * returns a KVC-compliant proxy to the currently selected favorite, or nil if nothing selected. + * + * see [NSObjectController selection] + */ +- (id)selectedFavorite +{ + if ([favoritesController selectionIndex] == NSNotFound) + return nil; + + return [favoritesController selection]; +} + +/** + * fetches the password [self selectedFavorite] from the keychain, returns nil if no selection. + */ +- (NSString *)selectedFavoritePassword +{ + if (![self selectedFavorite]) + return nil; + + NSString *keychainName = [NSString stringWithFormat:@"Sequel Pro : %@", [self valueForKeyPath:@"selectedFavorite.name"]]; + NSString *keychainAccount = [NSString stringWithFormat:@"%@@%@/%@", + [self valueForKeyPath:@"selectedFavorite.user"], + [self valueForKeyPath:@"selectedFavorite.host"], + [self valueForKeyPath:@"selectedFavorite.database"]]; + + return [keyChainInstance getPasswordForName:keychainName account:keychainAccount]; +} + +- (void)connectSheetAddToFavorites:(id)sender +{ + [self addToFavoritesHost:[hostField stringValue] socket:[socketField stringValue] user:[userField stringValue] password:[passwordField stringValue] port:[portField stringValue] database:[databaseField stringValue] useSSH:false sshHost:@"" sshUser:@"" sshPassword:@"" sshPort:@""]; +} + +/** + * add actual connection to favorites + */ +- (void)addToFavoritesHost:(NSString *)host socket:(NSString *)socket + user:(NSString *)user password:(NSString *)password + port:(NSString *)port database:(NSString *)database + useSSH:(BOOL)useSSH // no-longer in use + sshHost:(NSString *)sshHost // no-longer in use + sshUser:(NSString *)sshUser // no-longer in use + sshPassword:(NSString *)sshPassword // no-longer in use + sshPort:(NSString *)sshPort // no-longer in use +{ + NSString *favoriteName = [NSString stringWithFormat:@"%@@%@", user, host]; + if (![database isEqualToString:@""]) + favoriteName = [NSString stringWithFormat:@"%@ %@", database, favoriteName]; + + // test if host and socket are not nil + if ([host isEqualToString:@""] && [socket isEqualToString:@""]) { + NSRunAlertPanel(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"Please enter at least a host or socket.", @"message of panel when host/socket are missing"), NSLocalizedString(@"OK", @"OK button"), nil, nil); + return; + } + + [self willChangeValueForKey:@"favorites"]; + + // write favorites and password + NSMutableDictionary *newFavorite = [NSMutableDictionary dictionaryWithObjects:[NSArray arrayWithObjects:favoriteName, host, socket, user, port, database, nil] + forKeys:[NSArray arrayWithObjects:@"name", @"host", @"socket", @"user", @"port", @"database", nil]]; + [favorites addObject:newFavorite]; + + if (![password isEqualToString:@""]) { + [keyChainInstance addPassword:password + forName:[NSString stringWithFormat:@"Sequel Pro : %@", favoriteName] + account:[NSString stringWithFormat:@"%@@%@/%@", user, host, database]]; + } + + [self didChangeValueForKey:@"favorites"]; + [favoritesController setSelectedObjects:[NSArray arrayWithObject:newFavorite]]; +} + +/** + * alert sheets method + * invoked when alertSheet get closed + * if contextInfo == connect -> reopens the connectSheet + * if contextInfo == removedatabase -> tries to remove the selected database + */ +- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo +{ + [sheet orderOut:self]; + + if ([contextInfo isEqualToString:@"connect"]) { + [self connectToDB:nil]; + return; + } + + if ([contextInfo isEqualToString:@"removedatabase"]) { + if (returnCode != NSAlertDefaultReturn) + return; + + [mySQLConnection queryString:[NSString stringWithFormat:@"DROP DATABASE `%@`", [self database]]]; + if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) { + // error while deleting db + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, [NSString stringWithFormat:NSLocalizedString(@"Couldn't remove database.\nMySQL said: %@", @"message of panel when removing db failed"), [mySQLConnection getLastErrorMessage]]); + return; + } + + // db deleted with success + selectedDatabase = nil; + [self setDatabases:self]; + [tablesListInstance setConnection:mySQLConnection]; + [tableDumpInstance setConnection:mySQLConnection]; + [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@@%@/", mySQLVersion, [userField stringValue], [hostField stringValue]]]; + } +} + + +#pragma mark database methods + +/** + * sets up the database select toolbar item + */ +- (IBAction)setDatabases:(id)sender; +{ + if (!chooseDatabaseButton) + return; + + [chooseDatabaseButton removeAllItems]; + [chooseDatabaseButton addItemWithTitle:NSLocalizedString(@"Choose Database...", @"menu item for choose db")]; + [[chooseDatabaseButton menu] addItem:[NSMenuItem separatorItem]]; + [[chooseDatabaseButton menu] addItemWithTitle:NSLocalizedString(@"Add Database...", @"menu item to add db") action:@selector(addDatabase:) keyEquivalent:@""]; + [[chooseDatabaseButton menu] addItem:[NSMenuItem separatorItem]]; + + + MCPResult *queryResult = [mySQLConnection listDBs]; + if ([queryResult numOfRows]) [queryResult dataSeek:0]; + int i; + for ( i = 0 ; i < [queryResult numOfRows] ; i++ ) { + [chooseDatabaseButton addItemWithTitle:[[queryResult fetchRowAsArray] objectAtIndex:0]]; + } + if ( ![self database] ) { + [chooseDatabaseButton selectItemAtIndex:0]; + } else { + [chooseDatabaseButton selectItemWithTitle:[self database]]; + } +} + +/** + * selects the database choosen by the user + * errorsheet if connection failed + */ +- (IBAction)chooseDatabase:(id)sender +{ + if (![tablesListInstance selectionShouldChangeInTableView:nil]) { + [chooseDatabaseButton selectItemWithTitle:[self database]]; + return; + } + + if ( [chooseDatabaseButton indexOfSelectedItem] == 0 ) { + if ([self database]) { + [chooseDatabaseButton selectItemWithTitle:[self database]]; + } + return; + } + + // show error on connection failed + if ( ![mySQLConnection selectDB:[chooseDatabaseButton titleOfSelectedItem]] ) { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, [NSString stringWithFormat:NSLocalizedString(@"Unable to connect to database %@.\nBe sure that you have the necessary privileges.", @"message of panel when connection to db failed after selecting from popupbutton"), [chooseDatabaseButton titleOfSelectedItem]]); + [self setDatabases:self]; + return; + } + + //setConnection of TablesList and TablesDump to reload tables in db + [selectedDatabase release]; + selectedDatabase = nil; + selectedDatabase = [[chooseDatabaseButton titleOfSelectedItem] retain]; + [tablesListInstance setConnection:mySQLConnection]; + [tableDumpInstance setConnection:mySQLConnection]; + [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@@%@/%@", mySQLVersion, [userField stringValue], [hostField stringValue], [self database]]]; +} + +/** + * opens the add-db sheet and creates the new db + */ +- (IBAction)addDatabase:(id)sender +{ + int code = 0; + + if (![tablesListInstance selectionShouldChangeInTableView:nil]) + return; + + [databaseNameField setStringValue:@""]; + [NSApp beginSheet:databaseSheet + modalForWindow:tableWindow + modalDelegate:self + didEndSelector:nil + contextInfo:nil]; + code = [NSApp runModalForWindow:databaseSheet]; + + [NSApp endSheet:databaseSheet]; + [databaseSheet orderOut:nil]; + + if (!code) + return; + + if ([[databaseNameField stringValue] isEqualToString:@""]) { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, NSLocalizedString(@"Database must have a name.", @"message of panel when no db name is given")); + return; + } + + [mySQLConnection queryString:[NSString stringWithFormat:@"CREATE DATABASE `%@`", [databaseNameField stringValue]]]; + if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) { + //error while creating db + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, [NSString stringWithFormat:NSLocalizedString(@"Couldn't create database.\nMySQL said: %@", @"message of panel when creation of db failed"), [mySQLConnection getLastErrorMessage]]); + return; + } + + if (![mySQLConnection selectDB:[databaseNameField stringValue]] ) { //error while selecting new db (is this possible?!) + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, [NSString stringWithFormat:NSLocalizedString(@"Unable to connect to database %@.\nBe sure that you have the necessary privileges.", @"message of panel when connection to db failed after selecting from popupbutton"), + [databaseNameField stringValue]]); + [self setDatabases:self]; + return; + } + + //select new db + [selectedDatabase release]; + selectedDatabase = nil; + selectedDatabase = [[databaseNameField stringValue] retain]; + [self setDatabases:self]; + [tablesListInstance setConnection:mySQLConnection]; + [tableDumpInstance setConnection:mySQLConnection]; + [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@@%@/%@", mySQLVersion, [userField stringValue], [hostField stringValue], selectedDatabase]]; +} + +/** + * closes the add-db sheet and stops modal session + */ +- (IBAction)closeDatabaseSheet:(id)sender +{ + [NSApp stopModalWithCode:[sender tag]]; +} + +/** + * opens sheet to ask user if he really wants to delete the db + */ +- (IBAction)removeDatabase:(id)sender +{ + if ([chooseDatabaseButton indexOfSelectedItem] == 0) + return; + if (![tablesListInstance selectionShouldChangeInTableView:nil]) + return; + + NSBeginAlertSheet(NSLocalizedString(@"Warning", @"warning"), NSLocalizedString(@"Delete", @"delete button"), NSLocalizedString(@"Cancel", @"cancel button"), nil, tableWindow, self, nil, @selector(sheetDidEnd:returnCode:contextInfo:), @"removedatabase", [NSString stringWithFormat:NSLocalizedString(@"Do you really want to delete the database %@?", @"message of panel asking for confirmation for deleting db"), [self database]]); +} + +#pragma mark console methods + +//console methods +/** + * shows or hides the console + */ +- (void)toggleConsole:(id)sender +{ + if ([self consoleIsOpened]) { + [consoleDrawer close]; + } else { + [consoleTextView scrollRangeToVisible:[consoleTextView selectedRange]]; + [consoleDrawer openOnEdge:NSMinYEdge]; + } +} + +/** + * clears the console + */ +- (void)clearConsole:(id)sender +{ + [consoleTextView setString:@""]; +} + +/** + * returns YES if the console is visible + */ +- (BOOL)consoleIsOpened +{ + return ([consoleDrawer state] == NSDrawerOpeningState || [consoleDrawer state] == NSDrawerOpenState); +} + +/** + * shows a message in the console + */ +- (void)showMessageInConsole:(NSString *)message +{ + int begin, end; + + [consoleTextView setSelectedRange:NSMakeRange([[consoleTextView string] length],0)]; + begin = [[consoleTextView string] length]; + [consoleTextView replaceCharactersInRange:NSMakeRange(begin,0) withString:message]; + end = [[consoleTextView string] length]; + [consoleTextView setTextColor:[NSColor blackColor] range:NSMakeRange(begin,end-begin)]; + + if ([self consoleIsOpened]) { + [consoleTextView displayIfNeeded]; + [consoleTextView scrollRangeToVisible:[consoleTextView selectedRange]]; + } +} + +/** + * shows an error in the console (red) + */ +- (void)showErrorInConsole:(NSString *)error +{ + int begin, end; + + [consoleTextView setSelectedRange:NSMakeRange([[consoleTextView string] length],0)]; + begin = [[consoleTextView string] length]; + [consoleTextView replaceCharactersInRange:NSMakeRange(begin,0) withString:error]; + end = [[consoleTextView string] length]; + [consoleTextView setTextColor:[NSColor redColor] range:NSMakeRange(begin,end-begin)]; + + if ([self consoleIsOpened]) { + [consoleTextView displayIfNeeded]; + [consoleTextView scrollRangeToVisible:[consoleTextView selectedRange]]; + } +} + +#pragma mark Encoding Methods + +/** + * Set the encoding for the database connection + */ +- (void)setEncoding:(NSString *)mysqlEncoding +{ + // set encoding of connection and client + [mySQLConnection queryString:[NSString stringWithFormat:@"SET NAMES '%@'", mysqlEncoding]]; + + if ( [[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + [mySQLConnection setEncoding:[CMMCPConnection encodingForMySQLEncoding:[mysqlEncoding UTF8String]]]; + [_encoding autorelease]; + _encoding = [mysqlEncoding retain]; + } else { + [self detectDatabaseEncoding]; + } + + // update the selected menu item + [self updateEncodingMenuWithSelectedEncoding:[self encodingNameFromMySQLEncoding:mysqlEncoding]]; + + // reload stuff + [tableSourceInstance reloadTable:self]; + [tableContentInstance reloadTable:self]; + [tableStatusInstance reloadTable:self]; +} + +/** + * returns the current mysql encoding for this object + */ +- (NSString *)encoding +{ + return _encoding; +} + +/** + * updates the currently selected item in the encoding menu + * + * @param NSString *encoding - the title of the menu item which will be selected + */ +- (void)updateEncodingMenuWithSelectedEncoding:(NSString *)encoding +{ + NSEnumerator *dbEncodingMenuEn = [[selectEncodingMenu itemArray] objectEnumerator]; + id menuItem; + int correctStateForMenuItem; + while (menuItem = [dbEncodingMenuEn nextObject]) { + correctStateForMenuItem = [[menuItem title] isEqualToString:encoding] ? NSOnState : NSOffState; + + if ([menuItem state] == correctStateForMenuItem) // don't re-apply state incase it causes performance issues + continue; + + [menuItem setState:correctStateForMenuItem]; + } +} + +/** + * Returns the display name for a mysql encoding + */ +- (NSString *)encodingNameFromMySQLEncoding:(NSString *)mysqlEncoding +{ + NSDictionary *translationMap = [NSDictionary dictionaryWithObjectsAndKeys: + @"UCS-2 Unicode (ucs2)", @"ucs2", + @"UTF-8 Unicode (utf8)", @"utf8", + @"US ASCII (ascii)", @"ascii", + @"ISO Latin 1 (latin1)", @"latin1", + @"Mac Roman (macroman)", @"macroman", + @"Windows Latin 2 (cp1250)", @"cp1250", + @"ISO Latin 2 (latin2)", @"latin2", + @"Windows Arabic (cp1256)", @"cp1256", + @"ISO Greek (greek)", @"greek", + @"ISO Hebrew (hebrew)", @"hebrew", + @"ISO Turkish (latin5)", @"latin5", + @"Windows Baltic (cp1257)", @"cp1257", + @"Windows Cyrillic (cp1251)", @"cp1251", + @"Big5 Traditional Chinese (big5)", @"big5", + @"Shift-JIS Japanese (sjis)", @"sjis", + @"EUC-JP Japanese (ujis)", @"ujis", + nil]; + NSString *encodingName = [translationMap valueForKey:mysqlEncoding]; + + if (!encodingName) + return [NSString stringWithFormat:@"Unknown Encoding (%@)", mysqlEncoding, nil]; + + return encodingName; +} + +/** + * Returns the mysql encoding for an encoding string that is displayed to the user + */ +- (NSString *)mysqlEncodingFromDisplayEncoding:(NSString *)encodingName +{ + NSDictionary *translationMap = [NSDictionary dictionaryWithObjectsAndKeys: + @"ucs2", @"UCS-2 Unicode (ucs2)", + @"utf8", @"UTF-8 Unicode (utf8)", + @"ascii", @"US ASCII (ascii)", + @"latin1", @"ISO Latin 1 (latin1)", + @"macroman", @"Mac Roman (macroman)", + @"cp1250", @"Windows Latin 2 (cp1250)", + @"latin2", @"ISO Latin 2 (latin2)", + @"cp1256", @"Windows Arabic (cp1256)", + @"greek", @"ISO Greek (greek)", + @"hebrew", @"ISO Hebrew (hebrew)", + @"latin5", @"ISO Turkish (latin5)", + @"cp1257", @"Windows Baltic (cp1257)", + @"cp1251", @"Windows Cyrillic (cp1251)", + @"big5", @"Big5 Traditional Chinese (big5)", + @"sjis", @"Shift-JIS Japanese (sjis)", + @"ujis", @"EUC-JP Japanese (ujis)", + nil]; + NSString *mysqlEncoding = [translationMap valueForKey:encodingName]; + + if (!mysqlEncoding) + return @"utf8"; + + return mysqlEncoding; +} + +/** + * Autodetect the connection encoding and select the relevant encoding menu item in Database -> Database Encoding + */ +- (void)detectDatabaseEncoding +{ + // MySQL > 4.0 + id mysqlEncoding = [[[mySQLConnection queryString:@"SHOW VARIABLES LIKE 'character_set_connection'"] fetchRowAsDictionary] objectForKey:@"Value"]; + _supportsEncoding = (mysqlEncoding != nil); + + if ( [mysqlEncoding isKindOfClass:[NSData class]] ) { // MySQL 4.1.14 returns the mysql variables as nsdata + mysqlEncoding = [mySQLConnection stringWithText:mysqlEncoding]; + } + if ( !mysqlEncoding ) { // mysql 4.0 or older -> only default character set possible, cannot choose others using "set names xy" + mysqlEncoding = [[[mySQLConnection queryString:@"SHOW VARIABLES LIKE 'character_set'"] fetchRowAsDictionary] objectForKey:@"Value"]; + } + if ( !mysqlEncoding ) { // older version? -> set encoding to mysql default encoding latin1 + NSLog(@"Error: no character encoding found, mysql version is %@", [self mySQLVersion]); + mysqlEncoding = @"latin1"; + } + + [mySQLConnection setEncoding:[CMMCPConnection encodingForMySQLEncoding:[mysqlEncoding cString]]]; + + // save the encoding + [_encoding autorelease]; + _encoding = [mysqlEncoding retain]; + + // update the selected menu item + [self updateEncodingMenuWithSelectedEncoding:[self encodingNameFromMySQLEncoding:mysqlEncoding]]; +} + +/** + * Detects and sets the character set encoding of the supplied table name. + */ +- (void)detectTableEncodingForTable:(NSString *)table; +{ + NSString *tableCollation = [[[mySQLConnection queryString:[NSString stringWithFormat:@"SHOW TABLE STATUS WHERE NAME = '%@'", table]] fetchRowAsDictionary] objectForKey:@"Collation"]; + + if (tableCollation != nil) { + // Split up the collation string so we can get the encoding + NSArray *encodingComponents = [tableCollation componentsSeparatedByString:@"_"]; + + if ([encodingComponents count] > 0) { + NSString *tableEncoding = [encodingComponents objectAtIndex:0]; + + [self setEncoding:tableEncoding]; + } + } + else { + NSLog(@"Error: unable to determine table collation and thus character encoding for table '%@'", table); + } +} + +/** + * When sent by an NSMenuItem, will set the encoding based on the title of the menu item + */ +- (IBAction)chooseEncoding:(id)sender +{ + [self setEncoding:[self mysqlEncodingFromDisplayEncoding:[(NSMenuItem *)sender title]]]; +} + +/** + * return YES if MySQL server supports choosing connection and table encodings (MySQL 4.1 and newer) + */ +- (BOOL)supportsEncoding +{ + return _supportsEncoding; +} + + +#pragma mark Table Methods + +- (IBAction)showCreateTableSyntax:(id)sender +{ + //Create the query and get results + NSString *query = [NSString stringWithFormat:@"SHOW CREATE TABLE `%@`", [self table]]; + CMMCPResult *theResult = [mySQLConnection queryString:query]; + + // Check for errors + if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) { + NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while creating table syntax.\n\n: %@",[mySQLConnection getLastErrorMessage]], @"OK", nil, nil); + return; + } + + id tableSyntax = [[theResult fetchRowAsArray] objectAtIndex:1]; + + if ([tableSyntax isKindOfClass:[NSData class]]) + tableSyntax = [[NSString alloc] initWithData:tableSyntax encoding:[mySQLConnection encoding]]; + + [syntaxViewContent setString:tableSyntax]; + [createTableSyntaxWindow makeKeyAndOrderFront:self]; +} + +- (IBAction)copyCreateTableSyntax:(id)sender +{ + // Create the query and get results + NSString *query = [NSString stringWithFormat:@"SHOW CREATE TABLE `%@`", [self table]]; + CMMCPResult *theResult = [mySQLConnection queryString:query]; + + // Check for errors + if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) { + NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while creating table syntax.\n\n: %@",[mySQLConnection getLastErrorMessage]], @"OK", nil, nil); + return; + } + + id tableSyntax = [[theResult fetchRowAsArray] objectAtIndex:1]; + + if ([tableSyntax isKindOfClass:[NSData class]]) + tableSyntax = [[NSString alloc] initWithData:tableSyntax encoding:[mySQLConnection encoding]]; + + // copy to the clipboard + NSPasteboard *pb = [NSPasteboard generalPasteboard]; + [pb declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:self]; + [pb setString:tableSyntax forType:NSStringPboardType]; + + // Table syntax copied Growl notification + [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Table Syntax Copied" + description:[NSString stringWithFormat:NSLocalizedString(@"Syntax for %@ table copied",@"description for table syntax copied growl notification"), [self table]] + notificationName:@"Table Syntax Copied"]; +} + +- (IBAction)checkTable:(id)sender +{ + NSString *query; + CMMCPResult *theResult; + NSDictionary *theRow; + + //Create the query and get results + query = [NSString stringWithFormat:@"CHECK TABLE `%@`", [self table]]; + theResult = [mySQLConnection queryString:query]; + + // Check for errors + if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) { + NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while checking table.\n\n: %@",[mySQLConnection getLastErrorMessage]], @"OK", nil, nil); + return; + } + + // Process result + theRow = [[theResult fetch2DResultAsType:MCPTypeDictionary] lastObject]; + NSRunInformationalAlertPanel([NSString stringWithFormat:@"Check '%@' table", [self table]], [NSString stringWithFormat:@"Check: %@", [theRow objectForKey:@"Msg_text"]], @"OK", nil, nil); +} + +- (IBAction)analyzeTable:(id)sender +{ + NSString *query; + CMMCPResult *theResult; + NSDictionary *theRow; + + //Create the query and get results + query = [NSString stringWithFormat:@"ANALYZE TABLE `%@`", [self table]]; + theResult = [mySQLConnection queryString:query]; + + // Check for errors + if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) { + NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while analyzing table.\n\n: %@",[mySQLConnection getLastErrorMessage]], @"OK", nil, nil); + return; + } + + // Process result + theRow = [[theResult fetch2DResultAsType:MCPTypeDictionary] lastObject]; + NSRunInformationalAlertPanel([NSString stringWithFormat:@"Analyze '%@' table", [self table]], [NSString stringWithFormat:@"Analyze: %@", [theRow objectForKey:@"Msg_text"]], @"OK", nil, nil); +} + +- (IBAction)optimizeTable:(id)sender +{ + NSString *query; + CMMCPResult *theResult; + NSDictionary *theRow; + + //Create the query and get results + query = [NSString stringWithFormat:@"OPTIMIZE TABLE `%@`", [self table]]; + theResult = [mySQLConnection queryString:query]; + + // Check for errors + if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while optimizing table.\n\n: %@",[mySQLConnection getLastErrorMessage]], @"OK", nil, nil); + } + + // Process result + theRow = [[theResult fetch2DResultAsType:MCPTypeDictionary] lastObject]; + NSRunInformationalAlertPanel([NSString stringWithFormat:@"Optimize '%@' table", [self table]], [NSString stringWithFormat:@"Optimize: %@", [theRow objectForKey:@"Msg_text"]], @"OK", nil, nil); +} + +- (IBAction)repairTable:(id)sender +{ + NSString *query; + CMMCPResult *theResult; + NSDictionary *theRow; + + //Create the query and get results + query = [NSString stringWithFormat:@"REPAIR TABLE `%@`", [self table]]; + theResult = [mySQLConnection queryString:query]; + + // Check for errors + if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) { + NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while repairing table.\n\n: %@",[mySQLConnection getLastErrorMessage]], @"OK", nil, nil); + } + + // Process result + theRow = [[theResult fetch2DResultAsType:MCPTypeDictionary] lastObject]; + NSRunInformationalAlertPanel([NSString stringWithFormat:@"Repair '%@' table", [self table]], [NSString stringWithFormat:@"Repair: %@", [theRow objectForKey:@"Msg_text"]], @"OK", nil, nil); +} + +- (IBAction)flushTable:(id)sender +{ + NSString *query; + CMMCPResult *theResult; + + //Create the query and get results + query = [NSString stringWithFormat:@"FLUSH TABLE `%@`", [self table]]; + theResult = [mySQLConnection queryString:query]; + + // Check for errors + if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) { + NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while flushing table.\n\n: %@",[mySQLConnection getLastErrorMessage]], @"OK", nil, nil); + return; + } + + // Process result + NSRunInformationalAlertPanel([NSString stringWithFormat:@"Flush '%@' table", [self table]], @"Flushed", @"OK", nil, nil); +} + +- (IBAction)checksumTable:(id)sender +{ + NSString *query; + CMMCPResult *theResult; + NSDictionary *theRow; + + //Create the query and get results + query = [NSString stringWithFormat:@"CHECKSUM TABLE `%@`", [self table]]; + theResult = [mySQLConnection queryString:query]; + + // Check for errors + if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) { + NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while performming checksum on table.\n\n: %@",[mySQLConnection getLastErrorMessage]], @"OK", nil, nil); + } + + // Process result + theRow = [[theResult fetch2DResultAsType:MCPTypeDictionary] lastObject]; + NSRunInformationalAlertPanel([NSString stringWithFormat:@"Checksum '%@' table", [self table]], [NSString stringWithFormat:@"Checksum: %@", [theRow objectForKey:@"Checksum"]], @"OK", nil, nil); +} + +#pragma mark Other Methods +/** + * returns the host + */ +- (NSString *)host +{ + return [hostField stringValue]; +} + +/** + * passes query to tablesListInstance + */ +- (void)doPerformQueryService:(NSString *)query +{ + [tableWindow makeKeyAndOrderFront:self]; + [tablesListInstance doPerformQueryService:query]; +} + +/** + * flushes the mysql privileges + */ +- (void)flushPrivileges:(id)sender +{ + [mySQLConnection queryString:@"FLUSH PRIVILEGES"]; + + if ( [[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + //flushed privileges without errors + NSBeginAlertSheet(NSLocalizedString(@"Flushed Privileges", @"title of panel when successfully flushed privs"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, NSLocalizedString(@"Succesfully flushed privileges.", @"message of panel when successfully flushed privs")); + } else { + //error while flushing privileges + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, [NSString stringWithFormat:NSLocalizedString(@"Couldn't flush privileges.\nMySQL said: %@", @"message of panel when flushing privs failed"), + [mySQLConnection getLastErrorMessage]]); + } +} + +- (void)showVariables:(id)sender +/* + shows the mysql variables + */ +{ + CMMCPResult *theResult; + NSMutableArray *tempResult = [NSMutableArray array]; + int i; + + if ( variables ) { + [variables release]; + variables = nil; + } + //get variables + theResult = [mySQLConnection queryString:@"SHOW VARIABLES"]; + if ([theResult numOfRows]) [theResult dataSeek:0]; + for ( i = 0 ; i < [theResult numOfRows] ; i++ ) { + [tempResult addObject:[theResult fetchRowAsDictionary]]; + } + variables = [[NSArray arrayWithArray:tempResult] retain]; + [variablesTableView reloadData]; + //show variables sheet + [NSApp beginSheet:variablesSheet + modalForWindow:tableWindow modalDelegate:self + didEndSelector:nil contextInfo:nil]; + [NSApp runModalForWindow:variablesSheet]; + + [NSApp endSheet:variablesSheet]; + [variablesSheet orderOut:nil]; +} + +- (void)closeConnection +{ + [mySQLConnection disconnect]; + + // Disconnected Growl notification + [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Disconnected" + description:[NSString stringWithFormat:NSLocalizedString(@"Disconnected from %@",@"description for disconnected growl notification"), [tableWindow title]] + notificationName:@"Disconnected"]; +} + + +//getter methods +- (NSString *)database +/* + returns the currently selected database + */ +{ + return selectedDatabase; +} + +- (NSString *)table +/* + returns the currently selected table (passing the request to TablesList) + */ +{ + return (NSString *)[tablesListInstance table]; +} + +- (NSString *)mySQLVersion +/* + returns the mysql version + */ +{ + return mySQLVersion; +} + +- (NSString *)user +/* + returns the mysql version + */ +{ + return [userField stringValue]; +} + + +//notification center methods +- (void)willPerformQuery:(NSNotification *)notification +/* + invoked before a query is performed + */ +{ + // Only start the progress indicator is this document window is key. + // Because we are starting the progress indicator based on the notification + // of a query being started, we have to prevent other windows from + // starting theirs. The same is also true for the below hasPerformedQuery: + // method. + // + // This code should be removed. Updating user interface elements based on + // notifications is bad practice as notifications are global to the application. + if ([tableWindow isKeyWindow]) { + [queryProgressBar startAnimation:self]; + } +} + +- (void)hasPerformedQuery:(NSNotification *)notification +/* + invoked after a query has been performed + */ +{ + if ([tableWindow isKeyWindow]) { + [queryProgressBar stopAnimation:self]; + } +} + +- (void)applicationWillTerminate:(NSNotification *)notification +/* + invoked when the application will terminate + */ +{ + [tablesListInstance selectionShouldChangeInTableView:nil]; +} + +- (void)tunnelStatusChanged:(NSNotification *)notification +/* + the status of the tunnel has changed + */ +{ +} + +//menu methods +- (IBAction)import:(id)sender +/* + passes the request to the tableDump object + */ +{ + [tableDumpInstance importFile]; +} + +- (IBAction)export:(id)sender +/* + passes the request to the tableDump object + */ +{ + [tableDumpInstance exportFile:[sender tag]]; +} + +- (IBAction)exportTable:(id)sender +{ + return [self export:sender]; +} + +- (IBAction)exportMultipleTables:(id)sender +{ + return [self export:sender]; +} + +/** + * Menu validation + */ +- (BOOL)validateMenuItem:(NSMenuItem *)menuItem +{ + if ([menuItem action] == @selector(import:)) { + return ([self database] != nil); + } + + if ([menuItem action] == @selector(importCSV:)) { + return ([self database] != nil && [self table] != nil); + } + + if ([menuItem action] == @selector(export:)) { + return ([self database] != nil); + } + + if ([menuItem action] == @selector(exportTable:)) { + return ([self database] != nil && [self table] != nil); + } + + if ([menuItem action] == @selector(exportMultipleTables:)) { + return ([self database] != nil); + } + + if ([menuItem action] == @selector(chooseEncoding:)) { + return [self supportsEncoding]; + } + + // table menu items + if ([menuItem action] == @selector(showCreateTableSyntax:) || + [menuItem action] == @selector(copyCreateTableSyntax:) || + [menuItem action] == @selector(checkTable:) || + [menuItem action] == @selector(analyzeTable:) || + [menuItem action] == @selector(optimizeTable:) || + [menuItem action] == @selector(repairTable:) || + [menuItem action] == @selector(flushTable:) || + [menuItem action] == @selector(checksumTable:)) + { + return ([self table] != nil && [[self table] isNotEqualTo:@""]); + } + return [super validateMenuItem:menuItem]; +} + +- (IBAction)viewStructure:(id)sender +{ + [tableTabView selectTabViewItemAtIndex:0]; + [mainToolbar setSelectedItemIdentifier:@"SwitchToTableStructureToolbarItemIdentifier"]; +} + +- (IBAction)viewContent:(id)sender +{ + [tableTabView selectTabViewItemAtIndex:1]; + [mainToolbar setSelectedItemIdentifier:@"SwitchToTableContentToolbarItemIdentifier"]; +} + +- (IBAction)viewQuery:(id)sender +{ + [tableTabView selectTabViewItemAtIndex:2]; + [mainToolbar setSelectedItemIdentifier:@"SwitchToRunQueryToolbarItemIdentifier"]; +} + +- (IBAction)viewStatus:(id)sender +{ + [tableTabView selectTabViewItemAtIndex:3]; + [mainToolbar setSelectedItemIdentifier:@"SwitchToTableStatusToolbarItemIdentifier"]; +} + + +#pragma mark Toolbar Methods + +/** + * set up the standard toolbar + */ +- (void)setupToolbar +{ + // create a new toolbar instance, and attach it to our document window + mainToolbar = [[[NSToolbar alloc] initWithIdentifier:@"TableWindowToolbar"] autorelease]; + + // set up toolbar properties + [mainToolbar setAllowsUserCustomization: YES]; + [mainToolbar setAutosavesConfiguration: YES]; + [mainToolbar setDisplayMode:NSToolbarDisplayModeIconAndLabel]; + + // set ourself as the delegate + [mainToolbar setDelegate:self]; + + // attach the toolbar to the document window + [tableWindow setToolbar:mainToolbar]; + + // select the structure toolbar item + [self viewStructure:self]; + + // update the toolbar item size + [self updateChooseDatabaseToolbarItemWidth]; +} + +/** + * toolbar delegate method + */ +- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)willBeInsertedIntoToolbar +{ + NSToolbarItem *toolbarItem = [[[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier] autorelease]; + + if ([itemIdentifier isEqualToString:@"DatabaseSelectToolbarItemIdentifier"]) { + [toolbarItem setLabel:NSLocalizedString(@"Select Database", @"toolbar item for selecting a db")]; + [toolbarItem setPaletteLabel:[toolbarItem label]]; + [toolbarItem setView:chooseDatabaseButton]; + [toolbarItem setMinSize:NSMakeSize(200,26)]; + [toolbarItem setMaxSize:NSMakeSize(200,32)]; + [chooseDatabaseButton setTarget:self]; + [chooseDatabaseButton setAction:@selector(chooseDatabase:)]; + + if (willBeInsertedIntoToolbar) { + chooseDatabaseToolbarItem = toolbarItem; + [self updateChooseDatabaseToolbarItemWidth]; + } + + } else if ([itemIdentifier isEqualToString:@"ToggleConsoleIdentifier"]) { + //set the text label to be displayed in the toolbar and customization palette + [toolbarItem setPaletteLabel:NSLocalizedString(@"Show/Hide Console", @"toolbar item for show/hide console")]; + //set up tooltip and image + [toolbarItem setToolTip:NSLocalizedString(@"Show or hide the console which shows all MySQL commands performed by Sequel Pro", @"tooltip for toolbar item for show/hide console")]; + if ( [self consoleIsOpened] ) { + [toolbarItem setLabel:NSLocalizedString(@"Hide Console", @"toolbar item for hide console")]; + [toolbarItem setImage:[NSImage imageNamed:@"hideconsole"]]; + } else { + [toolbarItem setLabel:NSLocalizedString(@"Show Console", @"toolbar item for showconsole")]; + [toolbarItem setImage:[NSImage imageNamed:@"showconsole"]]; + } + //set up the target action + [toolbarItem setTarget:self]; + [toolbarItem setAction:@selector(toggleConsole:)]; + + } else if ([itemIdentifier isEqualToString:@"ClearConsoleIdentifier"]) { + //set the text label to be displayed in the toolbar and customization palette + [toolbarItem setLabel:NSLocalizedString(@"Clear Console", @"toolbar item for clear console")]; + [toolbarItem setPaletteLabel:NSLocalizedString(@"Clear Console", @"toolbar item for clear console")]; + //set up tooltip and image + [toolbarItem setToolTip:NSLocalizedString(@"Clear the console which shows all MySQL commands performed by Sequel Pro", @"tooltip for toolbar item for clear console")]; + [toolbarItem setImage:[NSImage imageNamed:@"clearconsole"]]; + //set up the target action + [toolbarItem setTarget:self]; + [toolbarItem setAction:@selector(clearConsole:)]; + + } else if ([itemIdentifier isEqualToString:@"SwitchToTableStructureToolbarItemIdentifier"]) { + [toolbarItem setLabel:NSLocalizedString(@"Table", @"toolbar item label for switching to the Table Structure tab")]; + [toolbarItem setPaletteLabel:NSLocalizedString(@"Table Structure", @"toolbar item label for switching to the Table Structure tab")]; + //set up tooltip and image + [toolbarItem setToolTip:NSLocalizedString(@"Switch to the Table Structure tab", @"tooltip for toolbar item for switching to the Table Structure tab")]; + [toolbarItem setImage:[NSImage imageNamed:@"toolbar-switch-to-structure"]]; + //set up the target action + [toolbarItem setTarget:self]; + [toolbarItem setAction:@selector(viewStructure:)]; + + } else if ([itemIdentifier isEqualToString:@"SwitchToTableContentToolbarItemIdentifier"]) { + [toolbarItem setLabel:NSLocalizedString(@"Browse", @"toolbar item label for switching to the Table Content tab")]; + [toolbarItem setPaletteLabel:NSLocalizedString(@"Table Content", @"toolbar item label for switching to the Table Content tab")]; + //set up tooltip and image + [toolbarItem setToolTip:NSLocalizedString(@"Switch to the Table Content tab", @"tooltip for toolbar item for switching to the Table Content tab")]; + [toolbarItem setImage:[NSImage imageNamed:@"toolbar-switch-to-browse"]]; + //set up the target action + [toolbarItem setTarget:self]; + [toolbarItem setAction:@selector(viewContent:)]; + + } else if ([itemIdentifier isEqualToString:@"SwitchToRunQueryToolbarItemIdentifier"]) { + [toolbarItem setLabel:NSLocalizedString(@"SQL", @"toolbar item label for switching to the Run Query tab")]; + [toolbarItem setPaletteLabel:NSLocalizedString(@"Run Query", @"toolbar item label for switching to the Run Query tab")]; + //set up tooltip and image + [toolbarItem setToolTip:NSLocalizedString(@"Switch to the Run Query tab", @"tooltip for toolbar item for switching to the Run Query tab")]; + [toolbarItem setImage:[NSImage imageNamed:@"toolbar-switch-to-sql"]]; + //set up the target action + [toolbarItem setTarget:self]; + [toolbarItem setAction:@selector(viewQuery:)]; + + } else if ([itemIdentifier isEqualToString:@"SwitchToTableStatusToolbarItemIdentifier"]) { + [toolbarItem setLabel:NSLocalizedString(@"Table Status", @"toolbar item label for switching to the Table Status tab")]; + [toolbarItem setPaletteLabel:NSLocalizedString(@"Table Status", @"toolbar item label for switching to the Table Status tab")]; + //set up tooltip and image + [toolbarItem setToolTip:NSLocalizedString(@"Switch to the Table Status tab", @"tooltip for toolbar item for switching to the Table Status tab")]; + [toolbarItem setImage:[NSImage imageNamed:@"toolbar-switch-to-table-info"]]; + //set up the target action + [toolbarItem setTarget:self]; + [toolbarItem setAction:@selector(viewStatus:)]; + + } else { + //itemIdentifier refered to a toolbar item that is not provided or supported by us or cocoa + toolbarItem = nil; + } + + return toolbarItem; +} + +/** + * toolbar delegate method + */ +- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar +{ + return [NSArray arrayWithObjects: + @"DatabaseSelectToolbarItemIdentifier", + @"ToggleConsoleIdentifier", + @"ClearConsoleIdentifier", + @"FlushPrivilegesIdentifier", + @"SwitchToTableStructureToolbarItemIdentifier", + @"SwitchToTableContentToolbarItemIdentifier", + @"SwitchToRunQueryToolbarItemIdentifier", + @"SwitchToTableStatusToolbarItemIdentifier", + NSToolbarCustomizeToolbarItemIdentifier, + NSToolbarFlexibleSpaceItemIdentifier, + NSToolbarSpaceItemIdentifier, + NSToolbarSeparatorItemIdentifier, + nil]; +} + +/** + * toolbar delegate method + */ +- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar +{ + return [NSArray arrayWithObjects: + @"DatabaseSelectToolbarItemIdentifier", + NSToolbarFlexibleSpaceItemIdentifier, + @"SwitchToTableStructureToolbarItemIdentifier", + @"SwitchToTableContentToolbarItemIdentifier", + @"SwitchToRunQueryToolbarItemIdentifier", + NSToolbarFlexibleSpaceItemIdentifier, + nil]; +} + +- (NSArray *)toolbarSelectableItemIdentifiers:(NSToolbar *)toolbar +{ + return [NSArray arrayWithObjects: + @"SwitchToTableStructureToolbarItemIdentifier", + @"SwitchToTableContentToolbarItemIdentifier", + @"SwitchToRunQueryToolbarItemIdentifier", + @"SwitchToTableStatusToolbarItemIdentifier", + nil]; + +} + +/** + * validates the toolbar items + */ +- (BOOL)validateToolbarItem:(NSToolbarItem *)toolbarItem; +{ + if ( [[toolbarItem itemIdentifier] isEqualToString:@"ToggleConsoleIdentifier"] ) { + if ( [self consoleIsOpened] ) { + [toolbarItem setLabel:@"Hide Console"]; + [toolbarItem setImage:[NSImage imageNamed:@"hideconsole"]]; + } else { + [toolbarItem setLabel:@"Show Console"]; + [toolbarItem setImage:[NSImage imageNamed:@"showconsole"]]; + } + } + + return YES; +} + + +//NSDocument methods +- (NSString *)windowNibName +/* + returns the name of the nib file + */ +{ + return @"DBView"; +} + +- (void)windowControllerDidLoadNib:(NSWindowController *) aController +/* + code that need to be executed once the windowController has loaded the document's window + sets upt the interface (small fonts) + */ +{ + [aController setShouldCascadeWindows:YES]; + [super windowControllerDidLoadNib:aController]; + + NSEnumerator *theCols = [[variablesTableView tableColumns] objectEnumerator]; + NSTableColumn *theCol; + + // [tableWindow makeKeyAndOrderFront:self]; + + prefs = [[NSUserDefaults standardUserDefaults] retain]; + + //register for notifications + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willPerformQuery:) + name:@"SMySQLQueryWillBePerformed" object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(hasPerformedQuery:) + name:@"SMySQLQueryHasBeenPerformed" object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) + name:@"NSApplicationWillTerminateNotification" object:nil]; + + //set up interface + if ( [prefs boolForKey:@"useMonospacedFonts"] ) { + [consoleTextView setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]]; + [syntaxViewContent setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]]; + + while ( (theCol = [theCols nextObject]) ) { + [[theCol dataCell] setFont:[NSFont fontWithName:@"Monaco" size:10]]; + } + } else { + [consoleTextView setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + [syntaxViewContent setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + while ( (theCol = [theCols nextObject]) ) { + [[theCol dataCell] setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + } + } + [consoleDrawer setContentSize:NSMakeSize(110,110)]; + + //set up toolbar + [self setupToolbar]; + // [self connectToDB:nil]; + [self performSelector:@selector(connectToDB:) withObject:tableWindow afterDelay:0.0f]; +} + +- (void)windowWillClose:(NSNotification *)aNotification +{ + if ([mySQLConnection isConnected]) [self closeConnection]; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + + +//NSWindow delegate methods +- (BOOL)windowShouldClose:(id)sender +/* + invoked when the document window should close + */ +{ + if ( ![tablesListInstance selectionShouldChangeInTableView:nil] ) { + return NO; + } else { + return YES; + } + +} + + +//SMySQL delegate methods +- (void)willQueryString:(NSString *)query +/* +invoked when framework will perform a query +*/ +{ + NSString *currentTime = [[NSDate date] descriptionWithCalendarFormat:@"%H:%M:%S" timeZone:nil locale:nil]; + + [self showMessageInConsole:[NSString stringWithFormat:@"/* MySQL %@ */ %@;\n", currentTime, query]]; +} + +- (void)queryGaveError:(NSString *)error +/* +invoked when query gave an error +*/ +{ + NSString *currentTime = [[NSDate date] descriptionWithCalendarFormat:@"%H:%M:%S" timeZone:nil locale:nil]; + + [self showErrorInConsole:[NSString stringWithFormat:@"/* ERROR %@ */ %@;\n", currentTime, error]]; +} + +#pragma mark SplitView delegate methods + +/** + * tells the splitView that it can collapse views + */ +- (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview +{ + return YES; +} + +/** + * defines max position of splitView + */ +- (float)splitView:(NSSplitView *)sender constrainMaxCoordinate:(float)proposedMax ofSubviewAt:(int)offset +{ + return proposedMax - 600; +} + +/** + * defines min position of splitView + */ +- (float)splitView:(NSSplitView *)sender constrainMinCoordinate:(float)proposedMin ofSubviewAt:(int)offset +{ + return proposedMin + 160; +} + +- (void)splitViewDidResizeSubviews:(NSNotification *)notification +{ + [self updateChooseDatabaseToolbarItemWidth]; +} + +- (NSRect)splitView:(NSSplitView *)splitView additionalEffectiveRectOfDividerAtIndex:(int)dividerIndex +{ + if (sidebarGrabber != nil) { + return [sidebarGrabber convertRect:[sidebarGrabber bounds] toView:splitView]; + } else { + return NSZeroRect; + } +} + +- (void)updateChooseDatabaseToolbarItemWidth +{ + // make sure the toolbar item is actually in the toolbar + if (!chooseDatabaseToolbarItem) + return; + + // grab the width of the left pane + float leftPaneWidth = [dbTablesTableView frame].size.width; + + // subtract some pixels to allow for misc stuff + leftPaneWidth -= 12; + + // make sure it's not too small or to big + if (leftPaneWidth < 130) + leftPaneWidth = 130; + if (leftPaneWidth > 360) + leftPaneWidth = 360; + + // apply the size + [chooseDatabaseToolbarItem setMinSize:NSMakeSize(leftPaneWidth, 26)]; + [chooseDatabaseToolbarItem setMaxSize:NSMakeSize(leftPaneWidth, 32)]; +} + + +//tableView datasource methods +- (int)numberOfRowsInTableView:(NSTableView *)aTableView +{ + return [variables count]; +} + +- (id)tableView:(NSTableView *)aTableView +objectValueForTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex +{ + id theValue; + + theValue = [[variables objectAtIndex:rowIndex] objectForKey:[aTableColumn identifier]]; + + if ( [theValue isKindOfClass:[NSData class]] ) { + theValue = [[NSString alloc] initWithData:theValue encoding:[mySQLConnection encoding]]; + } + + return theValue; +} + +- (IBAction)terminate:(id)sender +{ + [[NSApp orderedDocuments] makeObjectsPerformSelector:@selector(cancelConnectSheet:) withObject:nil]; + + // Save the favourites - commits any unsaved changes ie favourite renames + [prefs setObject:[self favorites] forKey:@"favorites"]; + + [NSApp terminate:sender]; +} + +- (void)dealloc +{ + [chooseDatabaseButton release]; + [mySQLConnection release]; + [favorites release]; + [variables release]; + [selectedDatabase release]; + [mySQLVersion release]; + [prefs release]; + + [super dealloc]; +} + +@end diff --git a/Source/TableDocumentSplitView.h b/Source/TableDocumentSplitView.h new file mode 100644 index 00000000..798e8088 --- /dev/null +++ b/Source/TableDocumentSplitView.h @@ -0,0 +1,35 @@ +// +// TableDocumentSplitView.h +// sequel-pro +// +// Created by Abhi Beckert (abhibeckert.com) on 2008-04-24. +// +// 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 <Cocoa/Cocoa.h> + +/** + * This is the main split view in the main database window (left col tables list + * right col main window content) + */ + +@interface TableDocumentSplitView : NSSplitView { + +} + +@end diff --git a/Source/TableDocumentSplitView.m b/Source/TableDocumentSplitView.m new file mode 100644 index 00000000..f8784d0e --- /dev/null +++ b/Source/TableDocumentSplitView.m @@ -0,0 +1,39 @@ +// +// TableDocumentSplitView.m +// sequel-pro +// +// Created by Abhi Beckert (abhibeckert.com) on 2008-04-24. +// +// 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 "TableDocumentSplitView.h" + +@implementation TableDocumentSplitView + +- (float)dividerThickness +{ + return 1; +} + +- (void)drawDividerInRect:(NSRect)rect +{ + [[NSColor controlShadowColor] set]; + NSRectFill(rect); +} + +@end diff --git a/Source/TableDump.h b/Source/TableDump.h new file mode 100644 index 00000000..22b123af --- /dev/null +++ b/Source/TableDump.h @@ -0,0 +1,156 @@ +// +// TableDump.h +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Wed May 01 2002. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import <Cocoa/Cocoa.h> +#import <MCPKit_bundled/MCPKit_bundled.h> +#import "CMMCPConnection.h" +#import "CMMCPResult.h" + + +@interface TableDump : NSObject { + + IBOutlet id tableDocumentInstance; + IBOutlet id tablesListInstance; + IBOutlet id tableSourceInstance; + IBOutlet id tableContentInstance; + IBOutlet id customQueryInstance; + + IBOutlet id tableWindow; + IBOutlet id tableListView; + + IBOutlet id exportDumpView; + IBOutlet id exportCSVView; + IBOutlet id exportMultipleCSVView; + IBOutlet id exportMultipleXMLView; + IBOutlet id exportDumpTableView; + IBOutlet id exportMultipleCSVTableView; + IBOutlet id exportMultipleXMLTableView; + IBOutlet id exportFieldNamesSwitch; + IBOutlet id exportFieldsTerminatedField; + IBOutlet id exportFieldsEnclosedField; + IBOutlet id exportFieldsEscapedField; + IBOutlet id exportLinesTerminatedField; + IBOutlet id exportMultipleFieldNamesSwitch; + IBOutlet id exportMultipleFieldsTerminatedField; + IBOutlet id exportMultipleFieldsEnclosedField; + IBOutlet id exportMultipleFieldsEscapedField; + IBOutlet id exportMultipleLinesTerminatedField; + + IBOutlet id importCSVView; + IBOutlet id importFormatPopup; + IBOutlet id importCSVBox; + IBOutlet id importFieldNamesSwitch; + IBOutlet id importFieldsTerminatedField; + IBOutlet id importFieldsEnclosedField; + IBOutlet id importFieldsEscapedField; + IBOutlet id importLinesTerminatedField; + + IBOutlet id addDropTableSwitch; + IBOutlet id addCreateTableSwitch; + IBOutlet id addTableContentSwitch; + IBOutlet id addErrorsSwitch; + IBOutlet id errorsSheet; + IBOutlet id errorsView; + IBOutlet id singleProgressSheet; + IBOutlet id singleProgressBar; + IBOutlet id singleProgressText; + + IBOutlet id fieldMappingSheet; + IBOutlet id fieldMappingPopup; + IBOutlet id fieldMappingTableView; + + IBOutlet id rowUpButton; + IBOutlet id rowDownButton; + IBOutlet id recordCountLabel; + + CMMCPConnection *mySQLConnection; + + NSMutableArray *tables; + NSArray *importArray; + NSMutableArray *fieldMappingArray; + NSMutableArray *fieldMappingButtonOptions; + int currentRow; + NSString *savePath; + NSString *openPath; + NSUserDefaults *prefs; +} + +//IBAction methods +- (IBAction)reloadTables:(id)sender; +- (IBAction)selectTables:(id)sender; +- (IBAction)closeSheet:(id)sender; +- (IBAction)stepRow:(id)sender; +//- (IBAction)chooseDumpType:(id)sender; + +//export methods +//- (IBAction)saveDump:(id)sender; +- (void)exportFile:(int)tag; +- (void)savePanelDidEnd:(NSSavePanel *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo; + +//import methods +//- (IBAction)openDump:(id)sender; +- (void)importFile; +- (IBAction)changeFormat:(id)sender; +- (IBAction)changeTable:(id)sender; +- (void)openPanelDidEnd:(NSOpenPanel *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo; +- (void)setupFieldMappingArray; +- (void)updateFieldMappingButtonCell; +- (NSArray *)arrayForCSV:(NSString *)csv terminatedBy:(NSString *)terminated + enclosedBy:(NSString *)enclosed escapedBy:(NSString *)escaped lineEnds:(NSString *)lineEnds; +- (NSArray *)arrayForString:(NSString *)string enclosed:(NSString *)enclosed + escaped:(NSString *)escaped terminated:(NSString *)terminated; +- (NSArray *)splitQueries:(NSString *)query; + +// Export methods +- (BOOL)dumpSelectedTablesAsSqlToFileHandle:(NSFileHandle *)fileHandle; +- (BOOL)writeCsvForArray:(NSArray *)array orQueryResult:(CMMCPResult *)queryResult + toFileHandle:(NSFileHandle *)fileHandle + outputFieldNames:(BOOL)firstLine terminatedBy:(NSString *)terminated + enclosedBy:(NSString *)enclosed escapedBy:(NSString *)escaped + lineEnds:(NSString *)lineEnds silently:(BOOL)silently; +- (BOOL)writeXmlForArray:(NSArray *)array orQueryResult:(CMMCPResult *)queryResult + toFileHandle:(NSFileHandle *)fileHandle + tableName:(NSString *)table withHeader:(BOOL)header silently:(BOOL)silently; +- (NSString *)htmlEscapeString:(NSString *)string; +- (BOOL)exportTables:(NSArray *)selectedTables toFileHandle:(NSFileHandle *)fileHandle usingFormat:(NSString *)type; +- (BOOL)exportSelectedTablesToFileHandle:(NSFileHandle *)fileHandle usingFormat:(NSString *)type; + +//additional methods +- (void)setConnection:(CMMCPConnection *)theConnection; + +//tableView datasource methods +- (int)numberOfRowsInTableView:(NSTableView *)aTableView; +- (id)tableView:(NSTableView *)aTableView + objectValueForTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex; +- (void)tableView:(NSTableView *)aTableView + setObjectValue:(id)anObject + forTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex; + +//last but not least +- (id)init; +- (void)dealloc; + +@end diff --git a/Source/TableDump.m b/Source/TableDump.m new file mode 100644 index 00000000..277ed3e7 --- /dev/null +++ b/Source/TableDump.m @@ -0,0 +1,1892 @@ +// +// TableDump.m +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Wed May 01 2002. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import "TableDump.h" +#import "TableDocument.h" +#import "TablesList.h" +#import "TableSource.h" +#import "TableContent.h" +#import "CustomQuery.h" +#import "SPGrowlController.h" + +@implementation TableDump + +//IBAction methods +- (IBAction)reloadTables:(id)sender +/* + get the tables in db + */ +{ + CMMCPResult *queryResult; + int i; + + //get tables + [tables removeAllObjects]; + queryResult = (CMMCPResult *)[mySQLConnection listTables]; + + if ([queryResult numOfRows]) [queryResult dataSeek:0]; + for ( i = 0 ; i < [queryResult numOfRows] ; i++ ) { + [tables addObject:[NSMutableArray arrayWithObjects: + [NSNumber numberWithBool:YES], [[queryResult fetchRowAsArray] objectAtIndex:0], nil]]; + } + + [exportDumpTableView reloadData]; + [exportMultipleCSVTableView reloadData]; + [exportMultipleXMLTableView reloadData]; + +} + +- (IBAction)selectTables:(id)sender +/* + selects or deselects all tables + */ +{ + NSEnumerator *enumerator; + id theObject; + + [self reloadTables:self]; + + enumerator = [tables objectEnumerator]; + while ( (theObject = [enumerator nextObject]) ) { + if ( [sender tag] ) { + [theObject replaceObjectAtIndex:0 withObject:[NSNumber numberWithBool:YES]]; + } else { + [theObject replaceObjectAtIndex:0 withObject:[NSNumber numberWithBool:NO]]; + } + } + + [exportDumpTableView reloadData]; + [exportMultipleCSVTableView reloadData]; + [exportMultipleXMLTableView reloadData]; +} + +- (IBAction)closeSheet:(id)sender +/* + ends the modal session + */ +{ + [NSApp stopModalWithCode:[sender tag]]; +} + +#pragma mark - +#pragma mark export methods + +- (void)exportFile:(int)tag +/* + invoked when user clicks on an export menuItem + */ +{ + NSString *file; + NSString *contextInfo; + NSSavePanel *savePanel = [NSSavePanel savePanel]; + NSString *currentDate = [[NSDate date] descriptionWithCalendarFormat:@"%d.%m.%Y" timeZone:nil locale:nil]; + + switch ( tag ) { + case 5: + // export dump + [self reloadTables:self]; + file = [NSString stringWithFormat:@"%@_dump %@.sql", [tableDocumentInstance database], currentDate]; + [savePanel setAccessoryView:exportDumpView]; + contextInfo = @"exportDump"; + break; + + // Export the full resultset for the currently selected table to a file in CSV format + case 6: + file = [NSString stringWithFormat:@"%@.csv", [tableDocumentInstance table]]; + [savePanel setAccessoryView:exportCSVView]; + contextInfo = @"exportTableContentAsCSV"; + break; + + // Export the full resultset for the currently selected table to a file in XML format + case 7: + file = [NSString stringWithFormat:@"%@.xml", [tableDocumentInstance table]]; + contextInfo = @"exportTableContentAsXML"; + break; + + // Export the current "browse" view to a file in CSV format + case 8: + file = [NSString stringWithFormat:@"%@ view.csv", [tableDocumentInstance table]]; + [savePanel setAccessoryView:exportCSVView]; + contextInfo = @"exportBrowseViewAsCSV"; + break; + + // Export the current "browse" view to a file in XML format + case 9: + file = [NSString stringWithFormat:@"%@ view.xml", [tableDocumentInstance table]]; + contextInfo = @"exportBrowseViewAsXML"; + break; + + // Export the current custom query result set to a file in CSV format + case 10: + file = @"customresult.csv"; + [savePanel setAccessoryView:exportCSVView]; + contextInfo = @"exportCustomResultAsCSV"; + break; + + // Export the current custom query result set to a file in XML format + case 11: + file = @"customresult.xml"; + contextInfo = @"exportCustomResultAsXML"; + break; + + // Export multiple tables to a file in CSV format + case 12: + [self reloadTables:self]; + file = [NSString stringWithFormat:@"%@.csv", [tableDocumentInstance database]]; + [savePanel setAccessoryView:exportMultipleCSVView]; + contextInfo = @"exportMultipleTablesAsCSV"; + break; + + // Export multiple tables to a file in XML format + case 13: + [self reloadTables:self]; + file = [NSString stringWithFormat:@"%@.xml", [tableDocumentInstance database]]; + [savePanel setAccessoryView:exportMultipleXMLView]; + contextInfo = @"exportMultipleTablesAsXML"; + break; + default: + NSLog(@"ERROR: unknown export item with tag %d", tag); + return; + break; + } + + // Open the savePanel + [savePanel beginSheetForDirectory:[prefs objectForKey:@"savePath"] + file:file modalForWindow:tableWindow modalDelegate:self + didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:) contextInfo:contextInfo]; +} + +/* + Save the export file; open a file handle, pass it to the appropriate data-writing function for streaming the export, and close the handle. + */ +- (void)savePanelDidEnd:(NSSavePanel *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo +{ + NSFileHandle *fileHandle = nil; + BOOL success; + + [sheet orderOut:self]; + + if ( returnCode != NSOKButton ) + return; + + // Save path to preferences + [prefs setObject:[sheet directory] forKey:@"savePath"]; + + // Error if the file already exists and is not writable, and get a fileHandle to it. + if ( [[NSFileManager defaultManager] fileExistsAtPath:[sheet filename]] ) { + if ( ![[NSFileManager defaultManager] isWritableFileAtPath:[sheet filename]] + || !(fileHandle = [NSFileHandle fileHandleForWritingAtPath:[sheet filename]]) ) { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + NSLocalizedString(@"Couldn't replace the file. Be sure that you have the necessary privileges.", @"message of panel when file cannot be replaced")); + return; + } + + // Truncate the file to zero bytes + [fileHandle truncateFileAtOffset:0]; + + // Otherwise attempt to create a file + } else { + if ( ![[NSFileManager defaultManager] createFileAtPath:[sheet filename] contents:[NSData data] attributes:nil] ) { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + NSLocalizedString(@"Couldn't write to file. Be sure that you have the necessary privileges.", @"message of panel when file cannot be written")); + return; + } + + // Retrieve a filehandle for the file, attempting to delete it on failure. + fileHandle = [NSFileHandle fileHandleForWritingAtPath:[sheet filename]]; + if ( !fileHandle ) { + [[NSFileManager defaultManager] removeFileAtPath:[sheet filename] handler:nil]; + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + NSLocalizedString(@"Couldn't write to file. Be sure that you have the necessary privileges.", @"message of panel when file cannot be written")); + return; + } + } + + // Export the tables selected in the MySQL export sheet to a file + if ( [contextInfo isEqualToString:@"exportDump"] ) { + success = [self dumpSelectedTablesAsSqlToFileHandle:fileHandle]; + + // Export the full resultset for the currently selected table to a file in CSV format + } else if ( [contextInfo isEqualToString:@"exportTableContentAsCSV"] ) { + success = [self exportTables:[NSArray arrayWithObject:[tableDocumentInstance table]] toFileHandle:fileHandle usingFormat:@"csv"]; + + // Export the full resultset for the currently selected table to a file in XML format + } else if ( [contextInfo isEqualToString:@"exportTableContentAsXML"] ) { + success = [self exportTables:[NSArray arrayWithObject:[tableDocumentInstance table]] toFileHandle:fileHandle usingFormat:@"xml"]; + + // Export the current "browse" view to a file in CSV format + } else if ( [contextInfo isEqualToString:@"exportBrowseViewAsCSV"] ) { + success = [self writeCsvForArray:[tableContentInstance currentResult] orQueryResult:nil + toFileHandle:fileHandle + outputFieldNames:[exportFieldNamesSwitch state] + terminatedBy:[exportFieldsTerminatedField stringValue] + enclosedBy:[exportFieldsEnclosedField stringValue] + escapedBy:[exportFieldsEscapedField stringValue] + lineEnds:[exportLinesTerminatedField stringValue] + silently:NO]; + + // Export the current "browse" view to a file in XML format + } else if ( [contextInfo isEqualToString:@"exportBrowseViewAsXML"] ) { + success = [self writeXmlForArray:[tableContentInstance currentResult] orQueryResult:nil + toFileHandle:fileHandle + tableName:[tableDocumentInstance table] + withHeader:YES + silently:NO]; + + // Export the current custom query result set to a file in CSV format + } else if ( [contextInfo isEqualToString:@"exportCustomResultAsCSV"] ) { + success = [self writeCsvForArray:[customQueryInstance currentResult] orQueryResult:nil + toFileHandle:fileHandle + outputFieldNames:[exportFieldNamesSwitch state] + terminatedBy:[exportFieldsTerminatedField stringValue] + enclosedBy:[exportFieldsEnclosedField stringValue] + escapedBy:[exportFieldsEscapedField stringValue] + lineEnds:[exportLinesTerminatedField stringValue] + silently:NO]; + + // Export the current custom query result set to a file in XML format + } else if ( [contextInfo isEqualToString:@"exportCustomResultAsXML"] ) { + success = [self writeXmlForArray:[customQueryInstance currentResult] orQueryResult:nil + toFileHandle:fileHandle + tableName:@"custom" + withHeader:YES + silently:NO]; + + // Export multiple tables to a file in CSV format + } else if ( [contextInfo isEqualToString:@"exportMultipleTablesAsCSV"] ) { + success = [self exportSelectedTablesToFileHandle:fileHandle usingFormat:@"csv"]; + + // Export multiple tables to a file in XML format + } else if ( [contextInfo isEqualToString:@"exportMultipleTablesAsXML"] ) { + success = [self exportSelectedTablesToFileHandle:fileHandle usingFormat:@"xml"]; + + // Unknown operation + } else { + NSLog(@"Unknown export operation: %@", [contextInfo description]); + return; + } + + // Close the file handle + [fileHandle closeFile]; + + if ( !success ) { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + NSLocalizedString(@"Couldn't write to file. Be sure that you have the necessary privileges.", @"message of panel when file cannot be written")); + } + + // Export finished Growl notification + [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Export Finished" + description:[NSString stringWithFormat:NSLocalizedString(@"Finished exporting to %@",@"description for finished exporting growl notification"), [[sheet filename] lastPathComponent]] + notificationName:@"Export Finished"]; +} + +#pragma mark - +#pragma mark import methods + +- (void)importFile +/* + invoked when user clicks on an import menuItem + */ +{ + NSOpenPanel *openPanel = [NSOpenPanel openPanel]; + [openPanel setAccessoryView:importCSVView]; + + // Show openPanel + [openPanel beginSheetForDirectory:[prefs objectForKey:@"openPath"] + file:nil + modalForWindow:tableWindow + modalDelegate:self + didEndSelector:@selector(openPanelDidEnd:returnCode:contextInfo:) + contextInfo:nil]; +} + +- (IBAction)changeFormat:(id)sender +{ + [importCSVBox setHidden:![[[importFormatPopup selectedItem] title] isEqualToString:@"CSV"]]; +} + +- (IBAction)changeTable:(id)sender +{ + [tableListView selectRowIndexes:[NSIndexSet indexSetWithIndex:[[tablesListInstance tables] indexOfObject:[fieldMappingPopup titleOfSelectedItem]]] byExtendingSelection:NO]; + + //set up tableView + currentRow = 0; + fieldMappingArray = nil; + [self setupFieldMappingArray]; + [rowDownButton setEnabled:NO]; + [rowUpButton setEnabled:([importArray count] > 1)]; + [recordCountLabel setStringValue:[NSString stringWithFormat:@"%i of %i records", currentRow+1, [importArray count]]]; + + [self updateFieldMappingButtonCell]; + [fieldMappingTableView reloadData]; +} + +- (void)openPanelDidEnd:(NSOpenPanel *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo +/* + reads mysql-dumpfile + */ +{ + NSString *dumpFile; + NSError **errorStr; + NSMutableString *errors = [NSMutableString string]; + NSString *fileType = [[importFormatPopup selectedItem] title]; + + [sheet orderOut:self]; + + if ( returnCode != NSOKButton ) + return; + + //save path to preferences + [prefs setObject:[sheet directory] forKey:@"openPath"]; + + //load file into string + if ( [NSString respondsToSelector:@selector(stringWithContentsOfFile:encoding:error:)] ) { + // mac os 10.4 or later + dumpFile = [NSString stringWithContentsOfFile:[sheet filename] + encoding:[CMMCPConnection encodingForMySQLEncoding:[[tableDocumentInstance encoding] cString]] + error:errorStr]; + } else { + // mac os pre 10.4 + dumpFile = [NSString stringWithContentsOfFile:[sheet filename]]; + } + + if ( !dumpFile ) { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"Title of error alert"), + NSLocalizedString(@"OK", @"OK button"), + nil, nil, + tableWindow, self, + nil, nil, nil, + NSLocalizedString(@"Couldn't open file. Be sure that the path is correct and that you have the necessary privileges.", @"Message of panel when file cannot be opened")); + return; + } + + // reset interface + [errorsView setString:@""]; + [errorsView displayIfNeeded]; + [singleProgressText setStringValue:NSLocalizedString(@"Reading...", @"text showing that app is reading dump")]; + [singleProgressText displayIfNeeded]; + [singleProgressBar setDoubleValue:0]; + [singleProgressBar displayIfNeeded]; + + if ( [fileType isEqualToString:@"SQL"] ) { + + //import dump file + NSArray *queries; + int i; + + //open progress sheet + [NSApp beginSheet:singleProgressSheet + modalForWindow:tableWindow + modalDelegate:self + didEndSelector:nil + contextInfo:nil]; + + [singleProgressBar setIndeterminate:YES]; + [singleProgressBar setUsesThreadedAnimation:YES]; + [singleProgressBar startAnimation:self]; + + //get array with an object for each mysql-query + queries = [self splitQueries:dumpFile]; + + [singleProgressBar stopAnimation:self]; + [singleProgressBar setUsesThreadedAnimation:NO]; + [singleProgressBar setIndeterminate:NO]; + + //perform all mysql-queries + for ( i = 0 ; i < [queries count] ; i++ ) { + [singleProgressBar setDoubleValue:((i+1)*100/[queries count])]; + [singleProgressBar displayIfNeeded]; + [mySQLConnection queryString:[queries objectAtIndex:i]]; + + if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""] && ![[mySQLConnection getLastErrorMessage] isEqualToString:@"Query was empty"]) { + [errors appendString:[NSString stringWithFormat:NSLocalizedString(@"[ERROR in query %d] %@\n", @"error text when multiple custom query failed"), (i+1),[mySQLConnection getLastErrorMessage]]]; + } + } + + //close progress sheet + [NSApp endSheet:singleProgressSheet]; + [singleProgressSheet orderOut:nil]; + + //display errors + if ( [errors length] ) { + [errorsView setString:errors]; + [NSApp beginSheet:errorsSheet + modalForWindow:tableWindow + modalDelegate:self + didEndSelector:nil + contextInfo:nil]; + + [NSApp runModalForWindow:errorsSheet]; + + [NSApp endSheet:errorsSheet]; + [errorsSheet orderOut:nil]; + } + + //////////////// + // IMPORT CSV // + //////////////// + + } else if ( [fileType isEqualToString:@"CSV"] ) { + //import csv file + int code; + NSPopUpButtonCell *buttonCell = [[NSPopUpButtonCell alloc] init]; + + //open progress sheet + [NSApp beginSheet:singleProgressSheet + modalForWindow:tableWindow + modalDelegate:self + didEndSelector:nil + contextInfo:nil]; + + [singleProgressBar setIndeterminate:YES]; + [singleProgressBar setUsesThreadedAnimation:YES]; + [singleProgressBar startAnimation:self]; + + //put file in array + if ( importArray ) + [importArray release]; + + importArray = [[self arrayForCSV:dumpFile + terminatedBy:[importFieldsTerminatedField stringValue] + enclosedBy:[importFieldsEnclosedField stringValue] + escapedBy:[importFieldsEscapedField stringValue] + lineEnds:[importLinesTerminatedField stringValue]] retain]; + + //close progress sheet + [NSApp endSheet:singleProgressSheet]; + [singleProgressSheet orderOut:nil]; + [singleProgressBar stopAnimation:self]; + [singleProgressBar setUsesThreadedAnimation:NO]; + [singleProgressBar setIndeterminate:NO]; + + + CMMCPResult *theResult; + int i; + theResult = (CMMCPResult *) [mySQLConnection listTables]; + if ([theResult numOfRows]) [theResult dataSeek:0]; + for ( i = 0 ; i < [theResult numOfRows] ; i++ ) { + [fieldMappingPopup addItemWithTitle:[[theResult fetchRowAsArray] objectAtIndex:0]]; + } + + if ([tableDocumentInstance table] != nil && ![(NSString *)[tableDocumentInstance table] isEqualToString:@""]) { + [fieldMappingPopup selectItemWithTitle:[(TableDocument *)tableDocumentInstance table]]; + } else { + [fieldMappingPopup selectItemAtIndex:0]; + } + + [tableListView selectRowIndexes:[NSIndexSet indexSetWithIndex:[[tablesListInstance tables] indexOfObject:[fieldMappingPopup titleOfSelectedItem]]] byExtendingSelection:NO]; + + //set up tableView + currentRow = 0; + fieldMappingArray = nil; + [self setupFieldMappingArray]; + [rowDownButton setEnabled:NO]; + [rowUpButton setEnabled:([importArray count] > 1)]; + [recordCountLabel setStringValue:[NSString stringWithFormat:@"%i of %i records", currentRow+1, [importArray count]]]; + + //set up tableView buttons + [buttonCell setControlSize:NSSmallControlSize]; + [buttonCell setFont:[NSFont labelFontOfSize:[NSFont smallSystemFontSize]]]; + [buttonCell setBordered:NO]; + [[fieldMappingTableView tableColumnWithIdentifier:@"value"] setDataCell:buttonCell]; + [self updateFieldMappingButtonCell]; + [fieldMappingTableView reloadData]; + + // show fieldMapping sheet + [NSApp beginSheet:fieldMappingSheet + modalForWindow:tableWindow + modalDelegate:self + didEndSelector:nil + contextInfo:nil]; + + code = [NSApp runModalForWindow:fieldMappingSheet]; + + [NSApp endSheet:fieldMappingSheet]; + [fieldMappingSheet orderOut:nil]; + + if ( code ) { + //import array into db + NSMutableString *fNames = [NSMutableString string]; + //NSMutableArray *fValuesIndexes = [NSMutableArray array]; + NSMutableString *fValues = [NSMutableString string]; + int i,j; + + //open progress sheet + [NSApp beginSheet:singleProgressSheet + modalForWindow:tableWindow + modalDelegate:self + didEndSelector:nil + contextInfo:nil]; + + // get fields to be imported + for (i = 0; i < [fieldMappingArray count] ; i++ ) { + if ([[fieldMappingArray objectAtIndex:i] intValue] > 0) { + if ( [fNames length] ) + [fNames appendString:@","]; + + [fNames appendString:[NSString stringWithFormat:@"`%@`", [[tableSourceInstance fieldNames] objectAtIndex:i]]]; + } + } + + //import array + for ( i = 0 ; i < [importArray count] ; i++ ) { + //show progress bar + [singleProgressBar setDoubleValue:((i+1)*100/[importArray count])]; + [singleProgressBar displayIfNeeded]; + + if ( !([importFieldNamesSwitch state] && (i == 0)) ) { + //put values in string + [fValues setString:@""]; + + for ( j = 0 ; j < [fieldMappingArray count] ; j++ ) { + + if ([[fieldMappingArray objectAtIndex:j] intValue] > 0) { + if ( [fValues length] ) + [fValues appendString:@","]; + + if ([[[importArray objectAtIndex:i] objectAtIndex:([[fieldMappingArray objectAtIndex:j] intValue] - 1)] isMemberOfClass:[NSNull class]] ) { + [fValues appendString:@"NULL"]; + } else { + [fValues appendString:[NSString stringWithFormat:@"'%@'",[mySQLConnection prepareString:[[importArray objectAtIndex:i] objectAtIndex:([[fieldMappingArray objectAtIndex:j] intValue] - 1)]]]]; + } + } + } + + //perform query + [mySQLConnection queryString:[NSString stringWithFormat:@"INSERT INTO `%@` (%@) VALUES (%@)", + [fieldMappingPopup titleOfSelectedItem], + fNames, + fValues]]; + + if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + [errors appendString:[NSString stringWithFormat:NSLocalizedString(@"[ERROR in line %d] %@\n", @"error text when reading of csv file gave errors"), (i+1),[mySQLConnection getLastErrorMessage]]]; + } + } + } + + //close progress sheet + [NSApp endSheet:singleProgressSheet]; + [singleProgressSheet orderOut:nil]; + } + + [tableContentInstance reloadTableValues:self]; + + //display errors + if ( [errors length] ) { + [errorsView setString:errors]; + [NSApp beginSheet:errorsSheet + modalForWindow:tableWindow + modalDelegate:self + didEndSelector:nil + contextInfo:nil]; + + [NSApp runModalForWindow:errorsSheet]; + + [NSApp endSheet:errorsSheet]; + [errorsSheet orderOut:nil]; + } + + //free arrays + fieldMappingArray = nil; + importArray = nil; + } + + // Import finished Growl notification + [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Import Finished" + description:[NSString stringWithFormat:NSLocalizedString(@"Finished importing %@",@"description for finished importing growl notification"), [[sheet filename] lastPathComponent]] + notificationName:@"Import Finished"]; +} + +- (void)setupFieldMappingArray +/* + sets up the fieldMapping array to be shown in the tableView + */ +{ + int i, value; + + if ( fieldMappingArray ) { + + // for ( i = 0 ; i < [fieldMappingArray count] ; i++ ) { + // + // if ( [[[importArray objectAtIndex:currentRow] objectAtIndex:i] isKindOfClass:[NSNull class]] ) { + // [fieldMappingArray replaceObjectAtIndex:i withObject:0]; + // + // } else { + // [fieldMappingArray replaceObjectAtIndex:i withObject:[[importArray objectAtIndex:currentRow] objectAtIndex:0]]; + // } + // } + + } else { + fieldMappingArray = [NSMutableArray array]; + + for (i = 0; i < [[tableSourceInstance fieldNames] count]; i++) { + if (i < [[importArray objectAtIndex:currentRow] count] && ![[[importArray objectAtIndex:currentRow] objectAtIndex:i] isKindOfClass:[NSNull class]]) { + value = i + 1; + } else { + value = 0; + } + + [fieldMappingArray addObject:[NSNumber numberWithInt:value]]; + } + + [fieldMappingArray retain]; + } + + [fieldMappingTableView reloadData]; +} + +/* + * Update the NSButtonCell items for use in the field mapping display + */ +- (void)updateFieldMappingButtonCell +{ + int i; + + [fieldMappingButtonOptions setArray:[importArray objectAtIndex:currentRow]]; + for (i = 0; i < [fieldMappingButtonOptions count]; i++) { + if ([[fieldMappingButtonOptions objectAtIndex:i] isNSNull]) { + [fieldMappingButtonOptions replaceObjectAtIndex:i withObject:[NSString stringWithFormat:@"%i. %@", i+1, [prefs objectForKey:@"nullValue"]]]; + } else { + [fieldMappingButtonOptions replaceObjectAtIndex:i withObject:[NSString stringWithFormat:@"%i. %@", i+1, [fieldMappingButtonOptions objectAtIndex:i]]]; + } + } +} + +- (IBAction)stepRow:(id)sender +/* + displays next/previous row in fieldMapping tableView + */ +{ + if ( [sender tag] == 0 ) { + currentRow--; + } else { + currentRow++; + } + [self updateFieldMappingButtonCell]; + + //-----------[self setupFieldMappingArray]; + [fieldMappingTableView reloadData]; + + [recordCountLabel setStringValue:[NSString stringWithFormat:@"%i of %i records", currentRow+1, [importArray count]]]; + + // enable/disable buttons + [rowDownButton setEnabled:(currentRow != 0)]; + [rowUpButton setEnabled:(currentRow != ([importArray count]-1))]; +} + +#pragma mark - +#pragma mark format methods + + +/* + Dump the selected tables to a file handle in SQL format. + */ +- (BOOL)dumpSelectedTablesAsSqlToFileHandle:(NSFileHandle *)fileHandle +{ + int i,j,t,rowCount, progressBarWidth, lastProgressValue, queryLength; + CMMCPResult *queryResult; + NSString *tableName; + NSArray *fieldNames; + NSArray *theRow; + NSMutableArray *selectedTables = [NSMutableArray array]; + NSMutableString *headerString = [NSMutableString string]; + NSMutableString *cellValue = [NSMutableString string]; + NSMutableString *sqlString = [NSMutableString string]; + NSMutableString *errors = [NSMutableString string]; + NSStringEncoding connectionEncoding = [mySQLConnection encoding]; + NSScanner *sqlNumericTester; + id createTableSyntax; + + // Reset the interface + [errorsView setString:@""]; + [errorsView displayIfNeeded]; + [singleProgressText setStringValue:NSLocalizedString(@"Dumping...", @"text showing that app is writing dump")]; + [singleProgressText displayIfNeeded]; + progressBarWidth = (int)[singleProgressBar bounds].size.width; + [singleProgressBar setDoubleValue:0]; + [singleProgressBar displayIfNeeded]; + + // Open the progress sheet + [NSApp beginSheet:singleProgressSheet + modalForWindow:tableWindow modalDelegate:self + didEndSelector:nil contextInfo:nil]; + + // Copy over the selected table names into a table in preparation for iteration + for ( i = 0 ; i < [tables count] ; i++ ) { + if ( [[[tables objectAtIndex:i] objectAtIndex:0] boolValue] ) { + [selectedTables addObject:[NSString stringWithString:[[tables objectAtIndex:i] objectAtIndex:1]]]; + } + } + + // Add the dump header to the dump file. + [headerString setString:@"# Sequel Pro dump\n"]; + [headerString appendString:[NSString stringWithFormat:@"# Version %@\n", + [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]]]; + [headerString appendString:@"# http://code.google.com/p/sequel-pro\n#\n"]; + [headerString appendString:[NSString stringWithFormat:@"# Host: %@ (MySQL %@)\n", + [tableDocumentInstance host], [tableDocumentInstance mySQLVersion]]]; + [headerString appendString:[NSString stringWithFormat:@"# Database: %@\n", [tableDocumentInstance database]]]; + [headerString appendString:[NSString stringWithFormat:@"# Generation Time: %@\n", [NSDate date]]]; + [headerString appendString:@"# ************************************************************\n\n"]; + [fileHandle writeData:[headerString dataUsingEncoding:connectionEncoding]]; + + // Loop through the selected tables + for ( i = 0 ; i < [selectedTables count] ; i++ ) { + lastProgressValue = 0; + + // Update the progress text and reset the progress bar to indeterminate status while fetching data + tableName = [selectedTables objectAtIndex:i]; + [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Table %i of %i (%@): Fetching data...", @"text showing that app is fetching data for table dump"), (i+1), [selectedTables count], tableName]]; + [singleProgressText displayIfNeeded]; + [singleProgressBar setIndeterminate:YES]; + [singleProgressBar setUsesThreadedAnimation:YES]; + [singleProgressBar startAnimation:self]; + + // Add the name of table + [fileHandle writeData:[[NSString stringWithFormat:@"# Dump of table %@\n# ------------------------------------------------------------\n\n", tableName] + dataUsingEncoding:connectionEncoding]]; + + + // Add a "drop table" command if specified in the export dialog + if ( [addDropTableSwitch state] == NSOnState ) + [fileHandle writeData:[[NSString stringWithFormat:@"DROP TABLE IF EXISTS `%@`;\n\n", tableName] + dataUsingEncoding:connectionEncoding]]; + + // Add the create syntax for the table if specified in the export dialog + if ( [addCreateTableSwitch state] == NSOnState ) { + queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW CREATE TABLE `%@`", tableName]]; + if ( [queryResult numOfRows] ) { + createTableSyntax = [[queryResult fetchRowAsDictionary] objectForKey:@"Create Table"]; + if ( [createTableSyntax isKindOfClass:[NSData class]] ) { + createTableSyntax = [[[NSString alloc] initWithData:createTableSyntax encoding:connectionEncoding] autorelease]; + } + [fileHandle writeData:[createTableSyntax dataUsingEncoding:connectionEncoding]]; + [fileHandle writeData:[[NSString stringWithString:@";\n\n"] dataUsingEncoding:connectionEncoding]]; + } + if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + [errors appendString:[NSString stringWithFormat:@"%@\n", [mySQLConnection getLastErrorMessage]]]; + if ( [addErrorsSwitch state] == NSOnState ) { + [fileHandle writeData:[[NSString stringWithFormat:@"# Error: %@\n", [mySQLConnection getLastErrorMessage]] dataUsingEncoding:connectionEncoding]]; + } + } + } + + // Add the table content if required + if ( [addTableContentSwitch state] == NSOnState ) { + queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SELECT * FROM `%@`", tableName]]; + fieldNames = [queryResult fetchFieldNames]; + rowCount = [queryResult numOfRows]; + + // Update the progress text and set the progress bar back to determinate + [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Table %i of %i (%@): Dumping...", @"text showing that app is writing data for table dump"), (i+1), [selectedTables count], tableName]]; + [singleProgressText displayIfNeeded]; + [singleProgressBar stopAnimation:self]; + [singleProgressBar setUsesThreadedAnimation:NO]; + [singleProgressBar setIndeterminate:NO]; + [singleProgressBar setDoubleValue:0]; + [singleProgressBar displayIfNeeded]; + + if (rowCount) { + [queryResult dataSeek:0]; + queryLength = 0; + + // Construct the start of the insertion command + [fileHandle writeData:[[NSString stringWithFormat:@"INSERT INTO `%@` (`%@`)\nVALUES\n\t(", + tableName, [fieldNames componentsJoinedByString:@"`,`"]] dataUsingEncoding:connectionEncoding]]; + + // Iterate through the rows to construct a VALUES group for each + for ( j = 0 ; j < rowCount ; j++ ) { + theRow = [queryResult fetchRowAsArray]; + [sqlString setString:@""]; + + // Update the progress bar + [singleProgressBar setDoubleValue:((j+1)*100/rowCount)]; + if ((int)[singleProgressBar doubleValue] > lastProgressValue) { + lastProgressValue = (int)[singleProgressBar doubleValue]; + [singleProgressBar displayIfNeeded]; + } + + for ( t = 0 ; t < [theRow count] ; t++ ) { + + // Add NULL values directly to the output row + if ( [[theRow objectAtIndex:t] isMemberOfClass:[NSNull class]] ) { + [sqlString appendString:@"NULL"]; + + // Add data types directly as hex data + } else if ( [[theRow objectAtIndex:t] isKindOfClass:[NSData class]] ) { + [sqlString appendString:@"X'"]; + [sqlString appendString:[mySQLConnection prepareBinaryData:[theRow objectAtIndex:t]]]; + [sqlString appendString:@"'"]; + + } else { + [cellValue setString:[[theRow objectAtIndex:t] description]]; + + // Add empty strings as a pair of quotes + if ([cellValue length] == 0) { + [sqlString appendString:@"''"]; + + } else { + + // Test whether this cell contains a number + sqlNumericTester = [NSScanner scannerWithString:cellValue]; + + // If it does contain a number, add the number directly + if ([sqlNumericTester scanFloat:nil] && [sqlNumericTester isAtEnd]) { + [sqlString appendString:cellValue]; + + // Otherwise add a quoted string with special characters escaped + } else { + [sqlString appendString:@"'"]; + [sqlString appendString:[mySQLConnection prepareString:cellValue]]; + [sqlString appendString:@"'"]; + } + } + } + + // Add the field separator if this isn't the last cell in the row + if (t != [theRow count] - 1) [sqlString appendString:@","]; + } + + queryLength += [sqlString length]; + + // Close this VALUES group and set up the next one if appropriate + if (j != rowCount - 1) { + + // Add a new INSERT starter command every ~250k of data. + if (queryLength > 250000) { + [sqlString appendString:[NSString stringWithFormat:@");\n\nINSERT INTO `%@` (`%@`)\nVALUES\n\t(", + tableName, [fieldNames componentsJoinedByString:@"`,`"]]]; + queryLength = 0; + } else { + [sqlString appendString:@"),\n\t("]; + } + } else { + [sqlString appendString:@")"]; + } + + // Write this row to the file + [fileHandle writeData:[sqlString dataUsingEncoding:connectionEncoding]]; + } + + // Complete the command + [fileHandle writeData:[[NSString stringWithString:@";\n\n"] dataUsingEncoding:connectionEncoding]]; + + if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + [errors appendString:[NSString stringWithFormat:@"%@\n", [mySQLConnection getLastErrorMessage]]]; + if ( [addErrorsSwitch state] == NSOnState ) { + [fileHandle writeData:[[NSString stringWithFormat:@"# Error: %@\n", [mySQLConnection getLastErrorMessage]] + dataUsingEncoding:connectionEncoding]]; + } + } + } + } + + // Add an additional separator between tables + [fileHandle writeData:[[NSString stringWithString:@"\n\n"] dataUsingEncoding:connectionEncoding]]; + } + + // Close the progress sheet + [NSApp endSheet:singleProgressSheet]; + [singleProgressSheet orderOut:nil]; + + // Show errors sheet if there have been errors + if ( [errors length] ) { + [errorsView setString:errors]; + [NSApp beginSheet:errorsSheet + modalForWindow:tableWindow modalDelegate:self + didEndSelector:nil contextInfo:nil]; + [NSApp runModalForWindow:errorsSheet]; + + [NSApp endSheet:errorsSheet]; + [errorsSheet orderOut:nil]; + } + + return TRUE; +} + +/* + Takes an array and writes it in CSV format to the supplied NSFileHandle + */ +- (BOOL)writeCsvForArray:(NSArray *)array orQueryResult:(CMMCPResult *)queryResult toFileHandle:(NSFileHandle *)fileHandle outputFieldNames:(BOOL)outputFieldNames terminatedBy:(NSString *)fieldSeparatorString + enclosedBy:(NSString *)enclosingString escapedBy:(NSString *)escapeString lineEnds:(NSString *)lineEndString silently:(BOOL)silently; +{ + NSStringEncoding tableEncoding = [CMMCPConnection encodingForMySQLEncoding:[[tableDocumentInstance encoding] cString]]; + NSMutableString *csvCell = [NSMutableString string]; + NSMutableArray *csvRow = [NSMutableArray array]; + NSMutableString *csvString = [NSMutableString string]; + NSString *nullString = [NSString stringWithString:[prefs objectForKey:@"nullValue"]]; + NSString *escapedEscapeString, *escapedFieldSeparatorString, *escapedEnclosingString, *escapedLineEndString; + NSString *dataConversionString; + NSScanner *csvNumericTester; + BOOL quoteFieldSeparators = [enclosingString isEqualToString:@""]; + BOOL csvCellIsNumeric; + int i, j, startingRow, totalRows, progressBarWidth, lastProgressValue; + + if (queryResult != nil && [queryResult numOfRows]) [queryResult dataSeek:0]; + + // Detect and restore special characters being used as terminating or line end strings + NSMutableString *tempSeparatorString = [NSMutableString stringWithString:fieldSeparatorString]; + [tempSeparatorString replaceOccurrencesOfString:@"\\t" withString:@"\t" + options:NSLiteralSearch + range:NSMakeRange(0, [tempSeparatorString length])]; + [tempSeparatorString replaceOccurrencesOfString:@"\\n" withString:@"\n" + options:NSLiteralSearch + range:NSMakeRange(0, [tempSeparatorString length])]; + [tempSeparatorString replaceOccurrencesOfString:@"\\r" withString:@"\r" + options:NSLiteralSearch + range:NSMakeRange(0, [tempSeparatorString length])]; + fieldSeparatorString = [NSString stringWithString:tempSeparatorString]; + NSMutableString *tempLineEndString = [NSMutableString stringWithString:lineEndString]; + [tempLineEndString replaceOccurrencesOfString:@"\\t" withString:@"\t" + options:NSLiteralSearch + range:NSMakeRange(0, [tempLineEndString length])]; + [tempLineEndString replaceOccurrencesOfString:@"\\n" withString:@"\n" + options:NSLiteralSearch + range:NSMakeRange(0, [tempLineEndString length])]; + [tempLineEndString replaceOccurrencesOfString:@"\\r" withString:@"\r" + options:NSLiteralSearch + range:NSMakeRange(0, [tempLineEndString length])]; + lineEndString = [NSString stringWithString:tempLineEndString]; + + // Updating the progress bar can take >20% of processing time - store details to only update when required + progressBarWidth = (int)[singleProgressBar bounds].size.width; + lastProgressValue = 0; + [singleProgressBar setDoubleValue:0]; + [singleProgressBar displayIfNeeded]; + + if ( !silently ) { + + // Set the progress text + [singleProgressText setStringValue:NSLocalizedString(@"Exporting...", @"text showing that app is exporting to text file")]; + [singleProgressText displayIfNeeded]; + + + // Open progress sheet + [NSApp beginSheet:singleProgressSheet + modalForWindow:tableWindow modalDelegate:self + didEndSelector:nil contextInfo:nil]; + } + + // Set up escaped versions of strings for substitution within the loop + escapedEscapeString = [NSString stringWithFormat:@"%@%@", escapeString, escapeString]; + escapedFieldSeparatorString = [NSString stringWithFormat:@"%@%@", escapeString, fieldSeparatorString]; + escapedEnclosingString = [NSString stringWithFormat:@"%@%@", escapeString, enclosingString]; + escapedLineEndString = [NSString stringWithFormat:@"%@%@", escapeString, lineEndString]; + + // Determine the total number of rows and starting row depending on supplied data format + if (array == nil) { + startingRow = outputFieldNames ? -1 : 0; + totalRows = [queryResult numOfRows]; + } else { + startingRow = outputFieldNames ? 0 : 1; + totalRows = [array count]; + } + + // Walk through the supplied data constructing the CSV string + for ( i = startingRow ; i < totalRows ; i++ ) { + + // Update the progress bar + [singleProgressBar setDoubleValue:((i+1)*100/totalRows)]; + if ((int)[singleProgressBar doubleValue] > lastProgressValue) { + lastProgressValue = (int)[singleProgressBar doubleValue]; + [singleProgressBar displayIfNeeded]; + } + + // Retrieve the row from the supplied data + if (array == nil) { + + // Header row + if (i == -1) { + [csvRow setArray:[queryResult fetchFieldNames]]; + } else { + [csvRow setArray:[queryResult fetchRowAsArray]]; + } + } else { + [csvRow setArray:[array objectAtIndex:i]]; + } + + [csvString setString:@""]; + for ( j = 0 ; j < [csvRow count] ; j++ ) { + + // For NULL objects supplied from a queryResult, add an unenclosed null string as per prefs + if ([[csvRow objectAtIndex:j] isKindOfClass:[NSNull class]]) { + [csvString appendString:nullString]; + if (j < [csvRow count] - 1) [csvString appendString:fieldSeparatorString]; + continue; + } + + // Retrieve the contents of this cell + if ([[csvRow objectAtIndex:j] isKindOfClass:[NSData class]]) { + dataConversionString = [[NSString alloc] initWithData:[csvRow objectAtIndex:j] encoding:tableEncoding]; + [csvCell setString:[NSString stringWithString:dataConversionString]]; + [dataConversionString release]; + } else { + [csvCell setString:[[csvRow objectAtIndex:j] description]]; + } + + // For NULL values supplied via an array add the unenclosed null string as set in preferences + if ( [csvCell isEqualToString:nullString] ) { + [csvString appendString:nullString]; + + // Add empty strings as a pair of enclosing characters. + } else if ( [csvCell length] == 0 ) { + [csvString appendString:enclosingString]; + [csvString appendString:enclosingString]; + + } else { + + // Test whether this cell contains a number + if ([[csvRow objectAtIndex:j] isKindOfClass:[NSData class]]) { + csvCellIsNumeric = FALSE; + } else { + csvNumericTester = [NSScanner scannerWithString:csvCell]; + csvCellIsNumeric = [csvNumericTester scanFloat:nil] && [csvNumericTester isAtEnd]; + } + + // Escape any occurrences of the escaping character + [csvCell replaceOccurrencesOfString:escapeString + withString:escapedEscapeString + options:NSLiteralSearch + range:NSMakeRange(0,[csvCell length])]; + + // Escape any occurrences of the enclosure string + if ( ![escapeString isEqualToString:enclosingString] ) { + [csvCell replaceOccurrencesOfString:enclosingString + withString:escapedEnclosingString + options:NSLiteralSearch + range:NSMakeRange(0,[csvCell length])]; + } + + // Escape occurrences of the line end character + [csvCell replaceOccurrencesOfString:lineEndString + withString:escapedLineEndString + options:NSLiteralSearch + range:NSMakeRange(0,[csvCell length])]; + + // If the string isn't quoted or otherwise enclosed, escape occurrences of the + // field separators + if ( quoteFieldSeparators || csvCellIsNumeric ) { + [csvCell replaceOccurrencesOfString:fieldSeparatorString + withString:escapedFieldSeparatorString + options:NSLiteralSearch + range:NSMakeRange(0,[csvCell length])]; + } + + // Write out the cell data by appending strings - this is significantly faster than stringWithFormat. + if (csvCellIsNumeric) { + [csvString appendString:csvCell]; + } else { + [csvString appendString:enclosingString]; + [csvString appendString:csvCell]; + [csvString appendString:enclosingString]; + } + } + if (j < [csvRow count] - 1) [csvString appendString:fieldSeparatorString]; + } + + // Append the line ending to the string for this row + [csvString appendString:lineEndString]; + + // Write it to the fileHandle + [fileHandle writeData:[csvString dataUsingEncoding:tableEncoding]]; + } + + // Close the progress sheet if it's present + if ( !silently ) { + [NSApp endSheet:singleProgressSheet]; + [singleProgressSheet orderOut:nil]; + } + + return TRUE; +} + +- (NSArray *)arrayForCSV:(NSString *)csv terminatedBy:(NSString *)terminated + enclosedBy:(NSString *)enclosed escapedBy:(NSString *)escaped lineEnds:(NSString *)lineEnds +/* + loads a csv string into an array + */ +{ + NSMutableString *tempTerminated, *tempLineEnds; + NSMutableArray *tempArray = [NSMutableArray array]; + NSMutableArray *tempRowArray = [NSMutableArray array]; + NSMutableString *mutableField; + NSScanner *scanner; + NSString *scanString; + NSMutableString *tempString = [NSMutableString string]; + NSMutableArray *linesArray = [NSMutableArray array]; + BOOL isEscaped, br; + int fieldCount = nil; + int x,i,j; + + //repare tabs and line ends + tempTerminated = [NSMutableString stringWithString:terminated]; + [tempTerminated replaceOccurrencesOfString:@"\\t" withString:@"\t" + options:NSLiteralSearch + range:NSMakeRange(0, [tempTerminated length])]; + [tempTerminated replaceOccurrencesOfString:@"\\n" withString:@"\n" + options:NSLiteralSearch + range:NSMakeRange(0, [tempTerminated length])]; + [tempTerminated replaceOccurrencesOfString:@"\\r" withString:@"\r" + options:NSLiteralSearch + range:NSMakeRange(0, [tempTerminated length])]; + terminated = [NSString stringWithString:tempTerminated]; + tempLineEnds = [NSMutableString stringWithString:lineEnds]; + [tempLineEnds replaceOccurrencesOfString:@"\\t" withString:@"\t" + options:NSLiteralSearch + range:NSMakeRange(0, [tempLineEnds length])]; + [tempLineEnds replaceOccurrencesOfString:@"\\n" withString:@"\n" + options:NSLiteralSearch + range:NSMakeRange(0, [tempLineEnds length])]; + [tempLineEnds replaceOccurrencesOfString:@"\\r" withString:@"\r" + options:NSLiteralSearch + range:NSMakeRange(0, [tempLineEnds length])]; + lineEnds = [NSString stringWithString:tempLineEnds]; + + //array with one line per object + scanner = [NSScanner scannerWithString:csv]; + [scanner setCharactersToBeSkipped:nil]; + + while ( ![scanner isAtEnd] ) { + [tempString setString:@""]; + br = NO; + + while ( !br ) { + scanString = @""; + [scanner scanUpToString:lineEnds intoString:&scanString]; + [tempString appendString:scanString]; + [scanner scanString:lineEnds intoString:&scanString]; + + //test if lineEnds-character is escaped + isEscaped = NO; + j = 1; + if ( ![escaped isEqualToString:enclosed] && ![escaped isEqualToString:@""] ) { + while ( ((j*[escaped length])<=[tempString length]) && + ([[tempString substringWithRange:NSMakeRange(([tempString length]-(j*[escaped length])),[escaped length])] isEqualToString:escaped]) ) { + isEscaped = !isEscaped; + j++; + } + } + if ( !isEscaped || [scanner isAtEnd] ) { + //end of row + br = YES; + } else { + //lineEnds-character was escaped + [tempString appendString:scanString]; + } + } + + // Skip blank lines + if (![tempString length]) continue; + + // Add the line to the array + [linesArray addObject:[NSString stringWithString:tempString]]; + } + + for ( x = 0 ; x < [linesArray count] ; x++ ) { + + //separate fields + [tempRowArray removeAllObjects]; + [tempRowArray addObjectsFromArray:[self arrayForString:[linesArray objectAtIndex:x] enclosed:enclosed escaped:escaped terminated:terminated]]; + if ( x == 0 ) { + fieldCount = [tempRowArray count]; + } else { + while ( [tempRowArray count] < fieldCount ) { + [tempRowArray addObject:[NSString stringWithString:[prefs objectForKey:@"nullValue"]]]; + } + } + for ( i = 0 ; i < [tempRowArray count] ; i++ ) { + + // Insert a NSNull object if the cell contains an unescaped null character or an unescaped string + // which matches the NULL string set in preferences. + if ( [[tempRowArray objectAtIndex:i] isEqualToString:@"\\N"] || [[tempRowArray objectAtIndex:i] isEqualToString:[prefs objectForKey:@"nullValue"]] ) { + [tempRowArray replaceObjectAtIndex:i withObject:[NSNull null]]; + + } else { + + //strip enclosed and escaped characters + mutableField = [NSMutableString stringWithString:[tempRowArray objectAtIndex:i]]; + + //strip enclosed characters + if ( [mutableField length] >= (2*[enclosed length]) ) { + if ( [[mutableField substringToIndex:[enclosed length]] isEqualToString:enclosed] ) { + [mutableField deleteCharactersInRange:NSMakeRange(0,[enclosed length])]; + } + if ( [[mutableField substringFromIndex:([mutableField length]-[enclosed length])] isEqualToString:enclosed] ) { + [mutableField deleteCharactersInRange:NSMakeRange(([mutableField length]-[enclosed length]),[enclosed length])]; + } + } + if ( [mutableField length] >= [enclosed length] ) { + if ( [[mutableField substringFromIndex:([mutableField length]-[enclosed length])] isEqualToString:enclosed] ) { + [mutableField deleteCharactersInRange:NSMakeRange(([mutableField length]-[enclosed length]),[enclosed length])]; + } + } + //strip escaped characters + if ( ![enclosed isEqualToString:@""] ) { + [mutableField replaceOccurrencesOfString:[NSString stringWithFormat:@"%@%@", escaped, enclosed] withString:enclosed options:NSLiteralSearch range:NSMakeRange(0, [mutableField length])]; + } else { + [mutableField replaceOccurrencesOfString:[NSString stringWithFormat:@"%@%@", escaped, terminated] withString:terminated options:NSLiteralSearch range:NSMakeRange(0, [mutableField length])]; + } + if ( ![lineEnds isEqualToString:@""] ) { + [mutableField replaceOccurrencesOfString:[NSString stringWithFormat:@"%@%@", escaped, lineEnds] withString:lineEnds options:NSLiteralSearch range:NSMakeRange(0, [mutableField length])]; + } + if ( ![escaped isEqualToString:@""] && ![escaped isEqualToString:enclosed] ) { + [mutableField replaceOccurrencesOfString:[NSString stringWithFormat:@"%@%@", escaped, escaped] withString:escaped options:NSLiteralSearch range:NSMakeRange(0, [mutableField length])]; + } + //add field to tempRowArray + [tempRowArray replaceObjectAtIndex:i withObject:[NSString stringWithString:mutableField]]; + } + } + //add row to tempArray + [tempArray addObject:[NSArray arrayWithArray:tempRowArray]]; + } + + return [NSArray arrayWithArray:tempArray]; +} + + +/* + Takes an array and writes it in XML format to the supplied NSFileHandle + */ +- (BOOL)writeXmlForArray:(NSArray *)array orQueryResult:(CMMCPResult *)queryResult toFileHandle:(NSFileHandle *)fileHandle tableName:(NSString *)table withHeader:(BOOL)header silently:(BOOL)silently +{ + NSStringEncoding tableEncoding = [CMMCPConnection encodingForMySQLEncoding:[[tableDocumentInstance encoding] cString]]; + NSMutableArray *xmlTags = [NSMutableArray array]; + NSMutableArray *xmlRow = [NSMutableArray array]; + NSMutableString *xmlString = [NSMutableString string]; + NSMutableString *xmlItem = [NSMutableString string]; + NSString *dataConversionString; + int i,j, startingRow, totalRows, progressBarWidth, lastProgressValue; + + if (queryResult != nil && [queryResult numOfRows]) [queryResult dataSeek:0]; + + // Updating the progress bar can take >20% of processing time - store details to only update when required + progressBarWidth = (int)[singleProgressBar bounds].size.width; + lastProgressValue = 0; + [singleProgressBar setDoubleValue:0]; + [singleProgressBar displayIfNeeded]; + + // Set up an array of encoded field names as opening and closing tags + if (array == nil) { + [xmlRow setArray:[queryResult fetchFieldNames]]; + } else { + [xmlRow setArray:[array objectAtIndex:0]]; + } + for ( j = 0; j < [xmlRow count]; j++ ) { + [xmlTags addObject:[NSMutableArray array]]; + [[xmlTags objectAtIndex:j] addObject:[NSString stringWithFormat:@"\t\t<%@>", + [self htmlEscapeString:[[xmlRow objectAtIndex:j] description]]]]; + [[xmlTags objectAtIndex:j] addObject:[NSString stringWithFormat:@"</%@>\n", + [self htmlEscapeString:[[xmlRow objectAtIndex:j] description]]]]; + } + + if ( !silently ) { + + // Set the progress text + [singleProgressText setStringValue:NSLocalizedString(@"Writing...", @"text showing that app is writing text file")]; + [singleProgressText displayIfNeeded]; + + // Open progress sheet + [NSApp beginSheet:singleProgressSheet + modalForWindow:tableWindow modalDelegate:self + didEndSelector:nil contextInfo:nil]; + } + + // Output the XML header if required + if ( header ) { + [xmlString setString:@"<?xml version=\"1.0\"?>\n\n"]; + [xmlString appendString:@"<!--\n-\n"]; + [xmlString appendString:@"- Sequel Pro dump\n"]; + [xmlString appendString:[NSString stringWithFormat:@"- Version %@\n", + [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]]]; + [xmlString appendString:@"- http://code.google.com/p/sequel-pro\n-\n"]; + [xmlString appendString:[NSString stringWithFormat:@"- Host: %@ (MySQL %@)\n", + [tableDocumentInstance host], [tableDocumentInstance mySQLVersion]]]; + [xmlString appendString:[NSString stringWithFormat:@"- Database: %@\n", [tableDocumentInstance database]]]; + [xmlString appendString:[NSString stringWithFormat:@"- Generation Time: %@\n", [NSDate date]]]; + [xmlString appendString:@"-\n-->\n\n"]; + [fileHandle writeData:[xmlString dataUsingEncoding:tableEncoding]]; + } + + // Write an opening tag in the form of the table name + [fileHandle writeData:[[NSString stringWithFormat:@"\t<%@>\n", + [self htmlEscapeString:table]] + dataUsingEncoding:tableEncoding]]; + + // Determine the total number of rows and starting row depending on supplied data format + if (array == nil) { + startingRow = 0; + totalRows = [queryResult numOfRows]; + } else { + startingRow = 1; + totalRows = [array count]; + } + + // Walk through the array, contructing the XML string. + for ( i = 1 ; i < totalRows ; i++ ) { + + // Update the progress bar + [singleProgressBar setDoubleValue:((i+1)*100/totalRows)]; + if ((int)[singleProgressBar doubleValue] > lastProgressValue) { + lastProgressValue = (int)[singleProgressBar doubleValue]; + [singleProgressBar displayIfNeeded]; + } + + // Retrieve the row from the supplied data + if (array == nil) { + [xmlRow setArray:[queryResult fetchRowAsArray]]; + } else { + [xmlRow setArray:[array objectAtIndex:i]]; + } + + // Construct the row + [xmlString setString:@"\t<row>\n"]; + for ( j = 0 ; j < [xmlRow count] ; j++ ) { + + // Retrieve the contents of this tag + if ([[xmlRow objectAtIndex:j] isKindOfClass:[NSData class]]) { + dataConversionString = [[NSString alloc] initWithData:[xmlRow objectAtIndex:j] encoding:tableEncoding]; + [xmlItem setString:[NSString stringWithString:dataConversionString]]; + [dataConversionString release]; + } else { + [xmlItem setString:[[xmlRow objectAtIndex:j] description]]; + } + + // Add the opening and closing tag and the contents to the XML string + [xmlString appendString:[[xmlTags objectAtIndex:j] objectAtIndex:0]]; + [xmlString appendString:[self htmlEscapeString:xmlItem]]; + [xmlString appendString:[[xmlTags objectAtIndex:j] objectAtIndex:1]]; + } + [xmlString appendString:@"\t</row>\n"]; + + // Write the row to the filehandle + [fileHandle writeData:[xmlString dataUsingEncoding:tableEncoding]]; + } + + // Write the closing tag for the table + [fileHandle writeData:[[NSString stringWithFormat:@"\t</%@>", + [self htmlEscapeString:table]] + dataUsingEncoding:tableEncoding]]; + + // Close the progress sheet if appropriate + if ( !silently ) { + [NSApp endSheet:singleProgressSheet]; + [singleProgressSheet orderOut:nil]; + } + + return TRUE; +} + +/* + Processes the selected tables within the multiple table export accessory view and passes them + to be exported. + */ +- (BOOL)exportSelectedTablesToFileHandle:(NSFileHandle *)fileHandle usingFormat:(NSString *)type +{ + int i; + NSMutableArray *selectedTables = [NSMutableArray array]; + + // Extract the table names of the selected tables + for ( i = 0 ; i < [tables count] ; i++ ) { + if ( [[[tables objectAtIndex:i] objectAtIndex:0] boolValue] ) { + [selectedTables addObject:[NSString stringWithString:[[tables objectAtIndex:i] objectAtIndex:1]]]; + } + } + + return [self exportTables:selectedTables toFileHandle:fileHandle usingFormat:type]; +} + +/* + Walks through the selected tables and exports them to a file handle. The export type must be + "csv" for CSV format, and "xml" for XML format. + */ +- (BOOL)exportTables:(NSArray *)selectedTables toFileHandle:(NSFileHandle *)fileHandle usingFormat:(NSString *)type +{ + int i; + CMMCPResult *queryResult; + NSString *tableName; + NSMutableString *infoString = [NSMutableString string]; + NSMutableString *errors = [NSMutableString string]; + NSStringEncoding connectionEncoding = [mySQLConnection encoding]; + NSMutableString *csvLineEnd; + + // Reset the interface + [errorsView setString:@""]; + [errorsView displayIfNeeded]; + [singleProgressText setStringValue:NSLocalizedString(@"Writing...", @"text showing that app is writing text file")]; + [singleProgressText displayIfNeeded]; + [singleProgressBar setDoubleValue:0]; + [singleProgressBar displayIfNeeded]; + + // Open the progress sheet + [NSApp beginSheet:singleProgressSheet + modalForWindow:tableWindow modalDelegate:self + didEndSelector:nil contextInfo:nil]; + + + // Add a dump header to the dump file, dependant on export type. + if ( [type isEqualToString:@"csv"] ) { + csvLineEnd = [NSMutableString stringWithString:[exportMultipleLinesTerminatedField stringValue]]; + [csvLineEnd replaceOccurrencesOfString:@"\\t" withString:@"\t" + options:NSLiteralSearch + range:NSMakeRange(0, [csvLineEnd length])]; + [csvLineEnd replaceOccurrencesOfString:@"\\n" withString:@"\n" + options:NSLiteralSearch + range:NSMakeRange(0, [csvLineEnd length])]; + [csvLineEnd replaceOccurrencesOfString:@"\\r" withString:@"\r" + options:NSLiteralSearch + range:NSMakeRange(0, [csvLineEnd length])]; + if ([selectedTables count] > 1) { + [infoString setString:[NSString stringWithFormat:@"Host: %@ Database: %@ Generation Time: %@%@%@", + [tableDocumentInstance host], [tableDocumentInstance database], [NSDate date], csvLineEnd, csvLineEnd]]; + } + } else if ( [type isEqualToString:@"xml"] ) { + [infoString setString:@"<?xml version=\"1.0\"?>\n\n"]; + [infoString appendString:@"<!--\n-\n"]; + [infoString appendString:@"- Sequel Pro dump\n"]; + [infoString appendString:[NSString stringWithFormat:@"- Version %@\n", + [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]]]; + [infoString appendString:@"- http://code.google.com/p/sequel-pro\n-\n"]; + [infoString appendString:[NSString stringWithFormat:@"- Host: %@ (MySQL %@)\n", + [tableDocumentInstance host], [tableDocumentInstance mySQLVersion]]]; + [infoString appendString:[NSString stringWithFormat:@"- Database: %@\n", [tableDocumentInstance database]]]; + [infoString appendString:[NSString stringWithFormat:@"- Generation Time: %@\n", [NSDate date]]]; + [infoString appendString:@"-\n-->\n\n\n"]; + [infoString appendString:[NSString stringWithFormat:@"<%@>\n\n\n", + [self htmlEscapeString:[tableDocumentInstance database]]]]; + } + [fileHandle writeData:[infoString dataUsingEncoding:connectionEncoding]]; + + // Loop through the selected tables + for ( i = 0 ; i < [selectedTables count] ; i++ ) { + + // Update the progress text and reset the progress bar to indeterminate status + tableName = [selectedTables objectAtIndex:i]; + [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Table %i of %i (%@): fetching data...", @"text showing that app is fetching data for table dump"), (i+1), [selectedTables count], tableName]]; + [singleProgressText displayIfNeeded]; + [singleProgressBar setIndeterminate:YES]; + [singleProgressBar setUsesThreadedAnimation:YES]; + [singleProgressBar startAnimation:self]; + + // For CSV exports of more than one table, output the name of the table + if ( [type isEqualToString:@"csv"] && [selectedTables count] > 1) { + [fileHandle writeData:[[NSString stringWithFormat:@"Table %@%@%@", tableName, csvLineEnd, csvLineEnd] dataUsingEncoding:connectionEncoding]]; + } + + // Retrieve all the content within this table + queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SELECT * FROM `%@`", tableName]]; + + // Note any errors during retrieval + if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + [errors appendString:[NSString stringWithFormat:@"%@\n", [mySQLConnection getLastErrorMessage]]]; + } + + // Update the progress text and set the progress bar back to determinate + [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Table %i of %i (%@): Writing...", @"text showing that app is writing data for table export"), (i+1), [selectedTables count], tableName]]; + [singleProgressText displayIfNeeded]; + [singleProgressBar stopAnimation:self]; + [singleProgressBar setUsesThreadedAnimation:NO]; + [singleProgressBar setIndeterminate:NO]; + [singleProgressBar setDoubleValue:0]; + [singleProgressBar displayIfNeeded]; + + // Use the appropriate export method to write the data to file + if ( [type isEqualToString:@"csv"] ) { + [self writeCsvForArray:nil orQueryResult:queryResult + toFileHandle:fileHandle + outputFieldNames:[exportMultipleFieldNamesSwitch state] + terminatedBy:[exportMultipleFieldsTerminatedField stringValue] + enclosedBy:[exportMultipleFieldsEnclosedField stringValue] + escapedBy:[exportMultipleFieldsEscapedField stringValue] + lineEnds:[exportMultipleLinesTerminatedField stringValue] + silently:YES]; + + // Add a spacer to the file + [fileHandle writeData:[[NSString stringWithFormat:@"%@%@%@", csvLineEnd, csvLineEnd, csvLineEnd] dataUsingEncoding:connectionEncoding]]; + } else if ( [type isEqualToString:@"xml"] ) { + [self writeXmlForArray:nil orQueryResult:queryResult + toFileHandle:fileHandle + tableName:tableName + withHeader:NO + silently:YES]; + + // Add a spacer to the file + [fileHandle writeData:[[NSString stringWithString:@"\n\n\n"] dataUsingEncoding:connectionEncoding]]; + } + + } + + // For XML output, close the database tag + if ( [type isEqualToString:@"xml"] ) { + [fileHandle writeData:[[NSString stringWithFormat:@"</%@>", + [self htmlEscapeString:[tableDocumentInstance database]]] + dataUsingEncoding:connectionEncoding]]; + } + + // Close the progress sheet + [NSApp endSheet:singleProgressSheet]; + [singleProgressSheet orderOut:nil]; + + // Show the errors sheet if there have been errors + if ( [errors length] ) { + [errorsView setString:errors]; + [NSApp beginSheet:errorsSheet + modalForWindow:tableWindow modalDelegate:self + didEndSelector:nil contextInfo:nil]; + [NSApp runModalForWindow:errorsSheet]; + + [NSApp endSheet:errorsSheet]; + [errorsSheet orderOut:nil]; + } + + return TRUE; +} + +- (NSString *)htmlEscapeString:(NSString *)string +/* + html escapes a string + */ +{ + NSMutableString *mutableString = [NSMutableString stringWithString:string]; + + [mutableString replaceOccurrencesOfString:@"&" withString:@"&" + options:NSLiteralSearch + range:NSMakeRange(0, [mutableString length])]; + [mutableString replaceOccurrencesOfString:@"<" withString:@"<" + options:NSLiteralSearch + range:NSMakeRange(0, [mutableString length])]; + [mutableString replaceOccurrencesOfString:@">" withString:@">" + options:NSLiteralSearch + range:NSMakeRange(0, [mutableString length])]; + [mutableString replaceOccurrencesOfString:@"\"" withString:@""" + options:NSLiteralSearch + range:NSMakeRange(0, [mutableString length])]; + + return [NSString stringWithString:mutableString]; +} + +- (NSArray *)arrayForString:(NSString *)string enclosed:(NSString *)enclosed + escaped:(NSString *)escaped terminated:(NSString *)terminated +/* + split a string by the terminated-character if this is not escaped + if enclosed-character is given, ignores characters inside enclosed-characters + */ +{ + NSMutableArray *tempArray = [NSMutableArray array]; + BOOL inString = NO; + BOOL isEscaped = NO; + BOOL br = NO; + unsigned i, j, start; + char enc = nil; + char esc = nil; + char ter = nil; + + //we take only first character by now (too complicated otherwise) + if ( [enclosed length] ) { + enc = [enclosed characterAtIndex:0]; + } + if ( [escaped length] ) { + esc = [escaped characterAtIndex:0]; + } + if ( [terminated length] ) { + ter = [terminated characterAtIndex:0]; + } + + start = 0; + + for ( i = 0 ; i < [string length] ; i++ ) { + if ( inString ) { + //we are in a string + br = NO; + while ( !br ) { + if ( i >= [string length] ) { + //end of string -> no second enclose character found + br = YES; + } else if ( [string characterAtIndex:i] == enc ) { + //second enclose-character found + //enclose-character escaped? + isEscaped = NO; + j = 1; + while ( (i-j>0) && ([string characterAtIndex:(i-j)] == esc) ) { + isEscaped = !isEscaped; + j++; + } + if ( !isEscaped ) { + inString = NO; + br = YES; + } + } + if ( !br ) + i++; + } + } else if ( [string characterAtIndex:i] == ter ) { + //terminated-character found + if ( [enclosed isEqualToString:@""] ) { + //check if terminated character is escaped + isEscaped = NO; + j = 1; + while ( (i-j>0) && ([string characterAtIndex:(i-j)] == esc) ) { + isEscaped = !isEscaped; + j++; + } + if ( !isEscaped ) { + [tempArray addObject:[string substringWithRange:NSMakeRange(start,(i-start))]]; + start = i + 1; + } + } else { + //add object to array + //NSLog([string substringWithRange:NSMakeRange(start,(i-start))]); + [tempArray addObject:[string substringWithRange:NSMakeRange(start,(i-start))]]; + start = i + 1; + } + } else if ( [string characterAtIndex:i] == enc ) { + //enclosed-character found + inString = YES; + } + } + + //add rest of string to array + [tempArray addObject:[string substringWithRange:NSMakeRange(start,([string length]-start))]]; + + return [NSArray arrayWithArray:tempArray]; +} + +- (NSArray *)splitQueries:(NSString *)query +/* + splits the queries by ;'s which aren't inside any ", ' or ` characters + */ +{ + NSMutableString *queries = [NSMutableString stringWithString:query]; + NSMutableArray *queryArray = [NSMutableArray array]; + char stringType = nil; + BOOL inString = NO; + BOOL escaped; + unsigned lineStart = 0; + unsigned i, j, x, currentLineLength; + + //parse string + for ( i = 0 ; i < [queries length] ; i++ ) { + if ( inString ) { + //we are in a string + //look for end of string + for ( ; i < [queries length] ; i++ ) { + + // For the backtick character treat the string as ended + if ( ([queries characterAtIndex:i] == '`') && (stringType == '`') ) { + + inString = NO; + break; + + // Otherwise, prepare to treat the string as ended after a stringType.... + } else if ( [queries characterAtIndex:i] == stringType ) { + + // ...but only if the stringType isn't escaped with an *odd* number of escaping characters. + escaped = NO; + j = 1; + currentLineLength = i - lineStart; + while ( ((currentLineLength-j) > 0) && ([queries characterAtIndex:i-j] == '\\') ) { + escaped = !escaped; + j++; + } + + // If an odd number have been found, it really is the end of the string. + if ( !escaped ) { + inString = NO; + break; + } + } + } + } else if ( ([queries characterAtIndex:i] == '#') || + ((i+2<[queries length]) && + ([queries characterAtIndex:i] == '-') && + ([queries characterAtIndex:i+1] == '-') && + ([queries characterAtIndex:i+2] == ' ')) ) { + //it's a comment -> delete it + x = i; + while ( (x<[queries length]) && ([queries characterAtIndex:x] != '\r') && ([queries characterAtIndex:x] != '\n') ) { + x++; + } + [queries deleteCharactersInRange:NSMakeRange(i,x-i)]; + } else if ( [queries characterAtIndex:i] == ';' ) { + //we are at the end of a query + [queryArray addObject:[queries substringWithRange:NSMakeRange(lineStart, (i-lineStart))]]; + while ( ((i+1)<[queries length]) && (([queries characterAtIndex:i+1]=='\n') || ([queries characterAtIndex:i+1]=='\r') || ([queries characterAtIndex:i+1]==' ')) ) { + i++; + } + lineStart = i + 1; + } else if ( ([queries characterAtIndex:i] == '\'') || + ([queries characterAtIndex:i] == '"') || + ([queries characterAtIndex:i] == '`') ) { + //we are entering a string + inString = YES; + stringType = [queries characterAtIndex:i]; + } + } + + //add rest of string to array (if last line has not ended with a ";") + if ( lineStart < [queries length] ) { + [queryArray addObject:[queries substringWithRange:NSMakeRange(lineStart, ([queries length]-lineStart))]]; + } + + //return array + return [NSArray arrayWithArray:queryArray]; +} + + +//additional methods +- (void)setConnection:(CMMCPConnection *)theConnection +/* + sets the connection (received from TableDocument) and makes things that have to be done only once + */ +{ + NSButtonCell *switchButton = [[NSButtonCell alloc] init]; + + prefs = [[NSUserDefaults standardUserDefaults] retain]; + + mySQLConnection = theConnection; + + //set up the interface + [switchButton setButtonType:NSSwitchButton]; + [switchButton setControlSize:NSSmallControlSize]; + [[exportDumpTableView tableColumnWithIdentifier:@"switch"] setDataCell:switchButton]; + [[exportMultipleCSVTableView tableColumnWithIdentifier:@"switch"] setDataCell:switchButton]; + [[exportMultipleXMLTableView tableColumnWithIdentifier:@"switch"] setDataCell:switchButton]; + if ( [prefs boolForKey:@"useMonospacedFonts"] ) { + [[[exportDumpTableView tableColumnWithIdentifier:@"tables"] dataCell] + setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]]; + [[[exportMultipleCSVTableView tableColumnWithIdentifier:@"tables"] dataCell] + setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]]; + [[[exportMultipleXMLTableView tableColumnWithIdentifier:@"tables"] dataCell] + setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]]; + [[[fieldMappingTableView tableColumnWithIdentifier:@"0"] dataCell] + setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]]; + [errorsView setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]]; + } else { + [[[exportDumpTableView tableColumnWithIdentifier:@"tables"] dataCell] + setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + [[[exportMultipleCSVTableView tableColumnWithIdentifier:@"tables"] dataCell] + setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + [[[exportMultipleXMLTableView tableColumnWithIdentifier:@"tables"] dataCell] + setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + [[[fieldMappingTableView tableColumnWithIdentifier:@"0"] dataCell] + setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + [errorsView setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + } + // [self reloadTables:self]; +} + +#pragma mark - +#pragma mark tableView datasource methods + +- (int)numberOfRowsInTableView:(NSTableView *)aTableView; +{ + if ( aTableView == fieldMappingTableView ) { + return [[tableSourceInstance fieldNames] count]; + } else { + return [tables count]; + } +} + +- (void)tableView:(NSTableView *)aTableView + willDisplayCell:(id)aCell + forTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex +{ + if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"useMonospacedFonts"] ) { + [aCell setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]]; + } + else + { + [aCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + } +} + +- (id)tableView:(NSTableView *)aTableView +objectValueForTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex +{ + if ( aTableView == fieldMappingTableView ) { + if ([[aTableColumn identifier] isEqualToString:@"field"]) { + return [[tableSourceInstance fieldNames] objectAtIndex:rowIndex]; + + } else if ([[aTableColumn identifier] isEqualToString:@"value"]) { + if ([[[aTableColumn dataCell] class] isEqualTo:[NSPopUpButtonCell class]]) { + [(NSPopUpButtonCell *)[aTableColumn dataCell] removeAllItems]; + [(NSPopUpButtonCell *)[aTableColumn dataCell] addItemWithTitle:NSLocalizedString(@"Do not import", @"text for csv import drop downs")]; + [(NSPopUpButtonCell *)[aTableColumn dataCell] addItemsWithTitles:fieldMappingButtonOptions]; + } + + return [fieldMappingArray objectAtIndex:rowIndex]; + } + } else { + if ( [[aTableColumn identifier] isEqualToString:@"switch"] ) { + return [[tables objectAtIndex:rowIndex] objectAtIndex:0]; + } else { + return [[tables objectAtIndex:rowIndex] objectAtIndex:1]; + } + } +} + +- (void)tableView:(NSTableView *)aTableView + setObjectValue:(id)anObject + forTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex +{ + if ( aTableView == fieldMappingTableView ) { + [fieldMappingArray replaceObjectAtIndex:rowIndex withObject:anObject]; + + } else { + [[tables objectAtIndex:rowIndex] replaceObjectAtIndex:0 withObject:anObject]; + } +} + +#pragma mark - +#pragma mark other +//last but not least +- (id)init; +{ + self = [super init]; + + tables = [[NSMutableArray alloc] init]; + fieldMappingButtonOptions = [[NSMutableArray alloc] init]; + + return self; +} + +- (void)dealloc +{ + // NSLog(@"TableDump dealloc"); + + [tables release]; + [importArray release]; + [fieldMappingButtonOptions release]; + [fieldMappingArray release]; + [savePath release]; + [openPath release]; + [prefs release]; + + [super dealloc]; +} + + +@end diff --git a/Source/TableSource.h b/Source/TableSource.h new file mode 100644 index 00000000..2a62232c --- /dev/null +++ b/Source/TableSource.h @@ -0,0 +1,127 @@ +// +// TableSource.h +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Wed May 01 2002. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import <Cocoa/Cocoa.h> +#import <MCPKit_bundled/MCPKit_bundled.h> +#import "CMMCPConnection.h" +#import "CMMCPResult.h" + + +@interface TableSource : NSObject { + + IBOutlet id tablesListInstance; + + IBOutlet id tableWindow; + IBOutlet id indexSheet; + IBOutlet id keySheet; + IBOutlet id tableSourceView; + IBOutlet id indexView; + IBOutlet id addFieldButton; + IBOutlet id copyFieldButton; + IBOutlet id removeFieldButton; + IBOutlet id addIndexButton; + IBOutlet id removeIndexButton; + IBOutlet id indexTypeField; + IBOutlet id indexNameField; + IBOutlet id indexedColumnsField; + IBOutlet id chooseKeyButton; + IBOutlet id tableTypeButton; + IBOutlet id structureGrabber; + + CMMCPConnection *mySQLConnection; + CMMCPResult *tableSourceResult; + CMMCPResult *indexResult; + + NSString *selectedTable; + NSString *tableType; + NSMutableArray *tableFields, *indexes; + NSMutableDictionary *oldRow, *enumFields; + NSDictionary *defaultValues; + BOOL isEditingRow, isEditingNewRow, alertSheetOpened; + NSUserDefaults *prefs; +} + +//table methods +- (void)loadTable:(NSString *)aTable; +- (IBAction)reloadTable:(id)sender; + +//edit methods +- (IBAction)addField:(id)sender; +- (IBAction)copyField:(id)sender; +- (IBAction)addIndex:(id)sender; +- (IBAction)removeField:(id)sender; +- (IBAction)removeIndex:(id)sender; +- (IBAction)typeChanged:(id)sender; + +//index sheet methods +- (IBAction)openIndexSheet:(id)sender; +- (IBAction)closeIndexSheet:(id)sender; +- (IBAction)chooseIndexType:(id)sender; +- (void)closeAlertSheet; + +//key sheet methods +- (IBAction)closeKeySheet:(id)sender; + +//additional methods +- (void)setConnection:(CMMCPConnection *)theConnection; +- (NSArray *)fetchResultAsArray:(CMMCPResult *)theResult; +- (BOOL)addRowToDB; +- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo; + +//getter methods +- (NSString *)defaultValueForField:(NSString *)field; +- (NSArray *)fieldNames; +- (NSDictionary *)enumFields; + +//tableView datasource methods +- (int)numberOfRowsInTableView:(NSTableView *)aTableView; +- (id)tableView:(NSTableView *)aTableView + objectValueForTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex; +- (void)tableView:(NSTableView *)aTableView + setObjectValue:(id)anObject + forTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex; + +//tableView drag&drop datasource methods +- (BOOL)tableView:(NSTableView *)tv writeRows:(NSArray*)rows toPasteboard:(NSPasteboard*)pboard; +- (NSDragOperation)tableView:(NSTableView*)tv validateDrop:(id <NSDraggingInfo>)info proposedRow:(int)row + proposedDropOperation:(NSTableViewDropOperation)operation; +- (BOOL)tableView:(NSTableView*)tv acceptDrop:(id <NSDraggingInfo>)info row:(int)row dropOperation:(NSTableViewDropOperation)operation; + +//tableView delegate methods +- (BOOL)selectionShouldChangeInTableView:(NSTableView *)aTableView; +- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command; + +//slitView delegate methods +- (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview; +- (float)splitView:(NSSplitView *)sender constrainMaxCoordinate:(float)proposedMax ofSubviewAt:(int)offset; +- (float)splitView:(NSSplitView *)sender constrainMinCoordinate:(float)proposedMin ofSubviewAt:(int)offset; +- (NSRect)splitView:(NSSplitView *)splitView additionalEffectiveRectOfDividerAtIndex:(int)dividerIndex; + +//last but not least +- (id)init; +- (void)dealloc; + +@end diff --git a/Source/TableSource.m b/Source/TableSource.m new file mode 100644 index 00000000..58c5fb34 --- /dev/null +++ b/Source/TableSource.m @@ -0,0 +1,1065 @@ +// +// TableSource.m +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Wed May 01 2002. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import "TableSource.h" +#import "TablesList.h" + + +@implementation TableSource + +/* +loads aTable, put it in an array, update the tableViewColumns and reload the tableView +*/ +- (void)loadTable:(NSString *)aTable +{ + NSEnumerator *enumerator; + id field; + NSScanner *scanner = [NSScanner alloc]; + NSArray *extrasArray; + NSMutableDictionary *tempDefaultValues; + NSEnumerator *extrasEnumerator; + id extra; + int i; + + selectedTable = aTable; + [tableSourceView deselectAll:self]; + + if ( isEditingRow ) + return; + + // empty variables + [enumFields removeAllObjects]; + + //query started + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; + + if ( [aTable isEqualToString:@""] || !aTable ) { + [tableFields removeAllObjects]; + [indexes removeAllObjects]; + [tableSourceView reloadData]; + [indexView reloadData]; + [addFieldButton setEnabled:NO]; + [copyFieldButton setEnabled:NO]; + [removeFieldButton setEnabled:NO]; + [addIndexButton setEnabled:NO]; + [removeIndexButton setEnabled:NO]; + + // set the table type menu back to the default, and disable it + [tableTypeButton selectItemAtIndex:0]; + [tableTypeButton setEnabled:NO]; + tableType = nil; + + //query finished + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; + + [scanner release]; + + return; + } + + //perform queries and load results in array (each row as a dictionary) + tableSourceResult = [[mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM `%@`", selectedTable]] retain]; + + // listFieldsFromTable is broken in the current version of the framework (no back-ticks for table name)! + // tableSourceResult = [[mySQLConnection listFieldsFromTable:selectedTable] retain]; + // [tableFields setArray:[[self fetchResultAsArray:tableSourceResult] retain]]; + [tableFields setArray:[self fetchResultAsArray:tableSourceResult]]; + [tableSourceResult release]; + + indexResult = [[mySQLConnection queryString:[NSString stringWithFormat:@"SHOW INDEX FROM `%@`", selectedTable]] retain]; + // [indexes setArray:[[self fetchResultAsArray:indexResult] retain]]; + [indexes setArray:[self fetchResultAsArray:indexResult]]; + [indexResult release]; + + CMMCPResult *tableStatusResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW TABLE STATUS LIKE '%@'", selectedTable]]; + [tableType release]; + + NSDictionary *tempRow = [tableStatusResult fetchRowAsDictionary]; + if ( [tempRow objectForKey:@"Type"]) { + tableType = [tempRow objectForKey:@"Type"]; + } else { + tableType = [tempRow objectForKey:@"Engine"]; + } + [tableType retain]; + + //get table default values + if ( defaultValues ) { + [defaultValues release]; + defaultValues = nil; + } + + tempDefaultValues = [NSMutableDictionary dictionary]; + for ( i = 0 ; i < [tableFields count] ; i++ ) { + [tempDefaultValues setObject:[[tableFields objectAtIndex:i] objectForKey:@"Default"] forKey:[[tableFields objectAtIndex:i] objectForKey:@"Field"]]; + } + defaultValues = [[NSDictionary dictionaryWithDictionary:tempDefaultValues] retain]; + + //put field length and extras in separate key + enumerator = [tableFields objectEnumerator]; + + while ( (field = [enumerator nextObject]) ) { + NSString *type; + NSString *length; + NSString *extras; + + // scan for length and extras like unsigned + [scanner initWithString:[field objectForKey:@"Type"]]; + [scanner scanUpToString:@"(" intoString:&type]; + [scanner scanString:@"(" intoString:nil]; + + if ( ![scanner scanUpToString:@")" intoString:&length] ) + length = @""; + + [scanner scanString:@")" intoString:nil]; + if ( ![scanner scanUpToString:@"" intoString:&extras] ) { + extras = @""; + } + + // get possible values if field is enum or set + if ( [type isEqualToString:@"enum"] || [type isEqualToString:@"set"] ) { + NSMutableArray *possibleValues = [[[length substringWithRange:NSMakeRange(1,[length length]-2)] componentsSeparatedByString:@"','"] mutableCopy]; + NSMutableString *possibleValue = [NSMutableString string]; + + for ( i = 0 ; i < [possibleValues count] ; i++ ) { + [possibleValue setString:[possibleValues objectAtIndex:i]]; + [possibleValue replaceOccurrencesOfString:@"''" withString:@"'" options:nil range:NSMakeRange(0,[possibleValue length])]; + [possibleValue replaceOccurrencesOfString:@"\\\\" withString:@"\\" options:nil range:NSMakeRange(0,[possibleValue length])]; + [possibleValues replaceObjectAtIndex:i withObject:[NSString stringWithString:possibleValue]]; + } + + [enumFields setObject:[NSArray arrayWithArray:possibleValues] forKey:[field objectForKey:@"Field"]]; + [possibleValues release]; + } + + // scan extras for values like unsigned, zerofill, binary + extrasArray = [extras componentsSeparatedByString:@" "]; + extrasEnumerator = [extrasArray objectEnumerator]; + + while ( (extra = [extrasEnumerator nextObject]) ) { + if ( [extra isEqualToString:@"unsigned"] ) { + [field setObject:@"1" forKey:@"unsigned"]; + } else if ( [extra isEqualToString:@"zerofill"] ) { + [field setObject:@"1" forKey:@"zerofill"]; + } else if ( [extra isEqualToString:@"binary"] ) { + [field setObject:@"1" forKey:@"binary"]; + } else { + if ( ![extra isEqualToString:@""] ) + NSLog(@"ERROR: unknown option in field definition: %@", extra); + } + } + + [field setObject:type forKey:@"Type"]; + [field setObject:length forKey:@"Length"]; + } + + // Determine the table type + if ( ![tableType isKindOfClass:[NSNull class]] ) { + [tableTypeButton selectItemWithTitle:tableType]; + [tableTypeButton setEnabled:YES]; + } else { + [tableTypeButton selectItemWithTitle:@"--"]; + [tableTypeButton setEnabled:NO]; + } + + //enable buttons + [addFieldButton setEnabled:YES]; + [copyFieldButton setEnabled:YES]; + [removeFieldButton setEnabled:YES]; + [addIndexButton setEnabled:YES]; + [removeIndexButton setEnabled:YES]; + + //add columns to indexedColumnsField + [indexedColumnsField removeAllItems]; + enumerator = [tableFields objectEnumerator]; + + while ( (field = [enumerator nextObject]) ) { + [indexedColumnsField addItemWithObjectValue:[field objectForKey:@"Field"]]; + } + + if ( [tableFields count] < 10 ) { + [indexedColumnsField setNumberOfVisibleItems:[tableFields count]]; + } else { + [indexedColumnsField setNumberOfVisibleItems:10]; + } + + [tableSourceView reloadData]; + [indexView reloadData]; + + // display and *then* tile to force scroll bars to be in the correct position + [[tableSourceView enclosingScrollView] display]; + [[tableSourceView enclosingScrollView] tile]; + + //query finished + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; + + [scanner release]; +} + +/* +reloads the table (performing a new mysql-query) +*/ +- (IBAction)reloadTable:(id)sender +{ + [self loadTable:selectedTable]; +} + + +#pragma mark Edit methods + +/* +adds an empty row to the tableSource-array and goes into edit mode +*/ +- (IBAction)addField:(id)sender +{ +/* + if ( ![self addRowToDB] ) + return; +*/ + if ( ![self selectionShouldChangeInTableView:nil] ) + return; + + [tableFields addObject:[NSMutableDictionary + dictionaryWithObjects:[NSArray arrayWithObjects:@"",@"int",@"",@"0",@"0",@"0",@"YES",@"",[prefs stringForKey:@"nullValue"],@"None",nil] + forKeys:[NSArray arrayWithObjects:@"Field",@"Type",@"Length",@"unsigned",@"zerofill",@"binary",@"Null",@"Key",@"Default",@"Extra",nil]]]; + + isEditingRow = YES; + isEditingNewRow = YES; + [tableSourceView reloadData]; + [tableSourceView selectRow:[tableSourceView numberOfRows]-1 byExtendingSelection:NO]; + [tableSourceView editColumn:0 row:[tableSourceView numberOfRows]-1 withEvent:nil select:YES]; +} + +/* +copies a field and goes in edit mode for the new field +*/ +- (IBAction)copyField:(id)sender +{ + NSMutableDictionary *tempRow; + + if ( ![tableSourceView numberOfSelectedRows] ) + return; + if ( ![self selectionShouldChangeInTableView:nil] ) + return; + + //add copy of selected row and go in edit mode + tempRow = [NSMutableDictionary dictionaryWithDictionary:[tableFields objectAtIndex:[tableSourceView selectedRow]]]; + [tempRow setObject:[[tempRow objectForKey:@"Field"] stringByAppendingString:@"Copy"] forKey:@"Field"]; + [tempRow setObject:@"" forKey:@"Key"]; + [tempRow setObject:@"None" forKey:@"Extra"]; + [tableFields addObject:tempRow]; + isEditingRow = YES; + isEditingNewRow = YES; + [tableSourceView reloadData]; + [tableSourceView selectRow:[tableSourceView numberOfRows]-1 byExtendingSelection:NO]; + [tableSourceView editColumn:0 row:[tableSourceView numberOfRows]-1 withEvent:nil select:YES]; +} + +/* +adds the index to the mysql-db and stops modal session with code 1 when success, 0 when error and -1 when no columns specified +*/ +- (IBAction)addIndex:(id)sender +{ + NSString *indexName; + NSArray *indexedColumns; + NSMutableArray *tempIndexedColumns = [NSMutableArray array]; + NSEnumerator *enumerator; + NSString *string; + + if ( [[indexedColumnsField stringValue] isEqualToString:@""] ) { + [NSApp stopModalWithCode:-1]; + } else { + if ( [[indexNameField stringValue] isEqualToString:@"PRIMARY"] ) { + indexName = @""; + } else { + if ( [[indexNameField stringValue] isEqualToString:@""] ) + { + indexName = @""; + } else { + indexName = [NSString stringWithFormat:@"`%@`", [indexNameField stringValue]]; + } + } + indexedColumns = [[indexedColumnsField stringValue] componentsSeparatedByString:@","]; + enumerator = [indexedColumns objectEnumerator]; + while ( (string = [enumerator nextObject]) ) { + if ( ([string characterAtIndex:0] == ' ') ) { + [tempIndexedColumns addObject:[string substringWithRange:NSMakeRange(1,([string length]-1))]]; + } else { + [tempIndexedColumns addObject:[NSString stringWithString:string]]; + } + } + + [mySQLConnection queryString:[NSString stringWithFormat:@"ALTER TABLE `%@` ADD %@ %@ (`%@`)", + selectedTable, [indexTypeField titleOfSelectedItem], indexName, + [tempIndexedColumns componentsJoinedByString:@"`,`"]]]; + +/* +NSLog([NSString stringWithFormat:@"ALTER TABLE `%@` ADD %@ %@ (`%@`)", + selectedTable, [indexTypeField titleOfSelectedItem], indexName, + [[[indexedColumnsField stringValue] componentsSeparatedByString:@","] componentsJoinedByString:@"`,`"]]); +*/ + + if ( [[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + [self loadTable:selectedTable]; + [NSApp stopModalWithCode:1]; + } else { + [NSApp stopModalWithCode:0]; + } + } +} + +/* +opens alertsheet and asks for confirmation +*/ +- (IBAction)removeField:(id)sender +{ + if ( ![tableSourceView numberOfSelectedRows] ) + return; + if ( ![self selectionShouldChangeInTableView:nil] ) + return; + + NSBeginAlertSheet(NSLocalizedString(@"Warning", @"warning"), NSLocalizedString(@"Delete", @"delete button"), NSLocalizedString(@"Cancel", @"cancel button"), nil, tableWindow, self, @selector(sheetDidEnd:returnCode:contextInfo:), + nil, @"removefield", [NSString stringWithFormat:NSLocalizedString(@"Do you really want to delete the field %@?", @"message of panel asking for confirmation for deleting field"), + [[tableFields objectAtIndex:[tableSourceView selectedRow]] objectForKey:@"Field"]] ); +} + +/* +opens alertsheet and asks for confirmation +*/ +- (IBAction)removeIndex:(id)sender +{ + if ( ![indexView numberOfSelectedRows] ) + return; + if ( ![self selectionShouldChangeInTableView:nil] ) + return; + + NSBeginAlertSheet(NSLocalizedString(@"Warning", @"warning"), NSLocalizedString(@"Delete", @"delete button"), NSLocalizedString(@"Cancel", @"cancel button"), nil, tableWindow, self, @selector(sheetDidEnd:returnCode:contextInfo:), + nil, @"removeindex", [NSString stringWithFormat:NSLocalizedString(@"Do you really want to delete the index %@?", @"message of panel asking for confirmation for deleting index"), + [[indexes objectAtIndex:[indexView selectedRow]] objectForKey:@"Key_name"]] ); +} + +- (IBAction)typeChanged:(id)sender +{ + NSString* selectedItem = [sender titleOfSelectedItem]; + if([selectedItem isEqualToString:@"--"] || [tableType isEqualToString:selectedItem]) { + [sender selectItemWithTitle:tableType]; + } else { + // alert any listeners that we are about to perform a query. + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; + + NSString *query = [NSString stringWithFormat:@"ALTER TABLE `%@` TYPE = %@",selectedTable,selectedItem]; + [mySQLConnection queryString:query]; + + // The query is now complete. + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; + + // Did the alter work? If so, we need to record the new data. If not, we must revert back to + // the previous state. + if([mySQLConnection getLastErrorID] == 0) + { + // Make sure "tableType" is changed and the status tab is flagged for reload... + [tableType release]; + tableType = selectedItem; + [tableType retain]; + +// [[NSNotificationCenter defaultCenter] postNotificationName:@"SelectedTableStatusHasChanged" object:self]; + + // Mark the content table for refresh + [tablesListInstance setContentRequiresReload:YES]; + } else { + [sender selectItemWithTitle:tableType]; + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + [NSString stringWithFormat:NSLocalizedString(@"Couldn't change table type.\nMySQL said: %@", @"message of panel when table type cannot be removed"), [mySQLConnection getLastErrorMessage]]); + } + } +} + + +#pragma mark Index sheet methods + +/* +opens the indexSheet +*/ +- (IBAction)openIndexSheet:(id)sender +{ + int code = 0; + + if ( ![self selectionShouldChangeInTableView:nil] ) + return; + + [indexTypeField selectItemAtIndex:0]; + [indexNameField setEnabled:NO]; + [indexNameField setStringValue:@"PRIMARY"]; + [indexedColumnsField setStringValue:@""]; + + [NSApp beginSheet:indexSheet + modalForWindow:tableWindow modalDelegate:self + didEndSelector:nil contextInfo:nil]; + code = [NSApp runModalForWindow:indexSheet]; + + [NSApp endSheet:indexSheet]; + [indexSheet orderOut:nil]; + + //code == -1 -> no columns specified + //code == 0 -> error while adding index + //code == 1 -> index added with succes OR sheet closed without adding index + if ( code == 0 ) { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + [NSString stringWithFormat:NSLocalizedString(@"Couldn't add index.\nMySQL said: %@", @"message of panel when index cannot be created"), [mySQLConnection getLastErrorMessage]]); + } else if ( code == -1 ) { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, @selector(closeAlertSheet), nil, + NSLocalizedString(@"Please insert the columns you want to index.", @"message of panel when no columns are specified to be indexed")); + } +} + +/* +closes the indexSheet without adding the index (stops modal session with code 1) +*/ +- (IBAction)closeIndexSheet:(id)sender +{ + [NSApp stopModalWithCode:1]; +} + +/* +invoked when user chooses an index type +*/ +- (IBAction)chooseIndexType:(id)sender +{ + if ( [[indexTypeField titleOfSelectedItem] isEqualToString:@"PRIMARY KEY"] ) { + [indexNameField setEnabled:NO]; + [indexNameField setStringValue:@"PRIMARY"]; + } else { + [indexNameField setEnabled:YES]; + if ( [[indexNameField stringValue] isEqualToString:@"PRIMARY"] ) + [indexNameField setStringValue:@""]; + } +} + +/* +reopens indexSheet after errorSheet (no columns specified) +*/ +- (void)closeAlertSheet +{ + [self openIndexSheet:self]; +} + +/* +closes the keySheet +*/ +- (IBAction)closeKeySheet:(id)sender +{ + [NSApp stopModalWithCode:[sender tag]]; +} + + +#pragma mark Additional methods + +/* +sets the connection (received from TableDocument) and makes things that have to be done only once +*/ +- (void)setConnection:(CMMCPConnection *)theConnection +{ + NSEnumerator *indexColumnsEnumerator = [[indexView tableColumns] objectEnumerator]; + NSEnumerator *fieldColumnsEnumerator = [[tableSourceView tableColumns] objectEnumerator]; + id indexColumn; + id fieldColumn; + + mySQLConnection = theConnection; + + prefs = [[NSUserDefaults standardUserDefaults] retain]; + + //set up tableView + [tableSourceView registerForDraggedTypes:[NSArray arrayWithObjects:@"SequelProPasteboard", nil]]; + + while ( (indexColumn = [indexColumnsEnumerator nextObject]) ) { + if ( [prefs boolForKey:@"useMonospacedFonts"] ) { + [[indexColumn dataCell] setFont:[NSFont fontWithName:@"Monaco" size:10]]; + } + else + { + [[indexColumn dataCell] setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + } + } + while ( (fieldColumn = [fieldColumnsEnumerator nextObject]) ) { + if ( [prefs boolForKey:@"useMonospacedFonts"] ) { + [[fieldColumn dataCell] setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]]; + } + else + { + [[fieldColumn dataCell] setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + } + } +} + +/* +fetches the result as an array with a dictionary for each row in it +*/ +- (NSArray *)fetchResultAsArray:(CMMCPResult *)theResult +{ + NSMutableArray *tempResult = [NSMutableArray array]; + NSMutableDictionary *tempRow; + NSEnumerator *enumerator; + id key; + int i; + + if ([theResult numOfRows]) [theResult dataSeek:0]; + for ( i = 0 ; i < [theResult numOfRows] ; i++ ) { + tempRow = [NSMutableDictionary dictionaryWithDictionary:[theResult fetchRowAsDictionary]]; + + //use NULL string from preferences instead of the NSNull oject returned by the framework + enumerator = [tempRow keyEnumerator]; + while ( (key = [enumerator nextObject]) ) { + if ( [[tempRow objectForKey:key] isMemberOfClass:[NSNull class]] ) + [tempRow setObject:[prefs objectForKey:@"nullValue"] forKey:key]; + } + // change some fields to be more human-readable or GUI compatible + if ( [[tempRow objectForKey:@"Extra"] isEqualToString:@""] ) { + [tempRow setObject:@"None" forKey:@"Extra"]; + } + if ( [[tempRow objectForKey:@"Null"] isEqualToString:@"YES"] ) { +// [tempRow setObject:[NSNumber numberWithInt:0] forKey:@"Null"]; + [tempRow setObject:@"YES" forKey:@"Null"]; + } else { +// [tempRow setObject:[NSNumber numberWithInt:1] forKey:@"Null"]; + [tempRow setObject:@"NO" forKey:@"Null"]; + } + [tempResult addObject:tempRow]; + } + + return tempResult; +} + +- (BOOL)addRowToDB; +/* +tries to write row to mysql-db +returns YES if row written to db, otherwies NO +returns YES if no row is beeing edited and nothing has to be written to db +*/ +{ + NSDictionary *theRow; + NSMutableString *queryString; + int code; + + if ( !isEditingRow || ![tableSourceView numberOfSelectedRows] ) + return YES; + if ( alertSheetOpened ) + return NO; + + theRow = [tableFields objectAtIndex:[tableSourceView selectedRow]]; + + if ( isEditingNewRow ) { + //ADD syntax + if ( [[theRow objectForKey:@"Length"] isEqualToString:@""] || ![theRow objectForKey:@"Length"] ) { + queryString = [NSMutableString stringWithFormat:@"ALTER TABLE `%@` ADD `%@` %@", + selectedTable, [theRow objectForKey:@"Field"], [theRow objectForKey:@"Type"]]; + } else { + queryString = [NSMutableString stringWithFormat:@"ALTER TABLE `%@` ADD `%@` %@(%@)", + selectedTable, [theRow objectForKey:@"Field"], [theRow objectForKey:@"Type"], + [theRow objectForKey:@"Length"]]; + } + } else { + //CHANGE syntax + if ( [[theRow objectForKey:@"Length"] isEqualToString:@""] || ![theRow objectForKey:@"Length"] ) { + queryString = [NSMutableString stringWithFormat:@"ALTER TABLE `%@` CHANGE `%@` `%@` %@", + selectedTable, [oldRow objectForKey:@"Field"], [theRow objectForKey:@"Field"], + [theRow objectForKey:@"Type"]]; + } else { + queryString = [NSMutableString stringWithFormat:@"ALTER TABLE `%@` CHANGE `%@` `%@` %@(%@)", + selectedTable, [oldRow objectForKey:@"Field"], [theRow objectForKey:@"Field"], + [theRow objectForKey:@"Type"], [theRow objectForKey:@"Length"]]; + } + } + + //field specification + if ( [[theRow objectForKey:@"unsigned"] intValue] == 1 ) { + [queryString appendString:@" UNSIGNED"]; + } + if ( [[theRow objectForKey:@"zerofill"] intValue] == 1 ) { + [queryString appendString:@" ZEROFILL"]; + } + if ( [[theRow objectForKey:@"binary"] intValue] == 1 ) { + [queryString appendString:@" BINARY"]; + } +// if ( [[theRow objectForKey:@"Null"] isEqualToString:@"NO"] || [[theRow objectForKey:@"Null"] isEqualToString:@"NOT NULL"] +// || [[theRow objectForKey:@"Null"] isEqualToString:@"no"] || [[theRow objectForKey:@"Null"] isEqualToString:@"not null"]) + if ( [[theRow objectForKey:@"Null"] isEqualToString:@"NO"] ) + [queryString appendString:@" NOT NULL"]; + if ( ![[theRow objectForKey:@"Extra"] isEqualToString:@"auto_increment"] && !([[theRow objectForKey:@"Type"] isEqualToString:@"timestamp"] && [[theRow objectForKey:@"Default"] isEqualToString:@"NULL"]) ) { + if ( [[theRow objectForKey:@"Default"] isEqualToString:[prefs objectForKey:@"nullValue"]] ) { + if ([[theRow objectForKey:@"Null"] isEqualToString:@"YES"] ) { + [queryString appendString:@" DEFAULT NULL "]; + } + } else if ( [[theRow objectForKey:@"Type"] isEqualToString:@"timestamp"] && ([[theRow objectForKey:@"Default"] isEqualToString:@"CURRENT_TIMESTAMP"] || [[theRow objectForKey:@"Default"] isEqualToString:@"current_timestamp"]) ) { + [queryString appendString:@" DEFAULT CURRENT_TIMESTAMP "]; + } else { + // [queryString appendString:[NSString stringWithFormat:@" DEFAULT \"%@\" ", [theRow objectForKey:@"Default"]]]; + [queryString appendString:[NSString stringWithFormat:@" DEFAULT '%@' ", [mySQLConnection prepareString:[theRow objectForKey:@"Default"]]]]; + } + } else { + [queryString appendString:@" "]; + } + + if ( ![[theRow objectForKey:@"Extra"] isEqualToString:@""] && ![[theRow objectForKey:@"Extra"] isEqualToString:@"None"] && [theRow objectForKey:@"Extra"] ) { + [queryString appendString:[theRow objectForKey:@"Extra"]]; + } + + //asks to add an index to query if auto_increment is set and field isn't indexed + if ( [[theRow objectForKey:@"Extra"] isEqualToString:@"auto_increment"] + && ([[theRow objectForKey:@"Key"] isEqualToString:@""] || ![theRow objectForKey:@"Key"]) ) { + [chooseKeyButton selectItemAtIndex:0]; + [NSApp beginSheet:keySheet + modalForWindow:tableWindow modalDelegate:self + didEndSelector:nil contextInfo:nil]; + code = [NSApp runModalForWindow:keySheet]; + + [NSApp endSheet:keySheet]; + [keySheet orderOut:nil]; + + if ( code ) { + if ( [chooseKeyButton indexOfSelectedItem] == 0 ) { + [queryString appendString:@" PRIMARY KEY"]; + } else { + [queryString appendString:[NSString stringWithFormat:@", ADD %@ (`%@`)", + [chooseKeyButton titleOfSelectedItem], [theRow objectForKey:@"Field"]]]; + } + } + } + + [mySQLConnection queryString:queryString]; + + //NSLog(queryString); + + if ( [[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + isEditingRow = NO; + isEditingNewRow = NO; + [self loadTable:selectedTable]; + + // Mark the content table for refresh + [tablesListInstance setContentRequiresReload:YES]; + + return YES; + } else { + alertSheetOpened = YES; + //problem: alert sheet doesn't respond to first click + if ( isEditingNewRow ) { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), NSLocalizedString(@"Cancel", @"cancel button"), nil, tableWindow, self, @selector(sheetDidEnd:returnCode:contextInfo:), + nil, @"addrow", [NSString stringWithFormat:NSLocalizedString(@"Couldn't add field %@.\nMySQL said: %@", @"message of panel when field cannot be added"), + [theRow objectForKey:@"Field"], [mySQLConnection getLastErrorMessage]]); + } else { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), NSLocalizedString(@"Cancel", @"cancel button"), nil, tableWindow, self, @selector(sheetDidEnd:returnCode:contextInfo:), + nil, @"addrow", [NSString stringWithFormat:NSLocalizedString(@"Couldn't change field %@.\nMySQL said: %@", @"message of panel when field cannot be changed"), + [theRow objectForKey:@"Field"], [mySQLConnection getLastErrorMessage]]); + } + return NO; + } +} + +- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo +{ + /* + if contextInfo == addrow: remain in edit-mode if user hits OK, otherwise cancel editing + if contextInfo == removefield: removes row from mysql-db if user hits ok + if contextInfo == removeindex: removes index from mysql-db if user hits ok + */ + + [sheet orderOut:self]; + + if ( [contextInfo isEqualToString:@"addrow"] ) { + alertSheetOpened = NO; + if ( returnCode == NSAlertDefaultReturn ) { + //problem: reentering edit mode for first cell doesn't function + [tableSourceView editColumn:0 row:[tableSourceView selectedRow] withEvent:nil select:YES]; + } else { + if ( !isEditingNewRow ) { + [tableFields replaceObjectAtIndex:[tableSourceView selectedRow] + withObject:[NSMutableDictionary dictionaryWithDictionary:oldRow]]; + isEditingRow = NO; + } else { + [tableFields removeObjectAtIndex:[tableSourceView selectedRow]]; + isEditingRow = NO; + isEditingNewRow = NO; + } + } + [tableSourceView reloadData]; + } else if ( [contextInfo isEqualToString:@"removefield"] ) { + if ( returnCode == NSAlertDefaultReturn ) { + //remove row + [mySQLConnection queryString:[NSString stringWithFormat:@"ALTER TABLE `%@` DROP `%@`", + selectedTable, [[tableFields objectAtIndex:[tableSourceView selectedRow]] objectForKey:@"Field"]]]; + + if ( [[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + [self loadTable:selectedTable]; + + // Mark the content table for refresh + [tablesListInstance setContentRequiresReload:YES]; + } else { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + [NSString stringWithFormat:NSLocalizedString(@"Couldn't remove field %@.\nMySQL said: %@", @"message of panel when field cannot be removed"), + [[tableFields objectAtIndex:[tableSourceView selectedRow]] objectForKey:@"Field"], + [mySQLConnection getLastErrorMessage]]); + } + } + } else if ( [contextInfo isEqualToString:@"removeindex"] ) { + if ( returnCode == NSAlertDefaultReturn ) { + //remove index + if ( [[[indexes objectAtIndex:[indexView selectedRow]] objectForKey:@"Key_name"] isEqualToString:@"PRIMARY"] ) { + [mySQLConnection queryString:[NSString stringWithFormat:@"ALTER TABLE `%@` DROP PRIMARY KEY", selectedTable]]; + } else { + [mySQLConnection queryString:[NSString stringWithFormat:@"ALTER TABLE `%@` DROP INDEX `%@`", + selectedTable, [[indexes objectAtIndex:[indexView selectedRow]] objectForKey:@"Key_name"]]]; + } + + if ( [[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + [self loadTable:selectedTable]; + } else { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + [NSString stringWithFormat:NSLocalizedString(@"Couldn't remove index.\nMySQL said: %@", @"message of panel when index cannot be removed"), [mySQLConnection getLastErrorMessage]]); + } + } + } +} + + +#pragma mark Getter methods + +/* +get the default value for a specified field +*/ +- (NSString *)defaultValueForField:(NSString *)field +{ + if ( ![defaultValues objectForKey:field] ) { + return [prefs objectForKey:@"nullValue"]; + } else if ( [[defaultValues objectForKey:field] isMemberOfClass:[NSNull class]] ) { + return [prefs objectForKey:@"nullValue"]; + } else { + return [defaultValues objectForKey:field]; + } +} + +/* +returns an array containing the field names of the selected table +*/ +- (NSArray *)fieldNames +{ + NSMutableArray *tempArray = [NSMutableArray array]; + NSEnumerator *enumerator; + id field; + + //load table if not already done + if ( ![tablesListInstance structureLoaded] ) { + [self loadTable:(NSString *)[tablesListInstance table]]; + } + + //get field names + enumerator = [tableFields objectEnumerator]; + while ( (field = [enumerator nextObject]) ) { + [tempArray addObject:[field objectForKey:@"Field"]]; + } + + return [NSArray arrayWithArray:tempArray]; +} + +/* +returns a dictionary containing enum/set field names as key and possible values as array +*/ +- (NSDictionary *)enumFields +{ + return [NSDictionary dictionaryWithDictionary:enumFields]; +} + +#pragma mark TableView datasource methods + +- (int)numberOfRowsInTableView:(NSTableView *)aTableView +{ + if ( aTableView == tableSourceView ) { + return [tableFields count]; + } else { + return [indexes count]; + } +} + +- (id)tableView:(NSTableView *)aTableView + objectValueForTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex +{ + id theRow, theValue; + + if ( aTableView == tableSourceView ) { + theRow = [tableFields objectAtIndex:rowIndex]; + } else { + theRow = [indexes objectAtIndex:rowIndex]; + } + theValue = [theRow objectForKey:[aTableColumn identifier]]; + + return theValue; +} + +- (void)tableView:(NSTableView *)aTableView + setObjectValue:(id)anObject + forTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex +{ + if ( !isEditingRow ) { + [oldRow setDictionary:[tableFields objectAtIndex:rowIndex]]; + isEditingRow = YES; + } + if ( anObject ) { + [[tableFields objectAtIndex:rowIndex] setObject:anObject forKey:[aTableColumn identifier]]; + } else { + [[tableFields objectAtIndex:rowIndex] setObject:@"" forKey:[aTableColumn identifier]]; + } +} + +/* +Begin a drag and drop operation from the table - copy a single dragged row to the drag pasteboard. +*/ +- (BOOL)tableView:(NSTableView *)tableView writeRows:(NSArray*)rows toPasteboard:(NSPasteboard*)pboard +{ + int originalRow; + NSArray *pboardTypes; + + if ( ![self selectionShouldChangeInTableView:nil] ) + return NO; + + if ( ([rows count] == 1) && (tableView == tableSourceView) ) { + pboardTypes=[NSArray arrayWithObjects:@"SequelProPasteboard", nil]; + originalRow = [[rows objectAtIndex:0] intValue]; + + [pboard declareTypes:pboardTypes owner:nil]; + [pboard setString:[[NSNumber numberWithInt:originalRow] stringValue] forType:@"SequelProPasteboard"]; + + return YES; + } else { + return NO; + } +} + +/* +Determine whether to allow a drag and drop operation on this table - for the purposes of drag reordering, +validate that the original source is of the correct type and within the same table, and that the drag +would result in a position change. +*/ +- (NSDragOperation)tableView:(NSTableView*)tableView validateDrop:(id <NSDraggingInfo>)info proposedRow:(int)row + proposedDropOperation:(NSTableViewDropOperation)operation +{ + NSArray *pboardTypes = [[info draggingPasteboard] types]; + int originalRow; + + // Ensure the drop is of the correct type + if (operation == NSTableViewDropAbove && row != -1 && [pboardTypes containsObject:@"SequelProPasteboard"]) { + + // Ensure the drag originated within this table + if ([info draggingSource] == tableView) { + originalRow = [[[info draggingPasteboard] stringForType:@"SequelProPasteboard"] intValue]; + + if (row != originalRow && row != (originalRow+1)) { + return NSDragOperationMove; + } + } + } + + return NSDragOperationNone; +} + +/* +Having validated a drop, perform the field/column reordering to match. +*/ +- (BOOL)tableView:(NSTableView*)tableView acceptDrop:(id <NSDraggingInfo>)info row:(int)destinationRow dropOperation:(NSTableViewDropOperation)operation +{ + int originalRow; + NSMutableString *queryString; + + // Extract the original row position from the pasteboard. + originalRow = [[[info draggingPasteboard] stringForType:@"SequelProPasteboard"] intValue]; + + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; + + // Begin construction of the reordering query + queryString = [NSMutableString stringWithFormat:@"ALTER TABLE `%@` MODIFY COLUMN `%@` %@", selectedTable, + [[tableFields objectAtIndex:originalRow] objectForKey:@"Field"], + [[tableFields objectAtIndex:originalRow] objectForKey:@"Type"]]; + + // Add the length parameter if necessary + if ( [[tableFields objectAtIndex:originalRow] objectForKey:@"Length"] && + ![[[tableFields objectAtIndex:originalRow] objectForKey:@"Length"] isEqualToString:@""]) + { + [queryString appendString:[NSString stringWithFormat:@"(%@)", + [[tableFields objectAtIndex:originalRow] objectForKey:@"Length"]]]; + } + + // Add the new location + if ( destinationRow == 0 ){ + [queryString appendString:@" FIRST"]; + } else { + [queryString appendString:[NSString stringWithFormat:@" AFTER `%@`", + [[tableFields objectAtIndex:destinationRow-1] objectForKey:@"Field"]]]; + } + + // Run the query; report any errors, or reload the table on success + [mySQLConnection queryString:queryString]; + if ( ![[mySQLConnection getLastErrorMessage] isEqualTo:@""] ) { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + [NSString stringWithFormat:NSLocalizedString(@"Couldn't move field. MySQL said: %@", @"message of panel when field cannot be added in drag&drop operation"), [mySQLConnection getLastErrorMessage]]); + } else { + [self loadTable:selectedTable]; + if ( originalRow < destinationRow ) { + [tableSourceView selectRow:destinationRow-1 byExtendingSelection:NO]; + } else { + [tableSourceView selectRow:destinationRow byExtendingSelection:NO]; + } + } + + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; + + // Mark the content table for refresh + [tablesListInstance setContentRequiresReload:YES]; + + return YES; +} + +#pragma mark TtableView delegate methods + +- (BOOL)selectionShouldChangeInTableView:(NSTableView *)aTableView +{ +/* + int row = [tableSourceView editedRow]; + int column = [tableSourceView editedColumn]; + NSTableColumn *tableColumn; + NSCell *cell; + + if ( row != -1 ) { + tableColumn = [[tableSourceView tableColumns] objectAtIndex:column]; + cell = [tableColumn dataCellForRow:row]; + [cell endEditing:[tableSourceView currentEditor]]; + } +*/ +//end editing (otherwise problems when user hits reload button) + [tableWindow endEditingFor:nil]; + + return [self addRowToDB]; +} + +/* +traps enter and esc and make/cancel editing without entering next row +*/ +- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command +{ + int row, column; + + row = [tableSourceView editedRow]; + column = [tableSourceView editedColumn]; + + if ( [textView methodForSelector:command] == [textView methodForSelector:@selector(insertNewline:)] || + [textView methodForSelector:command] == [textView methodForSelector:@selector(insertTab:)] ) //trap enter and tab + { + //save current line + [[control window] makeFirstResponder:control]; + if ( column == 9 ) { + if ( [self addRowToDB] && [textView methodForSelector:command] == [textView methodForSelector:@selector(insertTab:)] ) { + if ( row < ([tableSourceView numberOfRows] - 1) ) { + [tableSourceView selectRow:row+1 byExtendingSelection:NO]; + [tableSourceView editColumn:0 row:row+1 withEvent:nil select:YES]; + } else { + [tableSourceView selectRow:0 byExtendingSelection:NO]; + [tableSourceView editColumn:0 row:0 withEvent:nil select:YES]; + } + } + } else { + if ( column == 2 ) { + [tableSourceView editColumn:column+4 row:row withEvent:nil select:YES]; + } else if ( column == 6 ) { + [tableSourceView editColumn:column+2 row:row withEvent:nil select:YES]; + } else { + [tableSourceView editColumn:column+1 row:row withEvent:nil select:YES]; + } + } + return TRUE; + + } 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; + [tableFields replaceObjectAtIndex:row withObject:[NSMutableDictionary dictionaryWithDictionary:oldRow]]; + } else if ( isEditingNewRow ) { + isEditingRow = NO; + isEditingNewRow = NO; + [tableFields removeObjectAtIndex:row]; + [tableSourceView reloadData]; + } + return TRUE; + } else { + return FALSE; + } +} + + +#pragma mark SplitView delegate methods + +- (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview +{ + return YES; +} + +- (float)splitView:(NSSplitView *)sender constrainMaxCoordinate:(float)proposedMax ofSubviewAt:(int)offset +{ + return proposedMax - 150; +} + +- (float)splitView:(NSSplitView *)sender constrainMinCoordinate:(float)proposedMin ofSubviewAt:(int)offset +{ + return proposedMin + 150; +} + +- (NSRect)splitView:(NSSplitView *)splitView additionalEffectiveRectOfDividerAtIndex:(int)dividerIndex +{ + return [structureGrabber convertRect:[structureGrabber bounds] toView:splitView]; +} + +//last but not least +- (id)init +{ + self = [super init]; + + tableFields = [[NSMutableArray alloc] init]; + indexes = [[NSMutableArray alloc] init]; + oldRow = [[NSMutableDictionary alloc] init]; + enumFields = [[NSMutableDictionary alloc] init]; + + return self; +} + +- (void)dealloc +{ + [tableFields release]; + [indexes release]; + [oldRow release]; + [defaultValues release]; + [prefs release]; + [enumFields release]; + + [super dealloc]; +} + +@end diff --git a/Source/TableStatus.h b/Source/TableStatus.h new file mode 100644 index 00000000..cb631435 --- /dev/null +++ b/Source/TableStatus.h @@ -0,0 +1,63 @@ +// +// TableContent.h +// sequel-pro +// +// Created by Jason Hallford (jason.hallford@byu.edu) on Th July 08 2004. +// sequel-pro Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import <Cocoa/Cocoa.h> +#import <MCPKit_bundled/MCPKit_bundled.h> +#import "CMMCPConnection.h" +#import "CMMCPResult.h" + +@interface TableStatus : NSObject +{ + IBOutlet id commentsBox; + IBOutlet id rowsNumber; + IBOutlet id rowsFormat; + IBOutlet id rowsAvgLength; + IBOutlet id rowsAutoIncrement; + IBOutlet id sizeData; + IBOutlet id sizeFree; + IBOutlet id sizeIndex; + IBOutlet id sizeMaxData; + IBOutlet id tableCreatedAt; + IBOutlet id tableName; + IBOutlet id tableType; + IBOutlet id tableUpdatedAt; + + CMMCPConnection *mySQLConnection; + CMMCPResult *tableStatusResult; + + NSString *selectedTable; + NSDictionary* statusFields; +} + +// Table methods +- (void)loadTable:(NSString *)aTable; +- (IBAction)reloadTable:(id)sender; + +// Additional methods +- (void)setConnection:(CMMCPConnection *)theConnection; +- (void)awakeFromNib; + +// Initialization +- (id)init; +@end diff --git a/Source/TableStatus.m b/Source/TableStatus.m new file mode 100644 index 00000000..5bfd5513 --- /dev/null +++ b/Source/TableStatus.m @@ -0,0 +1,113 @@ +#import "TableStatus.h" + +@implementation TableStatus + +- (void)awakeFromNib +{ + // TODO: implement awake code. +} + +- (void)setConnection:(CMMCPConnection *)theConnection +{ + mySQLConnection = theConnection; + [mySQLConnection retain]; +} + +- (NSString*)getSQLColumnValue:(NSString *)withName usingFields:(NSDictionary*)fields withLabel:(NSString*)label +{ + NSString* value = [fields objectForKey:withName]; + if([value isKindOfClass:[NSNull class]]) + { + value = @"--"; + } + + NSString* labelVal = [NSString stringWithFormat:@"%@: %@",label,value]; + + return labelVal; +} + +- (void)loadTable:(NSString *)aTable +{ + // Store the table name away for future use... + selectedTable = aTable; + + // Notify any listeners that a query is about to begin... + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; + + // no table selected + if([aTable isEqualToString:@""] || !aTable) { + [tableName setStringValue:@"Name: --"]; + [tableType setStringValue:@"Type: --"]; + [tableCreatedAt setStringValue:@"Created At: --"]; + [tableUpdatedAt setStringValue:@"Updated At: --"]; + + // Assign the row values... + [rowsNumber setStringValue:@"Number Of: --"]; + [rowsFormat setStringValue:@"Format: --"]; + [rowsAvgLength setStringValue:@"Avg. Length: --"]; + [rowsAutoIncrement setStringValue:@"Auto Increment: --"]; + + // Assign the size values... + [sizeData setStringValue:@"Data: --"]; + [sizeMaxData setStringValue:@"Max Data: --"]; + [sizeIndex setStringValue:@"Index: --"]; + [sizeFree setStringValue:@"Free: --"]; + + // Finally, set the value of the comments box + [commentsBox setStringValue:@"--"]; + + // Tell everyone we've finished with our query... + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; + return; + } + + // Run the query to retrieve the status of the selected table. We'll then use this information to populate + // the associated view's controls. + tableStatusResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW TABLE STATUS LIKE '%@'", selectedTable]]; + + statusFields = [tableStatusResult fetchRowAsDictionary]; + + // Assign the table values... + [tableName setStringValue:[NSString stringWithFormat:@"Name: %@",selectedTable]]; + if ( [statusFields objectForKey:@"Type"] ) { + [tableType setStringValue:[self getSQLColumnValue:@"Type" usingFields:statusFields withLabel:@"Type"]]; + } else { + // mysql > 4.1 + [tableType setStringValue:[self getSQLColumnValue:@"Engine" usingFields:statusFields withLabel:@"Type"]]; + } + [tableCreatedAt setStringValue:[self getSQLColumnValue:@"Create_time" usingFields:statusFields withLabel:@"Created At"]]; + [tableUpdatedAt setStringValue:[self getSQLColumnValue:@"Update_time" usingFields:statusFields withLabel:@"Updated At"]]; + + // Assign the row values... + [rowsNumber setStringValue:[self getSQLColumnValue:@"Rows" usingFields:statusFields withLabel:@"Number Of"]]; + [rowsFormat setStringValue:[self getSQLColumnValue:@"Row_format" usingFields:statusFields withLabel:@"Format"]]; + [rowsAvgLength setStringValue:[self getSQLColumnValue:@"Avg_row_length" usingFields:statusFields withLabel:@"Avg. Length"]]; + [rowsAutoIncrement setStringValue:[self getSQLColumnValue:@"Auto_increment" usingFields:statusFields withLabel:@"Auto Increment"]]; + + // Assign the size values... + [sizeData setStringValue:[self getSQLColumnValue:@"Data_length" usingFields:statusFields withLabel:@"Data"]]; + [sizeMaxData setStringValue:[self getSQLColumnValue:@"Max_data_length" usingFields:statusFields withLabel:@"Max Data"]]; + [sizeIndex setStringValue:[self getSQLColumnValue:@"Index_length" usingFields:statusFields withLabel:@"Index"]]; + [sizeFree setStringValue:[self getSQLColumnValue:@"Data_free" usingFields:statusFields withLabel:@"Free"]]; + + // Finally, assign the comments... + [commentsBox setStringValue:[statusFields objectForKey:@"Comment"]]; + + // Tell everyone we've finished with our query... + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; + + return; +} + +- (IBAction)reloadTable:(id)sender +{ + [self loadTable:selectedTable]; +} + +- (id)init +{ + self = [super init]; + + return self; +} +@end diff --git a/Source/TablesList.h b/Source/TablesList.h new file mode 100644 index 00000000..7270219b --- /dev/null +++ b/Source/TablesList.h @@ -0,0 +1,95 @@ +// +// TablesList.h +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Wed May 01 2002. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import <Cocoa/Cocoa.h> +#import <MCPKit_bundled/MCPKit_bundled.h> + +@class CMMCResult; +@class CMMCPConnection; + +@interface TablesList : NSObject { + + IBOutlet id tableDocumentInstance; + IBOutlet id tableSourceInstance; + IBOutlet id tableContentInstance; + IBOutlet id customQueryInstance; + IBOutlet id tableDumpInstance; + IBOutlet id tableStatusInstance; + + IBOutlet id tableWindow; + IBOutlet id copyTableSheet; + IBOutlet id tablesListView; + IBOutlet id copyTableNameField; + IBOutlet id copyTableContentSwitch; + IBOutlet id tabView; + + CMMCPConnection *mySQLConnection; + NSMutableArray *tables; +// NSUserDefaults *prefs; + BOOL structureLoaded, contentLoaded, statusLoaded, alertSheetOpened; +} + +//IBAction methods +- (IBAction)updateTables:(id)sender; +- (IBAction)addTable:(id)sender; +- (IBAction)removeTable:(id)sender; +- (IBAction)copyTable:(id)sender; + +//alert sheet methods +- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo; + +//copyTableSheet methods +- (IBAction)closeCopyTableSheet:(id)sender; + +//additional methods +- (void)removeTable; +- (void)setConnection:(CMMCPConnection *)theConnection; +- (void)doPerformQueryService:(NSString *)query; + +//getter methods +- (NSString *)table; +- (NSArray *)tables; +- (BOOL)structureLoaded; +- (BOOL)contentLoaded; +- (BOOL)statusLoaded; + +// Setter methods +- (void)setContentRequiresReload:(BOOL)reload; + +//tableView datasource methods +- (int)numberOfRowsInTableView:(NSTableView *)aTableView; +- (id)tableView:(NSTableView *)aTableView + objectValueForTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex; +- (void)tableView:(NSTableView *)aTableView + setObjectValue:(id)anObject + forTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex; + +//tableView delegate methods +- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command; +- (BOOL)selectionShouldChangeInTableView:(NSTableView *)aTableView; +- (void)tableViewSelectionDidChange:(NSNotification *)aNotification; + +@end diff --git a/Source/TablesList.m b/Source/TablesList.m new file mode 100644 index 00000000..40ea87c5 --- /dev/null +++ b/Source/TablesList.m @@ -0,0 +1,700 @@ +// +// TablesList.m +// sequel-pro +// +// Created by lorenz textor (lorenz@textor.ch) on Wed May 01 2002. +// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> +// Or mail to <lorenz@textor.ch> + +#import "TablesList.h" +#import "TableDocument.h" +#import "TableSource.h" +#import "TableContent.h" +#import "TableDump.h" +#import "ImageAndTextCell.h" +#import "CMMCPConnection.h" +#import "CMMCPResult.h" + +@implementation TablesList + + +#pragma mark IBAction methods + +/* +loads all table names in array tables and reload the tableView +*/ +- (IBAction)updateTables:(id)sender +{ + CMMCPResult *theResult; + int i; + + //query started + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; + + [tablesListView deselectAll:self]; + [tables removeAllObjects]; + [tables addObject:NSLocalizedString(@"TABLES",@"header for table list")]; + + theResult = (CMMCPResult *)[mySQLConnection listTables]; + if ([theResult numOfRows]) [theResult dataSeek:0]; + for ( i = 0 ; i < [theResult numOfRows] ; i++ ) { + [tables addObject:[[theResult fetchRowAsArray] objectAtIndex:0]]; + } + + [tablesListView reloadData]; + + //query finished + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; +} + +/* +adds a new table to the tables-array (no changes in mysql-db) +*/ +- (IBAction)addTable:(id)sender +{ + if ( ![tableSourceInstance selectionShouldChangeInTableView:nil] || + ![tableContentInstance selectionShouldChangeInTableView:nil] || + ![tableDocumentInstance database] ) + return; + [tableWindow endEditingFor:nil]; + + [tables addObject:@""]; + [tablesListView reloadData]; + [tablesListView selectRow:[tables count]-1 byExtendingSelection:NO]; + [tablesListView editColumn:0 row:[tables count]-1 withEvent:nil select:YES]; +} + +/* +invoked when user hits the remove button +alert sheet to ask user if he really wants to delete the table +*/ +- (IBAction)removeTable:(id)sender +{ + if ( ![tablesListView numberOfSelectedRows] ) + return; + [tableWindow endEditingFor:nil]; + + if ( [tablesListView numberOfSelectedRows] == 1 ) { + NSBeginAlertSheet(NSLocalizedString(@"Warning", @"warning"), NSLocalizedString(@"Delete", @"delete button"), NSLocalizedString(@"Cancel", @"cancel button"), nil, tableWindow, self, + @selector(sheetDidEnd:returnCode:contextInfo:), nil, + @"removeRow", [NSString stringWithFormat:NSLocalizedString(@"Do you really want to delete the table %@?", @"message of panel asking for confirmation for deleting table"), + [tables objectAtIndex:[tablesListView selectedRow]]]); + } else { + NSBeginAlertSheet(NSLocalizedString(@"Warning", @"warning"), NSLocalizedString(@"Delete", @"delete button"), NSLocalizedString(@"Cancel", @"cancel button"), nil, tableWindow, self, + @selector(sheetDidEnd:returnCode:contextInfo:), nil, + @"removeRow", [NSString stringWithFormat:NSLocalizedString(@"Do you really want to delete the selected tables?", @"message of panel asking for confirmation for deleting tables"), + [tables objectAtIndex:[tablesListView selectedRow]]]); + } +} + +/* +copies a table, if desired with content +*/ +- (IBAction)copyTable:(id)sender +{ + CMMCPResult *queryResult; + NSScanner *scanner = [NSScanner alloc]; + NSString *scanString; +// NSArray *fieldNames; +// NSArray *theRow; +// NSMutableString *rowValue = [NSMutableString string]; +// NSMutableArray *fieldValues; + int code; +// int rowCount, i, j; +// BOOL errors = NO; + + if ( [tablesListView numberOfSelectedRows] != 1 ) + return; + if ( ![tableSourceInstance selectionShouldChangeInTableView:nil] || ![tableContentInstance selectionShouldChangeInTableView:nil] ) + return; + [tableWindow endEditingFor:nil]; + + //open copyTableSheet + [copyTableNameField setStringValue:[NSString stringWithFormat:@"%@Copy", [tables objectAtIndex:[tablesListView selectedRow]]]]; + [copyTableContentSwitch setState:NSOffState]; + [NSApp beginSheet:copyTableSheet + modalForWindow:tableWindow + modalDelegate:self + didEndSelector:nil + contextInfo:nil]; + + code = [NSApp runModalForWindow:copyTableSheet]; + + [NSApp endSheet:copyTableSheet]; + [copyTableSheet orderOut:nil]; + + if ( !code ) + return; + if ( [[copyTableNameField stringValue] isEqualToString:@""] ) { + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, NSLocalizedString(@"Table must have a name.", @"message of panel when no name is given for table")); + return; + } + + //get table structure + queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW CREATE TABLE `%@`", + [tables objectAtIndex:[tablesListView selectedRow]]]]; + + if ( ![queryResult numOfRows] ) { + //error while getting table structure + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + [NSString stringWithFormat:NSLocalizedString(@"Couldn't get table information.\nMySQL said: %@", @"message of panel when table information cannot be retrieved"), [mySQLConnection getLastErrorMessage]]); + + } else { + //insert new table name in create syntax and create new table + [scanner initWithString:[[queryResult fetchRowAsDictionary] objectForKey:@"Create Table"]]; + [scanner scanUpToString:@"(" intoString:nil]; + [scanner scanUpToString:@"" intoString:&scanString]; + [mySQLConnection queryString:[NSString stringWithFormat:@"CREATE TABLE `%@` %@", [copyTableNameField stringValue], scanString]]; + + if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + //error while creating new table + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + [NSString stringWithFormat:NSLocalizedString(@"Couldn't create table.\nMySQL said: %@", @"message of panel when table cannot be created"), [mySQLConnection getLastErrorMessage]]); + } else { + + if ( [copyTableContentSwitch state] == NSOnState ) { + //copy table content + [mySQLConnection queryString:[NSString stringWithFormat: + @"INSERT INTO `%@` SELECT * FROM `%@`", + [copyTableNameField stringValue], + [tables objectAtIndex:[tablesListView selectedRow]] + ]]; + + if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + NSBeginAlertSheet( + NSLocalizedString(@"Warning", @"warning"), + NSLocalizedString(@"OK", @"OK button"), + nil, + nil, + tableWindow, + self, + nil, + nil, + nil, + NSLocalizedString(@"There have been errors while copying table content. Please control the new table.", @"message of panel when table content cannot be copied") + ); + } + } + + [tables insertObject:[copyTableNameField stringValue] atIndex:[tablesListView selectedRow]+1]; + [tablesListView reloadData]; + [tablesListView selectRow:[tablesListView selectedRow]+1 byExtendingSelection:NO]; + [tablesListView scrollRowToVisible:[tablesListView selectedRow]]; + } + } +} + + +#pragma mark Alert sheet methods + +/* +method for alert sheets +invoked when user wants to delete a table +*/ +- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo +{ + if ( [contextInfo isEqualToString:@"addRow"] ) { + alertSheetOpened = NO; + } else if ( [contextInfo isEqualToString:@"removeRow"] ) { + if ( returnCode == NSAlertDefaultReturn ) { + [sheet orderOut:self]; + [self removeTable]; + } + } +} + +/* +closes copyTableSheet and stops modal session +*/ +- (IBAction)closeCopyTableSheet:(id)sender +{ + [NSApp stopModalWithCode:[sender tag]]; +} + +#pragma mark Additional methods + +/* +removes selected table(s) from mysql-db and tableView +*/ +- (void)removeTable; +{ + NSIndexSet *indexes = [tablesListView selectedRowIndexes]; + NSString *errorText; + BOOL error = FALSE; + + // get last index + unsigned currentIndex = [indexes lastIndex]; + while (currentIndex != NSNotFound) + { + [mySQLConnection queryString:[NSString stringWithFormat:@"DROP TABLE `%@`", [tables objectAtIndex:currentIndex]]]; + + if ( [[mySQLConnection getLastErrorMessage] isEqualTo:@""] ) { + //dropped table with success + [tables removeObjectAtIndex:currentIndex]; + } else { + //couldn't drop table + error = TRUE; + errorText = [mySQLConnection getLastErrorMessage]; + } + + // get next index (beginning from the end) + currentIndex = [indexes indexLessThanIndex:currentIndex]; + } + + [tablesListView deselectAll:self]; + //[tableSourceInstance loadTable:nil]; + //[tableContentInstance loadTable:nil]; + //[tableStatusInstance loadTable:nil]; + [tablesListView reloadData]; + + // set window title + [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@@%@/%@", [tableDocumentInstance mySQLVersion], [tableDocumentInstance user], + [tableDocumentInstance host], [tableDocumentInstance database]]]; + if ( error ) + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + [NSString stringWithFormat:NSLocalizedString(@"Couldn't remove table.\nMySQL said: %@", @"message of panel when table cannot be removed"), errorText]); +} + +/* +sets the connection (received from TableDocument) and makes things that have to be done only once +*/ +- (void)setConnection:(CMMCPConnection *)theConnection +{ + mySQLConnection = theConnection; + [self updateTables:self]; +} + +/* +selects customQuery tab and passes query to customQueryInstance +*/ +- (void)doPerformQueryService:(NSString *)query +{ + [tabView selectTabViewItemAtIndex:2]; + [customQueryInstance doPerformQueryService:query]; +} + + +#pragma mark Getter methods + +/* +returns the currently selected table or nil if no table or mulitple tables are selected +*/ +- (NSString *)table +{ + if ( [tablesListView numberOfSelectedRows] == 1 ) { + return [tables objectAtIndex:[tablesListView selectedRow]]; + } else if ([tablesListView numberOfSelectedRows] > 1) { + return @""; + } else { + return nil; + } +} + +- (NSArray *)tables +{ + return tables; +} + +/* +returns YES if table source has already been loaded +*/ +- (BOOL)structureLoaded +{ + return structureLoaded; +} + +/* +returns YES if table content has already been loaded +*/ +- (BOOL)contentLoaded +{ + return contentLoaded; +} + +/* +returns YES if table status has already been loaded +*/ +- (BOOL)statusLoaded +{ + return statusLoaded; +} + +#pragma mark Setter methods + +/* +Mark the content table for refresh when it's next switched to +*/ +- (void)setContentRequiresReload:(BOOL)reload +{ + contentLoaded = !reload; +} + +#pragma mark Datasource methods + +- (int)numberOfRowsInTableView:(NSTableView *)aTableView +{ + return [tables count]; +} + +- (id)tableView:(NSTableView *)aTableView + objectValueForTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex +{ + return [tables objectAtIndex:rowIndex]; +} + +/** + * adds or renames a table (in tables-array and mysql-db) + * removes new table from table-array if renaming had no success + */ +- (void)tableView:(NSTableView *)aTableView + setObjectValue:(id)anObject + forTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex +{ + + if ( [[tables objectAtIndex:rowIndex] isEqualToString:@""] ) { + //new table + if ( [anObject isEqualToString:@""] ) { + //table has no name + alertSheetOpened = YES; + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, + @selector(sheetDidEnd:returnCode:contextInfo:), nil, @"addRow", NSLocalizedString(@"Table must have a name.", @"message of panel when no name is given for table")); + [tables removeObjectAtIndex:rowIndex]; + [tablesListView reloadData]; + } else { + if ( [tableDocumentInstance supportsEncoding] ) { + [mySQLConnection queryString:[NSString stringWithFormat:@"CREATE TABLE `%@` (id int) DEFAULT CHARACTER SET %@", anObject, [tableDocumentInstance encoding]]]; + } else { + [mySQLConnection queryString:[NSString stringWithFormat:@"CREATE TABLE `%@` (id int)", anObject]]; + } + + if ( [[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + //added table with success + [tables replaceObjectAtIndex:rowIndex withObject:anObject]; + + if ( [tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 0 ) { + [tableSourceInstance loadTable:anObject]; + structureLoaded = YES; + contentLoaded = NO; + statusLoaded = NO; + } else if ( [tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 1 ) { + [tableSourceInstance loadTable:anObject]; + [tableContentInstance loadTable:anObject]; + structureLoaded = YES; + contentLoaded = YES; + statusLoaded = NO; + } else if ( [tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 3 ) { + [tableStatusInstance loadTable:anObject]; + statusLoaded = YES; + structureLoaded = NO; + contentLoaded = NO; + } else { + statusLoaded = NO; + structureLoaded = NO; + contentLoaded = NO; + } + + // set window title + [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@@%@/%@/%@", [tableDocumentInstance mySQLVersion], [tableDocumentInstance user], + [tableDocumentInstance host], [tableDocumentInstance database], anObject]]; + } else { + + //error while adding new table + alertSheetOpened = YES; + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, + @selector(sheetDidEnd:returnCode:contextInfo:), nil, @"addRow", + [NSString stringWithFormat:NSLocalizedString(@"Couldn't add table %@.\nMySQL said: %@", @"message of panel when table cannot be created with the given name"), + anObject, [mySQLConnection getLastErrorMessage]]); + [tables removeObjectAtIndex:rowIndex]; + [tablesListView reloadData]; + } + } + } else { + + //table modification + if ( [[tables objectAtIndex:rowIndex] isEqualToString:anObject] ) { + //no changes in table name +// NSLog(@"no changes in table name"); + } else if ( [anObject isEqualToString:@""] ) { + //table has no name +// NSLog(@"name is nil"); + alertSheetOpened = YES; + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, + @selector(sheetDidEnd:returnCode:contextInfo:), nil, @"addRow", NSLocalizedString(@"Table must have a name.", @"message of panel when no name is given for table")); + } else { + [mySQLConnection queryString:[NSString stringWithFormat:@"RENAME TABLE `%@` TO `%@`", [tables objectAtIndex:rowIndex], anObject]]; + if ( [[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { +// NSLog(@"renamed table with success"); + //renamed with success + [tables replaceObjectAtIndex:rowIndex withObject:anObject]; + + if ( [tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 0 ) { + [tableSourceInstance loadTable:anObject]; + structureLoaded = YES; + contentLoaded = NO; + statusLoaded = NO; + } else if ( [tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 1 ) { + [tableSourceInstance loadTable:anObject]; + [tableContentInstance loadTable:anObject]; + structureLoaded = YES; + contentLoaded = YES; + statusLoaded = NO; + } else if ( [tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 3 ) { + [tableStatusInstance loadTable:anObject]; + structureLoaded = NO; + contentLoaded = NO; + statusLoaded = YES; + } else { + statusLoaded = NO; + structureLoaded = NO; + contentLoaded = NO; + } + + // set window title + [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@@%@/%@/%@", [tableDocumentInstance mySQLVersion], [tableDocumentInstance user], + [tableDocumentInstance host], [tableDocumentInstance database], anObject]]; + } else { + //error while renaming +// NSLog(@"couldn't rename table"); + alertSheetOpened = YES; + NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, + @selector(sheetDidEnd:returnCode:contextInfo:), nil, @"addRow", + [NSString stringWithFormat:NSLocalizedString(@"Couldn't rename table.\nMySQL said: %@", @"message of panel when table cannot be renamed"), + [mySQLConnection getLastErrorMessage]]); + } + } + } +} + +#pragma mark TableView delegate methods + +/* +traps enter and esc and edit/cancel without entering next row +*/ +- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command +{ + if ( [textView methodForSelector:command] == [textView methodForSelector:@selector(insertNewline:)] ) { + //save current line + [[control window] makeFirstResponder:control]; + return TRUE; + + } else if ( [[control window] methodForSelector:command] == [[control window] methodForSelector:@selector(_cancelKey:)] || + [textView methodForSelector:command] == [textView methodForSelector:@selector(complete:)] ) { + + //abort editing + [control abortEditing]; + + if ( [[tables objectAtIndex:[tablesListView selectedRow]] isEqualToString:@""] ) { + //user added new table and then pressed escape + [tables removeObjectAtIndex:[tablesListView selectedRow]]; + [tablesListView reloadData]; + } + + return TRUE; + } else{ + return FALSE; + } +} + +- (BOOL)selectionShouldChangeInTableView:(NSTableView *)aTableView +{ +/* + int row = [tablesListView editedRow]; + int column = [tablesListView editedColumn]; + NSTableColumn *tableColumn; + NSCell *cell; + + if ( row != -1 ) { + tableColumn = [[tablesListView tableColumns] objectAtIndex:column]; + cell = [tableColumn dataCellForRow:row]; + [cell endEditing:[tablesListView currentEditor]]; + } +*/ + //end editing (otherwise problems when user hits reload button) + [tableWindow endEditingFor:nil]; + if ( alertSheetOpened ) { + return NO; + } + + //we have to be sure that TableSource and TableContent have finished editing +// if ( ![tableSourceInstance addRowToDB] || ![tableContentInstance addRowToDB] ) { + if ( ![tableSourceInstance selectionShouldChangeInTableView:nil] || + ![tableContentInstance selectionShouldChangeInTableView:nil] ) { + return NO; + } else { + return YES; + } +} + +/** + * Loads a table in content or source view (if tab selected) + */ +- (void)tableViewSelectionDidChange:(NSNotification *)aNotification +{ + if ( [tablesListView numberOfSelectedRows] == 1 ) { + if ( [tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 0 ) { + [tableSourceInstance loadTable:[tables objectAtIndex:[tablesListView selectedRow]]]; + structureLoaded = YES; + contentLoaded = NO; + statusLoaded = NO; + } else if ( [tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 1 ) { + [tableSourceInstance loadTable:[tables objectAtIndex:[tablesListView selectedRow]]]; + [tableContentInstance loadTable:[tables objectAtIndex:[tablesListView selectedRow]]]; + structureLoaded = YES; + contentLoaded = YES; + statusLoaded = NO; + } else if ( [tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 3 ) { + [tableStatusInstance loadTable:[tables objectAtIndex:[tablesListView selectedRow]]]; + structureLoaded = NO; + contentLoaded = NO; + statusLoaded = YES; + } else { + structureLoaded = NO; + contentLoaded = NO; + statusLoaded = NO; + } + + // set window title + [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@@%@/%@/%@", [tableDocumentInstance mySQLVersion], [tableDocumentInstance user], + [tableDocumentInstance host], [tableDocumentInstance database], [tables objectAtIndex:[tablesListView selectedRow]]]]; + + // Update connection characater set encoding based on the table's encoding if required + if ([[[NSUserDefaults standardUserDefaults] objectForKey:@"encoding"] isEqualToString:@"Autodetect"]) { + [tableDocumentInstance detectTableEncodingForTable:[tables objectAtIndex:[tablesListView selectedRow]]]; + } + } else { + [tableSourceInstance loadTable:nil]; + [tableContentInstance loadTable:nil]; + [tableStatusInstance loadTable:nil]; + structureLoaded = NO; + contentLoaded = NO; + statusLoaded = NO; + + // set window title + [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@@%@/%@", [tableDocumentInstance mySQLVersion], [tableDocumentInstance user], + [tableDocumentInstance host], [tableDocumentInstance database]]]; + } +} + +- (BOOL)tableView:(NSTableView *)aTableView shouldSelectRow:(int)rowIndex +{ + return (rowIndex != 0); +} + + +- (BOOL)tableView:(NSTableView *)aTableView isGroupRow:(int)row +{ + return (row == 0); +} + +- (void)tableView:(NSTableView *)aTableView + willDisplayCell:(id)aCell + forTableColumn:(NSTableColumn *)aTableColumn + row:(int)rowIndex +{ + if (rowIndex > 0 && [[aTableColumn identifier] isEqualToString:@"tables"]) { + [(ImageAndTextCell*)aCell setImage:[NSImage imageNamed:@"table-small"]]; + [(ImageAndTextCell*)aCell setIndentationLevel:1]; + if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"useMonospacedFonts"] ) { + [(ImageAndTextCell*)aCell setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]]; + } + else + { + [(ImageAndTextCell*)aCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + } + + } else { + [(ImageAndTextCell*)aCell setImage:nil]; + [(ImageAndTextCell*)aCell setIndentationLevel:0]; + } +} + +- (float)tableView:(NSTableView *)tableView heightOfRow:(int)row +{ + if (row == 0) { + return 25; + } else { + return 17; + } +} + +#pragma mark - +#pragma mark TabView delegate methods + +/* +loads structure or source if tab selected the first time +*/ +- (void)tabView:(NSTabView *)aTabView didSelectTabViewItem:(NSTabViewItem *)tabViewItem +{ + if ( [tablesListView numberOfSelectedRows] == 1 ) { + + if ( ([tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 0) && !structureLoaded ) { + + [tableSourceInstance loadTable:[tables objectAtIndex:[tablesListView selectedRow]]]; + structureLoaded = YES; + } + + if ( ([tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 1) && !contentLoaded ) { + + if ( !structureLoaded ) { + [tableSourceInstance loadTable:[tables objectAtIndex:[tablesListView selectedRow]]]; + structureLoaded = YES; + } + + [tableContentInstance loadTable:[tables objectAtIndex:[tablesListView selectedRow]]]; + contentLoaded = YES; + } + + if ( ([tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 3) && !statusLoaded ) { + [tableStatusInstance loadTable:[tables objectAtIndex:[tablesListView selectedRow]]]; + statusLoaded = YES; + } + } +/* + if ( [tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 3 ) { + [tableDumpInstance reloadTables:self]; + } +*/ +} + +#pragma mark - +//last but not least +- (id)init +{ + self = [super init]; + + tables = [[NSMutableArray alloc] init]; + structureLoaded = NO; + contentLoaded = NO; + [tables addObject:NSLocalizedString(@"TABLES",@"header for table list")]; + return self; +} + +- (void)dealloc +{ +// NSLog(@"TableList dealloc"); + + [tables release]; + + [super dealloc]; +} + + +@end diff --git a/Source/main.m b/Source/main.m new file mode 100644 index 00000000..593bb3ac --- /dev/null +++ b/Source/main.m @@ -0,0 +1,6 @@ +#import <Cocoa/Cocoa.h> + +int main(int argc, char *argv[]) +{ + return NSApplicationMain(argc, (const char **) argv); +} diff --git a/Source/sequel-pro_Prefix.pch b/Source/sequel-pro_Prefix.pch new file mode 100644 index 00000000..8df1011d --- /dev/null +++ b/Source/sequel-pro_Prefix.pch @@ -0,0 +1,7 @@ +// +// Prefix header for all source files of the 'sequel-pro' target in the 'sequel-pro' project +// + +#ifdef __OBJC__ + #import <Cocoa/Cocoa.h> +#endif |