aboutsummaryrefslogtreecommitdiffstats
path: root/Source
diff options
context:
space:
mode:
authorrowanbeentje <rowan@beent.je>2009-04-19 14:34:30 +0000
committerrowanbeentje <rowan@beent.je>2009-04-19 14:34:30 +0000
commitd4dd7e79ce8373fe94521da2294a076887758ee2 (patch)
tree85d6828f2131b3e14afee0bcb0d88d09cb23fa1e /Source
parent39adddc081ea010756886ce5819ec4565469d893 (diff)
downloadsequelpro-d4dd7e79ce8373fe94521da2294a076887758ee2.tar.gz
sequelpro-d4dd7e79ce8373fe94521da2294a076887758ee2.tar.bz2
sequelpro-d4dd7e79ce8373fe94521da2294a076887758ee2.zip
Bring Tiger branch up to version 0.9.5:
- Merge in revisions up to r592 from trunk - Rewrite code where appropriate to use Tiger-compaible methods (no easy object enumeriation, no @properties, etc) - Remove printing again - problems printing, and template engine is 10.5-only - Rework xibs as nibs, and ensure everything looks and works correctly on Tiger; revert interface elements where necessary. - Add a method to check whether the app is being run on 10.5+, and show appropriate warning about interface and features - Alter strings and change sparkle URL to Tiger-specific appcast
Diffstat (limited to 'Source')
-rw-r--r--Source/CMMCPConnection.h13
-rw-r--r--Source/CMMCPConnection.m117
-rw-r--r--Source/CMTextView.h26
-rw-r--r--Source/CMTextView.m1190
-rw-r--r--Source/CustomQuery.h22
-rw-r--r--Source/CustomQuery.m642
-rw-r--r--Source/MainController.h55
-rw-r--r--Source/MainController.m779
-rw-r--r--Source/NoodleLineNumberView.h60
-rw-r--r--Source/NoodleLineNumberView.m493
-rw-r--r--Source/SPArrayAdditions.h29
-rw-r--r--Source/SPArrayAdditions.m45
-rw-r--r--Source/SPConsoleMessage.h44
-rw-r--r--Source/SPConsoleMessage.m84
-rw-r--r--Source/SPEditorTokens.h19
-rw-r--r--Source/SPEditorTokens.l666
-rw-r--r--Source/SPExportController.h91
-rw-r--r--Source/SPExportController.m178
-rw-r--r--Source/SPFavoriteTextFieldCell.h44
-rw-r--r--Source/SPFavoriteTextFieldCell.m239
-rw-r--r--Source/SPGrowlController.h1
-rw-r--r--Source/SPGrowlController.m71
-rw-r--r--Source/SPPreferenceController.h84
-rw-r--r--Source/SPPreferenceController.m842
-rw-r--r--Source/SPQueryConsole.h24
-rw-r--r--Source/SPQueryConsole.m510
-rw-r--r--Source/SPSQLParser.h18
-rw-r--r--Source/SPSQLParser.m116
-rw-r--r--Source/SPStringAdditions.h4
-rw-r--r--Source/SPStringAdditions.m177
-rw-r--r--Source/SPTableData.h1
-rw-r--r--Source/SPTableData.m120
-rw-r--r--Source/SPTableInfo.m2
-rw-r--r--Source/SPTextViewAdditions.h39
-rw-r--r--Source/SPTextViewAdditions.m287
-rw-r--r--Source/SPWindowAdditions.h30
-rw-r--r--Source/SPWindowAdditions.m75
-rw-r--r--Source/TableContent.h8
-rw-r--r--Source/TableContent.m414
-rw-r--r--Source/TableDocument.h47
-rw-r--r--Source/TableDocument.m800
-rw-r--r--Source/TableDump.h16
-rw-r--r--Source/TableDump.m313
-rw-r--r--Source/TableSource.m137
-rw-r--r--Source/TableStatus.h5
-rw-r--r--Source/TableStatus.m45
-rw-r--r--Source/TablesList.h39
-rw-r--r--Source/TablesList.m588
48 files changed, 7573 insertions, 2076 deletions
diff --git a/Source/CMMCPConnection.h b/Source/CMMCPConnection.h
index 584b8056..8473c962 100644
--- a/Source/CMMCPConnection.h
+++ b/Source/CMMCPConnection.h
@@ -26,15 +26,11 @@
#import <MCPKit_bundled/MCPKit_bundled.h>
#import "CMMCPResult.h"
-// Set the connection timeout to enforce for all connections - used for the initial connection
-// timeout and ping timeouts, but not for long queries/reads/writes.
-// Probably worth moving this to a preference at some point.
-#define SP_CONNECTION_TIMEOUT 10
-
@interface NSObject (CMMCPConnectionDelegate)
- (void)willQueryString:(NSString *)query;
- (void)queryGaveError:(NSString *)error;
+- (BOOL)connectionEncodingViaLatin1;
@end
@@ -49,6 +45,10 @@
NSString *connectionHost;
int connectionPort;
NSString *connectionSocket;
+ float lastQueryExecutionTime;
+ int connectionTimeout;
+ BOOL useKeepAlive;
+ float keepAliveInterval;
NSTimer *keepAliveTimer;
NSDate *lastKeepAliveSuccess;
@@ -66,6 +66,8 @@
- (void) setParentWindow:(NSWindow *)theWindow;
- (BOOL) selectDB:(NSString *) dbName;
- (CMMCPResult *) queryString:(NSString *) query;
+- (CMMCPResult *) queryString:(NSString *) query usingEncoding:(NSStringEncoding) encoding;
+- (float) lastQueryExecutionTime;
- (MCPResult *) listDBsLike:(NSString *) dbsName;
- (BOOL) checkConnection;
- (void) setDelegate:(id)object;
@@ -75,5 +77,6 @@
- (void) stopKeepAliveTimer;
- (void) keepAlive:(NSTimer *)theTimer;
- (void) threadedKeepAlive;
+- (const char *) cStringFromString:(NSString *) theString usingEncoding:(NSStringEncoding) encoding;
@end
diff --git a/Source/CMMCPConnection.m b/Source/CMMCPConnection.m
index 14cd6ba7..99b72e42 100644
--- a/Source/CMMCPConnection.m
+++ b/Source/CMMCPConnection.m
@@ -67,8 +67,16 @@ static void forcePingTimeout(int signalNumber);
connectionPort = 0;
connectionSocket = nil;
keepAliveTimer = nil;
+ connectionTimeout = [[[NSUserDefaults standardUserDefaults] objectForKey:@"ConnectionTimeout"] intValue];
+ if (!connectionTimeout) connectionTimeout = 10;
+ useKeepAlive = [[[NSUserDefaults standardUserDefaults] objectForKey:@"UseKeepAlive"] doubleValue];
+ keepAliveInterval = [[[NSUserDefaults standardUserDefaults] objectForKey:@"KeepAliveInterval"] doubleValue];
+ if (!keepAliveInterval) keepAliveInterval = 0;
lastKeepAliveSuccess = nil;
- [NSBundle loadNibNamed:@"ConnectionErrorDialog" owner:self];
+ lastQueryExecutionTime = 0;
+ if (![NSBundle loadNibNamed:@"ConnectionErrorDialog" owner:self]) {
+ NSLog(@"Connection error dialog could not be loaded; connection failure handling will not function correctly.");
+ }
}
@@ -90,7 +98,6 @@ static void forcePingTimeout(int signalNumber);
if (socket) connectionSocket = [[NSString alloc] initWithString:socket];
if (mConnection != NULL) {
- unsigned int connectionTimeout = SP_CONNECTION_TIMEOUT;
mysql_options(mConnection, MYSQL_OPT_CONNECT_TIMEOUT, (const void *)&connectionTimeout);
}
@@ -130,6 +137,7 @@ static void forcePingTimeout(int signalNumber);
- (BOOL) reconnect
{
NSString *currentEncoding = nil;
+ BOOL currentEncodingUsesLatin1Transport = NO;
NSString *currentDatabase = nil;
// Store the current database and encoding so they can be re-set if reconnection was successful
@@ -139,6 +147,9 @@ static void forcePingTimeout(int signalNumber);
if (delegate && [delegate valueForKey:@"_encoding"]) {
currentEncoding = [NSString stringWithString:[delegate valueForKey:@"_encoding"]];
}
+ if (delegate && [delegate respondsToSelector:@selector(connectionEncodingViaLatin1)]) {
+ currentEncodingUsesLatin1Transport = [delegate connectionEncodingViaLatin1];
+ }
// Close the connection if it exists.
if (mConnected) {
@@ -155,7 +166,6 @@ static void forcePingTimeout(int signalNumber);
if (mConnection != NULL) {
// Set a connection timeout for the new connection
- unsigned int connectionTimeout = SP_CONNECTION_TIMEOUT;
mysql_options(mConnection, MYSQL_OPT_CONNECT_TIMEOUT, (const void *)&connectionTimeout);
// Attempt to reestablish the connection - using own method so everything gets set up as standard.
@@ -169,8 +179,11 @@ static void forcePingTimeout(int signalNumber);
[self selectDB:currentDatabase];
}
if (currentEncoding) {
- [self queryString:[NSString stringWithFormat:@"SET NAMES '%@'", currentEncoding]];
+ [self queryString:[NSString stringWithFormat:@"/*!40101 SET NAMES '%@' */", currentEncoding]];
[self setEncoding:[CMMCPConnection encodingForMySQLEncoding:[currentEncoding UTF8String]]];
+ if (currentEncodingUsesLatin1Transport) {
+ [self queryString:@"/*!40101 SET CHARACTER_SET_RESULTS=latin1 */"];
+ }
}
} else if (parentWindow) {
@@ -333,22 +346,36 @@ static void forcePingTimeout(int signalNumber);
/*
+ * Override the standard queryString: method to default to the connection encoding, as before,
+ * before pssing on to queryString: usingEncoding:.
+ */
+- (CMMCPResult *)queryString:(NSString *) query
+{
+ return [self queryString:query usingEncoding:mEncoding];
+}
+
+
+/*
* Modified version of queryString to be used in Sequel Pro.
* Error checks extensively - if this method fails, it will ask how to proceed and loop depending
* on the status, not returning control until either the query has been executed and the result can
* be returned or the connection and document have been closed.
*/
-- (CMMCPResult *)queryString:(NSString *) query
+- (CMMCPResult *)queryString:(NSString *) query usingEncoding:(NSStringEncoding) encoding
{
CMMCPResult *theResult;
- const char *theCQuery = [self cStringFromString:query];
+ const char *theCQuery;
int theQueryCode;
+ NSDate *queryStartDate;
// If no connection is present, return nil.
if (!mConnected) return nil;
[self stopKeepAliveTimer];
+ // Generate the cString as appropriate
+ theCQuery = [self cStringFromString:query usingEncoding:encoding];
+
// Check the connection. This triggers reconnects as necessary, and should only return false if a disconnection
// has been requested - in which case return nil
if (![self checkConnection]) return nil;
@@ -358,10 +385,16 @@ static void forcePingTimeout(int signalNumber);
[delegate willQueryString:query];
}
- if (0 == (theQueryCode = mysql_query(mConnection, theCQuery))) {
+ // Run the query, storing run time (note this will include some network and overhead)
+ queryStartDate = [NSDate date];
+ theQueryCode = mysql_query(mConnection, theCQuery);
+ lastQueryExecutionTime = [[NSDate date] timeIntervalSinceDate:queryStartDate];
+
+ // Retrieve the result or error appropriately.
+ if (0 == theQueryCode) {
if (mysql_field_count(mConnection) != 0) {
- // Use CMMCPResult instad of MCPResult
+ // Use CMMCPResult instead of MCPResult
theResult = [[CMMCPResult alloc] initWithMySQLPtr:mConnection encoding:mEncoding timeZone:mTimeZone];
} else {
return nil;
@@ -383,6 +416,16 @@ static void forcePingTimeout(int signalNumber);
/*
+ * Return the time taken to execute the last query. This should be close to the time it took
+ * the server to run the query, but will include network lag and some client library overhead.
+ */
+- (float) lastQueryExecutionTime
+{
+ return lastQueryExecutionTime;
+}
+
+
+/*
* Modified version of selectDB to be used in Sequel Pro.
* Checks the connection exists, and handles keepalive, otherwise calling the parent implementation.
*/
@@ -402,10 +445,15 @@ static void forcePingTimeout(int signalNumber);
*/
- (BOOL)checkConnection
{
+ unsigned long threadid;
+
if (!mConnected) return NO;
BOOL connectionVerified = FALSE;
+ // Get the current thread ID for this connection
+ threadid = mConnection->thread_id;
+
// Check whether the connection is still operational via a wrapped version of MySQL ping.
connectionVerified = [self pingConnection];
@@ -432,6 +480,16 @@ static void forcePingTimeout(int signalNumber);
default:
return [self checkConnection];
}
+
+ // If a connection exists, check whether the thread id differs; if so, the connection has
+ // probably been reestablished and we need to reset the connection encoding
+ } else if (threadid != mConnection->thread_id) {
+ if (delegate && [delegate valueForKey:@"_encoding"]) {
+ [self queryString:[NSString stringWithFormat:@"/*!40101 SET NAMES '%@' */", [NSString stringWithString:[delegate valueForKey:@"_encoding"]]]];
+ if (delegate && [delegate respondsToSelector:@selector(connectionEncodingViaLatin1)]) {
+ if ([delegate connectionEncodingViaLatin1]) [self queryString:@"/*!40101 SET CHARACTER_SET_RESULTS=latin1 */"];
+ }
+ }
}
return connectionVerified;
@@ -522,7 +580,7 @@ static void forcePingTimeout(int signalNumber);
sigemptyset(&timeoutAction.sa_mask);
timeoutAction.sa_flags = 0;
sigaction(SIGALRM, &timeoutAction, NULL);
- alarm(SP_CONNECTION_TIMEOUT+1);
+ alarm(connectionTimeout+1);
// Set up a "restore point", returning 0; if longjmp is used later with this reference, execution
// jumps back to this point and returns a nonzero value, so this function evaluates to false when initially
@@ -579,14 +637,16 @@ static void forcePingTimeout(int signalNumber)
[lastKeepAliveSuccess release];
lastKeepAliveSuccess = nil;
}
-
- keepAliveTimer = [NSTimer
- scheduledTimerWithTimeInterval:[[[NSUserDefaults standardUserDefaults] objectForKey:@"keepAliveInterval"] doubleValue]
- target:self
- selector:@selector(keepAlive:)
- userInfo:nil
- repeats:NO];
- [keepAliveTimer retain];
+
+ if (useKeepAlive && keepAliveInterval) {
+ keepAliveTimer = [NSTimer
+ scheduledTimerWithTimeInterval:keepAliveInterval
+ target:self
+ selector:@selector(keepAlive:)
+ userInfo:nil
+ repeats:NO];
+ [keepAliveTimer retain];
+ }
}
/*
@@ -612,7 +672,7 @@ static void forcePingTimeout(int signalNumber)
// cut but mysql doesn't pick up on the fact - see comment for pingConnection above. The same
// forced-timeout approach cannot be used here on a background thread.
// When the connection is disconnected in code, these 5 "hanging" threads are automatically cleaned.
- if (lastKeepAliveSuccess && [lastKeepAliveSuccess timeIntervalSinceNow] < -5 * [[[NSUserDefaults standardUserDefaults] objectForKey:@"keepAliveInterval"] doubleValue]) return;
+ if (lastKeepAliveSuccess && [lastKeepAliveSuccess timeIntervalSinceNow] < -5 * keepAliveInterval) return;
[NSThread detachNewThreadSelector:@selector(threadedKeepAlive) toTarget:self withObject:nil];
[self startKeepAliveTimerResettingState:NO];
@@ -631,4 +691,23 @@ static void forcePingTimeout(int signalNumber)
}
lastKeepAliveSuccess = [[NSDate alloc] initWithTimeIntervalSinceNow:0];
}
-@end
+
+
+/*
+ * Modified version of the original to support a supplied encoding.
+ * For internal use only. Transforms a NSString to a C type string (ending with \0).
+ * Lossy conversions are enabled.
+ */
+- (const char *) cStringFromString:(NSString *) theString usingEncoding:(NSStringEncoding) encoding
+{
+ NSMutableData *theData;
+
+ if (! theString) {
+ return (const char *)NULL;
+ }
+
+ theData = [NSMutableData dataWithData:[theString dataUsingEncoding:encoding allowLossyConversion:YES]];
+ [theData increaseLengthBy:1];
+ return (const char *)[theData bytes];
+}
+@end \ No newline at end of file
diff --git a/Source/CMTextView.h b/Source/CMTextView.h
index e70c6ea4..8f32ff8e 100644
--- a/Source/CMTextView.h
+++ b/Source/CMTextView.h
@@ -22,11 +22,33 @@
// Or mail to <lorenz@textor.ch>
#import <Cocoa/Cocoa.h>
+#import "NoodleLineNumberView.h"
@interface CMTextView : NSTextView {
+ BOOL autoindentEnabled;
+ BOOL autopairEnabled;
+ BOOL autoindentIgnoresEnter;
+ BOOL autouppercaseKeywordsEnabled;
+ BOOL delBackwardsWasPressed;
+ NoodleLineNumberView *lineNumberView;
+
+ IBOutlet NSScrollView *scrollView;
}
--(NSArray *)completionsForPartialWordRange:(NSRange)charRange indexOfSelectedItem:(int *)index;
--(NSArray *)keywords;
+- (BOOL) isNextCharMarkedBy:(id)attribute;
+- (BOOL) areAdjacentCharsLinked;
+- (BOOL) wrapSelectionWithPrefix:(NSString *)prefix suffix:(NSString *)suffix;
+- (BOOL) shiftSelectionRight;
+- (BOOL) shiftSelectionLeft;
+- (NSArray *) completionsForPartialWordRange:(NSRange)charRange indexOfSelectedItem:(int *)index;
+- (NSArray *) keywords;
+- (void) setAutoindent:(BOOL)enableAutoindent;
+- (BOOL) autoindent;
+- (void) setAutoindentIgnoresEnter:(BOOL)enableAutoindentIgnoresEnter;
+- (BOOL) autoindentIgnoresEnter;
+- (void) setAutopair:(BOOL)enableAutopair;
+- (BOOL) autopair;
+- (void) setAutouppercaseKeywords:(BOOL)enableAutouppercaseKeywords;
+- (BOOL) autouppercaseKeywords;
@end
diff --git a/Source/CMTextView.m b/Source/CMTextView.m
index 7ca2f782..427c66d1 100644
--- a/Source/CMTextView.m
+++ b/Source/CMTextView.m
@@ -24,15 +24,481 @@
#import "CMTextView.h"
#import "SPStringAdditions.h"
+/*
+ * Include all the extern variables and prototypes required for flex (used for syntax highlighting)
+ */
+#import "SPEditorTokens.h"
+extern int yylex();
+extern int yyuoffset, yyuleng;
+typedef struct yy_buffer_state *YY_BUFFER_STATE;
+void yy_switch_to_buffer(YY_BUFFER_STATE);
+YY_BUFFER_STATE yy_scan_string (const char *);
+
+#define kAPlinked @"Linked" // attribute for a via auto-pair inserted char
+#define kAPval @"linked"
+#define kWQquoted @"Quoted" // set via lex to indicate a quoted string
+#define kWQval @"quoted"
+#define kSQLkeyword @"SQLkw" // attribute for found SQL keywords
+#define kQuote @"Quote"
+
+
@implementation CMTextView
+/*
+ * Checks if the char after the current caret position/selection matches a supplied attribute
+ */
+- (BOOL) isNextCharMarkedBy:(id)attribute
+{
+ unsigned int caretPosition = [self selectedRange].location;
+
+ // Perform bounds checking
+ if (caretPosition >= [[self string] length]) return NO;
+
+ // Perform the check
+ if ([[self textStorage] attribute:attribute atIndex:caretPosition effectiveRange:nil])
+ return YES;
+
+ return NO;
+}
+
+
+/*
+ * Checks if the caret is wrapped by auto-paired characters.
+ * e.g. [| := caret]: "|"
+ */
+- (BOOL) areAdjacentCharsLinked
+{
+ unsigned int caretPosition = [self selectedRange].location;
+ unichar leftChar, matchingChar;
+
+ // Perform bounds checking
+ if ([self selectedRange].length) return NO;
+ if (caretPosition < 1) return NO;
+ if (caretPosition >= [[self string] length]) return NO;
+
+ // Check the character to the left of the cursor and set the pairing character if appropriate
+ leftChar = [[self string] characterAtIndex:caretPosition - 1];
+ if (leftChar == '(')
+ matchingChar = ')';
+ else if (leftChar == '"' || leftChar == '`' || leftChar == '\'')
+ matchingChar = leftChar;
+ else
+ return NO;
+
+ // Check that the pairing character exists after the caret, and is tagged with the link attribute
+ if (matchingChar == [[self string] characterAtIndex:caretPosition]
+ && [[self textStorage] attribute:kAPlinked atIndex:caretPosition effectiveRange:nil]) {
+ return YES;
+ }
+
+ return NO;
+}
+
+
+/*
+ * If the textview has a selection, wrap it with the supplied prefix and suffix strings;
+ * return whether or not any wrap was performed.
+ */
+- (BOOL) wrapSelectionWithPrefix:(NSString *)prefix suffix:(NSString *)suffix
+{
+
+ // Only proceed if a selection is active
+ if ([self selectedRange].length == 0)
+ return NO;
+
+ // Replace the current selection with the selected string wrapped in prefix and suffix
+ [self insertText:
+ [NSString stringWithFormat:@"%@%@%@",
+ prefix,
+ [[self string] substringWithRange:[self selectedRange]],
+ suffix
+ ]
+ ];
+ return YES;
+}
+
+/*
+ * Copy selected text chunk as RTF to preserve syntax highlighting
+ */
+- (void)copyAsRTF
+{
+
+ NSPasteboard *pb = [NSPasteboard generalPasteboard];
+ NSTextStorage *textStorage = [self textStorage];
+ NSData *rtf = [textStorage RTFFromRange:[self selectedRange]
+ documentAttributes:nil];
+
+ if (rtf)
+ {
+ [pb declareTypes:[NSArray arrayWithObject:NSRTFPboardType] owner:self];
+ [pb setData:rtf forType:NSRTFPboardType];
+ }
+
+}
+
+
+/*
+ * Handle some keyDown events in order to provide autopairing functionality (if enabled).
+ */
+- (void) keyDown:(NSEvent *)theEvent
+{
+
+ long allFlags = (NSShiftKeyMask|NSControlKeyMask|NSAlternateKeyMask|NSCommandKeyMask);
+
+ // Check if user pressed ⌥ to allow composing of accented characters.
+ // e.g. for US keyboard "⌥u a" to insert ä
+ // or for non-US keyboards to allow to enter dead keys
+ // e.g. for German keyboard ` is a dead key, press space to enter `
+ if (([theEvent modifierFlags] & allFlags) == NSAlternateKeyMask || [[theEvent characters] length] == 0)
+ {
+ [super keyDown: theEvent];
+ return;
+ }
+
+ NSString *characters = [theEvent characters];
+ NSString *charactersIgnMod = [theEvent charactersIgnoringModifiers];
+ unichar insertedCharacter = [characters characterAtIndex:0];
+ long curFlags = ([theEvent modifierFlags] & allFlags);
+
+
+ // Note: switch(insertedCharacter) {} does not work instead use charactersIgnoringModifiers
+ if([charactersIgnMod isEqualToString:@"c"]) // ^C copy as RTF
+ if(curFlags==(NSControlKeyMask))
+ {
+ [self copyAsRTF];
+ return;
+ }
+
+ // Only process for character autopairing if autopairing is enabled and a single character is being added.
+ if (autopairEnabled && characters && [characters length] == 1) {
+
+ delBackwardsWasPressed = NO;
+
+ NSString *matchingCharacter = nil;
+ BOOL processAutopair = NO, skipTypedLinkedCharacter = NO;
+ NSRange currentRange;
+
+ // When a quote character is being inserted into a string quoted with other
+ // quote characters, or if it's the same character but is escaped, don't
+ // automatically match it.
+ if(
+ // Only for " ` or ' quote characters
+ (insertedCharacter == '\'' || insertedCharacter == '"' || insertedCharacter == '`')
+
+ // And if the next char marked as linked auto-pair
+ && [self isNextCharMarkedBy:kAPlinked]
+
+ // And we are inside a quoted string
+ && [self isNextCharMarkedBy:kWQquoted]
+
+ // And there is no selection, just the text caret
+ && ![self selectedRange].length
+
+ && (
+ // And the user is inserting an escaped string
+ [[self string] characterAtIndex:[self selectedRange].location-1] == '\\'
+
+ // Or the user is inserting a character not matching the characters used to quote this string
+ || [[self string] characterAtIndex:[self selectedRange].location] != insertedCharacter
+ )
+ )
+ {
+ [super keyDown: theEvent];
+ return;
+ }
+
+ // If the caret is inside a text string, without any selection, skip autopairing.
+ // There is one exception to this - if the caret is before a linked pair character,
+ // processing continues in order to check whether the next character should be jumped
+ // over; e.g. [| := caret]: "foo|" and press " => only caret will be moved "foo"|
+ if(![self isNextCharMarkedBy:kAPlinked] && [self isNextCharMarkedBy:kWQquoted] && ![self selectedRange].length) {
+ [super keyDown:theEvent];
+ return;
+ }
+
+ // Check whether the submitted character should trigger autopair processing.
+ switch (insertedCharacter)
+ {
+ case '(':
+ matchingCharacter = @")";
+ processAutopair = YES;
+ break;
+ case '"':
+ matchingCharacter = @"\"";
+ processAutopair = YES;
+ skipTypedLinkedCharacter = YES;
+ break;
+ case '`':
+ matchingCharacter = @"`";
+ processAutopair = YES;
+ skipTypedLinkedCharacter = YES;
+ break;
+ case '\'':
+ matchingCharacter = @"'";
+ processAutopair = YES;
+ skipTypedLinkedCharacter = YES;
+ break;
+ case ')':
+ skipTypedLinkedCharacter = YES;
+ break;
+ }
+
+ // Check to see whether the next character should be compared to the typed character;
+ // if it matches the typed character, and is marked with the is-linked-pair attribute,
+ // select the next character and replace it with the typed character. This allows
+ // a normally quoted string to be typed in full, with the autopair appearing as a hint and
+ // then being automatically replaced when the user types it.
+ if (skipTypedLinkedCharacter) {
+ currentRange = [self selectedRange];
+ if (currentRange.location != NSNotFound && currentRange.length == 0) {
+ if ([self isNextCharMarkedBy:kAPlinked]) {
+ if ([[[self textStorage] string] characterAtIndex:currentRange.location] == insertedCharacter) {
+ currentRange.length = 1;
+ [self setSelectedRange:currentRange];
+ processAutopair = NO;
+ }
+ }
+ }
+ }
+
+ // If an appropriate character has been typed, and a matching character has been set,
+ // some form of autopairing is required.
+ if (processAutopair && matchingCharacter) {
+
+ // Check to see whether several characters are selected, and if so, wrap them with
+ // the auto-paired characters. This returns false if the selection has zero length.
+ if ([self wrapSelectionWithPrefix:characters suffix:matchingCharacter])
+ return;
+
+ // Otherwise, start by inserting the original character - the first half of the autopair.
+ [super keyDown:theEvent];
+
+ // Then process the second half of the autopair - the matching character.
+ currentRange = [self selectedRange];
+ if (currentRange.location != NSNotFound) {
+ NSTextStorage *textStorage = [self textStorage];
+
+ // Register the auto-pairing for undo
+ [self shouldChangeTextInRange:currentRange replacementString:matchingCharacter];
+
+ // Insert the matching character and give it the is-linked-pair-character attribute
+ [self replaceCharactersInRange:currentRange withString:matchingCharacter];
+ currentRange.length = 1;
+ [textStorage addAttribute:kAPlinked value:kAPval range:currentRange];
+
+ // Restore the original selection.
+ currentRange.length=0;
+ [self setSelectedRange:currentRange];
+ }
+ return;
+ }
+ }
+
+ // The default action is to perform the normal key-down action.
+ [super keyDown:theEvent];
+
+}
+
+
+- (void) deleteBackward:(id)sender
+{
+
+ // If the caret is currently inside a marked auto-pair, delete the characters on both sides
+ // of the caret.
+ NSRange currentRange = [self selectedRange];
+ if (currentRange.length == 0 && currentRange.location > 0 && [self areAdjacentCharsLinked])
+ [self setSelectedRange:NSMakeRange(currentRange.location - 1,2)];
+
+ // Avoid auto-uppercasing if resulting word would be a SQL keyword;
+ // e.g. type inta| and deleteBackward:
+ delBackwardsWasPressed = YES;
+
+ [super deleteBackward:sender];
+
+}
+
+
+/*
+ * Handle special commands - see NSResponder.h for a sample list.
+ * This subclass currently handles insertNewline: in order to preserve indentation
+ * when adding newlines.
+ */
+- (void) doCommandBySelector:(SEL)aSelector
+{
+
+ // Handle newlines, adding any indentation found on the current line to the new line - ignoring the enter key if appropriate
+ if (aSelector == @selector(insertNewline:)
+ && autoindentEnabled
+ && (!autoindentIgnoresEnter || [[NSApp currentEvent] keyCode] != 0x4C))
+ {
+ NSString *textViewString = [[self textStorage] string];
+ NSString *currentLine, *indentString = nil;
+ NSScanner *whitespaceScanner;
+ NSRange currentLineRange;
+
+ // Extract the current line based on the text caret or selection start position
+ currentLineRange = [textViewString lineRangeForRange:NSMakeRange([self selectedRange].location, 0)];
+ currentLine = [[NSString alloc] initWithString:[textViewString substringWithRange:currentLineRange]];
+
+ // Scan all indentation characters on the line into a string
+ whitespaceScanner = [[NSScanner alloc] initWithString:currentLine];
+ [whitespaceScanner setCharactersToBeSkipped:nil];
+ [whitespaceScanner scanCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:&indentString];
+ [whitespaceScanner release];
+ [currentLine release];
+
+ // Always add the newline, whether or not we want to indent the next line
+ [self insertNewline:self];
+
+ // Replicate the indentation on the previous line if one was found.
+ if (indentString) [self insertText:indentString];
+
+ // Return to avoid the original implementation, preventing double linebreaks
+ return;
+ }
+ [super doCommandBySelector:aSelector];
+}
+
+
+/*
+ * Shifts the selection, if any, rightwards by indenting any selected lines with one tab.
+ * If the caret is within a line, the selection is not changed after the index; if the selection
+ * has length, all lines crossed by the length are indented and fully selected.
+ * Returns whether or not an indentation was performed.
+ */
+- (BOOL) shiftSelectionRight
+{
+ NSString *textViewString = [[self textStorage] string];
+ NSRange currentLineRange;
+ NSArray *lineRanges;
+ NSString *tabString = @"\t";
+ int i, indentedLinesLength = 0;
+
+ if ([self selectedRange].location == NSNotFound) return NO;
+
+ // Indent the currently selected line if the caret is within a single line
+ if ([self selectedRange].length == 0) {
+ NSRange currentLineRange;
+
+ // Extract the current line range based on the text caret
+ currentLineRange = [textViewString lineRangeForRange:[self selectedRange]];
+
+ // Register the indent for undo
+ [self shouldChangeTextInRange:NSMakeRange(currentLineRange.location, 0) replacementString:tabString];
+
+ // Insert the new tab
+ [self replaceCharactersInRange:NSMakeRange(currentLineRange.location, 0) withString:tabString];
+
+ return YES;
+ }
+
+ // Otherwise, the selection has a length - get an array of current line ranges for the specified selection
+ lineRanges = [textViewString lineRangesForRange:[self selectedRange]];
+
+ // Loop through the ranges, storing a count of the overall length.
+ for (i = 0; i < [lineRanges count]; i++) {
+ currentLineRange = NSRangeFromString([lineRanges objectAtIndex:i]);
+ indentedLinesLength += currentLineRange.length + 1;
+
+ // Register the indent for undo
+ [self shouldChangeTextInRange:NSMakeRange(currentLineRange.location+i, 0) replacementString:tabString];
+
+ // Insert the new tab
+ [self replaceCharactersInRange:NSMakeRange(currentLineRange.location+i, 0) withString:tabString];
+ }
+
+ // Select the entirety of the new range
+ [self setSelectedRange:NSMakeRange(NSRangeFromString([lineRanges objectAtIndex:0]).location, indentedLinesLength)];
+
+ return YES;
+}
+
+
+/*
+ * Shifts the selection, if any, leftwards by un-indenting any selected lines by one tab if possible.
+ * If the caret is within a line, the selection is not changed after the undent; if the selection has
+ * length, all lines crossed by the length are un-indented and fully selected.
+ * Returns whether or not an indentation was performed.
+ */
+- (BOOL) shiftSelectionLeft
+{
+ NSString *textViewString = [[self textStorage] string];
+ NSRange currentLineRange;
+ NSArray *lineRanges;
+ int i, unindentedLines = 0, unindentedLinesLength = 0;
+
+ if ([self selectedRange].location == NSNotFound) return NO;
+
+ // Undent the currently selected line if the caret is within a single line
+ if ([self selectedRange].length == 0) {
+ NSRange currentLineRange;
+
+ // Extract the current line range based on the text caret
+ currentLineRange = [textViewString lineRangeForRange:[self selectedRange]];
+
+ // Ensure that the line has length and that the first character is a tab
+ if (currentLineRange.length < 1
+ || [textViewString characterAtIndex:currentLineRange.location] != '\t')
+ return NO;
+
+ // Register the undent for undo
+ [self shouldChangeTextInRange:NSMakeRange(currentLineRange.location, 1) replacementString:@""];
+
+ // Remove the tab
+ [self replaceCharactersInRange:NSMakeRange(currentLineRange.location, 1) withString:@""];
+
+ return YES;
+ }
+
+ // Otherwise, the selection has a length - get an array of current line ranges for the specified selection
+ lineRanges = [textViewString lineRangesForRange:[self selectedRange]];
+
+ // Loop through the ranges, storing a count of the total lines changed and the new length.
+ for (i = 0; i < [lineRanges count]; i++) {
+ currentLineRange = NSRangeFromString([lineRanges objectAtIndex:i]);
+ unindentedLinesLength += currentLineRange.length;
+
+ // Ensure that the line has length and that the first character is a tab
+ if (currentLineRange.length < 1
+ || [textViewString characterAtIndex:currentLineRange.location-unindentedLines] != '\t')
+ continue;
+
+ // Register the undent for undo
+ [self shouldChangeTextInRange:NSMakeRange(currentLineRange.location-unindentedLines, 1) replacementString:@""];
+
+ // Remove the tab
+ [self replaceCharactersInRange:NSMakeRange(currentLineRange.location-unindentedLines, 1) withString:@""];
+
+ // As a line has been unindented, modify counts and lengths
+ unindentedLines++;
+ unindentedLinesLength--;
+ }
+
+ // If a change was made, select the entirety of the new range and return success
+ if (unindentedLines) {
+ [self setSelectedRange:NSMakeRange(NSRangeFromString([lineRanges objectAtIndex:0]).location, unindentedLinesLength)];
+ return YES;
+ }
+
+ return NO;
+}
+
+/*
+ * Handle autocompletion, returning a list of suggested completions for the supplied character range.
+ */
- (NSArray *)completionsForPartialWordRange:(NSRange)charRange indexOfSelectedItem:(int *)index
{
+ // Check if the caret is inside quotes "" or ''; if so
+ // return the normal word suggestion due to the spelling's settings
+ if([[self textStorage] attribute:kQuote atIndex:charRange.location effectiveRange:nil])
+ return [[NSSpellChecker sharedSpellChecker] completionsForPartialWordRange:NSMakeRange(0,charRange.length) inString:[[self string] substringWithRange:charRange] language:nil inSpellDocumentWithTag:0];
+
NSCharacterSet *separators = [NSCharacterSet characterSetWithCharactersInString:@" \t\r\n,()\"'`-!"];
- NSArray *textViewWords = [[self string] componentsSeparatedByCharactersInSet:separators];
- NSString *partialString = [[self string] substringWithRange:charRange];
+ NSArray *textViewWords = [[self string] componentsSeparatedByCharactersInSet:separators];
+ NSString *partialString = [[self string] substringWithRange:charRange];
unsigned int partialLength = [partialString length];
+
id tableNames = [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"tables"];
//unsigned int options = NSCaseInsensitiveSearch | NSAnchoredSearch;
@@ -52,254 +518,568 @@
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF beginswith[cd] %@ AND length > %d", partialString, partialLength];
NSArray *matchingCompletions = [[possibleCompletions filteredArrayUsingPredicate:predicate] sortedArrayUsingSelector:@selector(compare:)];
+
unsigned i, insindex;
insindex = 0;
- for (i = 0; i < [matchingCompletions count]; i ++)
+ for (i = 0; i < [matchingCompletions count]; i++)
{
- if ([partialString isEqualToString:[[matchingCompletions objectAtIndex:i] substringToIndex:partialLength]])
- {
- // Matches case --> Insert at beginning of completion list
- [compl insertObject:[matchingCompletions objectAtIndex:i] atIndex:insindex++];
- }
- else
- {
- // Not matching case --> Insert at end of completion list
- [compl addObject:[matchingCompletions objectAtIndex:i]];
- }
+ NSString* obj = [matchingCompletions objectAtIndex:i];
+ if(![compl containsObject:obj])
+ if ([partialString isEqualToString:[obj substringToIndex:partialLength]])
+ // Matches case --> Insert at beginning of completion list
+ [compl insertObject:obj atIndex:insindex++];
+ else
+ // Not matching case --> Insert at end of completion list
+ [compl addObject:obj];
}
-
+
return [compl autorelease];
}
+/*
+ * Hook to invoke the auto-uppercasing of SQL keywords after pasting
+ */
+- (void)paste:(id)sender
+{
+
+ [super paste:sender];
+ // Invoke the auto-uppercasing of SQL keywords via an additional trigger
+ [self insertText:@""];
+}
--(NSArray *)keywords {
+
+/*
+ * List of keywords for autocompletion. If you add a keyword here,
+ * it should also be added to the flex file SPEditorTokens.l
+ */
+-(NSArray *)keywords
+{
return [NSArray arrayWithObjects:
+ @"ACCESSIBLE",
+ @"ACTION",
@"ADD",
+ @"AFTER",
+ @"AGAINST",
+ @"AGGREGATE",
+ @"ALGORITHM",
@"ALL",
- @"ALTER TABLE",
- @"ALTER VIEW",
- @"ALTER SCHEMA",
- @"ALTER SCHEMA",
- @"ALTER FUNCTION",
+ @"ALTER",
@"ALTER COLUMN",
@"ALTER DATABASE",
+ @"ALTER EVENT",
+ @"ALTER FUNCTION",
+ @"ALTER LOGFILE GROUP",
@"ALTER PROCEDURE",
+ @"ALTER SCHEMA",
+ @"ALTER SERVER",
+ @"ALTER TABLE",
+ @"ALTER TABLESPACE",
+ @"ALTER VIEW",
@"ANALYZE",
+ @"ANALYZE TABLE",
@"AND",
+ @"ANY",
+ @"AS",
@"ASC",
+ @"ASCII",
@"ASENSITIVE",
+ @"AT",
+ @"AUTHORS",
+ @"AUTOEXTEND_SIZE",
+ @"AUTO_INCREMENT",
+ @"AVG",
+ @"AVG_ROW_LENGTH",
+ @"BACKUP",
+ @"BACKUP TABLE",
@"BEFORE",
+ @"BEGIN",
@"BETWEEN",
@"BIGINT",
@"BINARY",
+ @"BINLOG",
+ @"BIT",
@"BLOB",
+ @"BOOL",
+ @"BOOLEAN",
@"BOTH",
+ @"BTREE",
+ @"BY",
+ @"BYTE",
+ @"CACHE",
+ @"CACHE INDEX",
@"CALL",
@"CASCADE",
+ @"CASCADED",
@"CASE",
+ @"CHAIN",
@"CHANGE",
+ @"CHANGED",
@"CHAR",
@"CHARACTER",
+ @"CHARACTER SET",
+ @"CHARSET",
@"CHECK",
+ @"CHECK TABLE",
+ @"CHECKSUM",
+ @"CHECKSUM TABLE",
+ @"CIPHER",
+ @"CLIENT",
+ @"CLOSE",
+ @"COALESCE",
+ @"CODE",
@"COLLATE",
+ @"COLLATION",
@"COLUMN",
@"COLUMNS",
+ @"COLUMN_FORMAT"
+ @"COMMENT",
+ @"COMMIT",
+ @"COMMITTED",
+ @"COMPACT",
+ @"COMPLETION",
+ @"COMPRESSED",
+ @"CONCURRENT",
@"CONDITION",
@"CONNECTION",
+ @"CONSISTENT",
@"CONSTRAINT",
+ @"CONTAINS",
@"CONTINUE",
+ @"CONTRIBUTORS",
@"CONVERT",
- @"CREATE VIEW",
- @"CREATE INDEX",
- @"CREATE FUNCTION",
+ @"CREATE",
@"CREATE DATABASE",
+ @"CREATE EVENT",
+ @"CREATE FUNCTION",
+ @"CREATE INDEX",
+ @"CREATE LOGFILE GROUP",
@"CREATE PROCEDURE",
@"CREATE SCHEMA",
- @"CREATE TRIGGER",
@"CREATE TABLE",
+ @"CREATE TABLESPACE",
+ @"CREATE TRIGGER",
@"CREATE USER",
+ @"CREATE VIEW",
@"CROSS",
+ @"CUBE",
@"CURRENT_DATE",
@"CURRENT_TIME",
@"CURRENT_TIMESTAMP",
@"CURRENT_USER",
@"CURSOR",
+ @"DATA",
@"DATABASE",
@"DATABASES",
+ @"DATAFILE",
+ @"DATE",
+ @"DATETIME",
+ @"DAY",
@"DAY_HOUR",
@"DAY_MICROSECOND",
@"DAY_MINUTE",
@"DAY_SECOND",
+ @"DEALLOCATE",
+ @"DEALLOCATE PREPARE",
@"DEC",
@"DECIMAL",
@"DECLARE",
@"DEFAULT",
+ @"DEFINER",
@"DELAYED",
+ @"DELAY_KEY_WRITE",
@"DELETE",
@"DESC",
@"DESCRIBE",
+ @"DES_KEY_FILE",
@"DETERMINISTIC",
+ @"DIRECTORY",
+ @"DISABLE",
+ @"DISCARD",
+ @"DISK",
@"DISTINCT",
@"DISTINCTROW",
@"DIV",
+ @"DO",
@"DOUBLE",
- @"DROP TABLE",
- @"DROP TRIGGER",
- @"DROP VIEW",
- @"DROP SCHEMA",
- @"DROP USER",
- @"DROP PROCEDURE",
- @"DROP FUNCTION",
+ @"DROP",
+ @"DROP DATABASE",
+ @"DROP EVENT",
@"DROP FOREIGN KEY",
+ @"DROP FUNCTION",
@"DROP INDEX",
+ @"DROP LOGFILE GROUP",
@"DROP PREPARE",
@"DROP PRIMARY KEY",
- @"DROP DATABASE",
+ @"DROP PREPARE",
+ @"DROP PROCEDURE",
+ @"DROP SCHEMA",
+ @"DROP SERVER",
+ @"DROP TABLE",
+ @"DROP TABLESPACE",
+ @"DROP TRIGGER",
+ @"DROP USER",
+ @"DROP VIEW",
@"DUAL",
+ @"DUMPFILE",
+ @"DUPLICATE",
+ @"DYNAMIC",
@"EACH",
@"ELSE",
@"ELSEIF",
+ @"ENABLE",
@"ENCLOSED",
+ @"END",
+ @"ENDS",
+ @"ENGINE",
+ @"ENGINES",
+ @"ENUM",
+ @"ERRORS",
+ @"ESCAPE",
@"ESCAPED",
+ @"EVENT",
+ @"EVENTS",
+ @"EVERY",
+ @"EXECUTE",
@"EXISTS",
@"EXIT",
+ @"EXPANSION",
@"EXPLAIN",
+ @"EXTENDED",
+ @"EXTENT_SIZE",
@"FALSE",
+ @"FAST",
@"FETCH",
@"FIELDS",
+ @"FILE",
+ @"FIRST",
+ @"FIXED",
@"FLOAT",
+ @"FLOAT4",
+ @"FLOAT8",
+ @"FLUSH",
@"FOR",
@"FORCE",
@"FOREIGN KEY",
+ @"FOREIGN",
@"FOUND",
+ @"FRAC_SECOND",
@"FROM",
+ @"FULL",
@"FULLTEXT",
- @"GOTO",
+ @"FUNCTION",
+ @"GEOMETRY",
+ @"GEOMETRYCOLLECTION",
+ @"GET_FORMAT",
+ @"GLOBAL",
@"GRANT",
+ @"GRANTS",
@"GROUP",
+ @"HANDLER",
+ @"HASH",
@"HAVING",
+ @"HELP",
@"HIGH_PRIORITY",
+ @"HOSTS",
+ @"HOUR",
@"HOUR_MICROSECOND",
@"HOUR_MINUTE",
@"HOUR_SECOND",
+ @"IDENTIFIED",
+ @"IF",
@"IGNORE",
+ @"IMPORT",
+ @"IN",
@"INDEX",
+ @"INDEXES",
@"INFILE",
+ @"INITIAL_SIZE",
@"INNER",
+ @"INNOBASE",
+ @"INNODB",
@"INOUT",
@"INSENSITIVE",
@"INSERT",
+ @"INSERT_METHOD",
+ @"INSTALL",
+ @"INSTALL PLUGIN",
@"INT",
+ @"INT1",
+ @"INT2",
+ @"INT3",
+ @"INT4",
+ @"INT8",
@"INTEGER",
@"INTERVAL",
@"INTO",
+ @"INVOKER",
+ @"IO_THREAD",
+ @"IS",
+ @"ISOLATION",
+ @"ISSUER",
@"ITERATE",
@"JOIN",
@"KEY",
@"KEYS",
+ @"KEY_BLOCK_SIZE",
@"KILL",
+ @"LANGUAGE",
+ @"LAST",
@"LEADING",
@"LEAVE",
+ @"LEAVES",
@"LEFT",
+ @"LESS",
+ @"LEVEL",
@"LIKE",
@"LIMIT",
+ @"LINEAR",
@"LINES",
- @"LOAD",
+ @"LINESTRING",
+ @"LIST",
+ @"LOAD DATA",
+ @"LOAD INDEX INTO CACHE",
+ @"LOCAL",
@"LOCALTIME",
@"LOCALTIMESTAMP",
@"LOCK",
+ @"LOCK TABLES",
+ @"LOCKS",
+ @"LOGFILE",
+ @"LOGS",
@"LONG",
@"LONGBLOB",
@"LONGTEXT",
@"LOOP",
@"LOW_PRIORITY",
+ @"MASTER",
+ @"MASTER_CONNECT_RETRY",
+ @"MASTER_HOST",
+ @"MASTER_LOG_FILE",
+ @"MASTER_LOG_POS",
+ @"MASTER_PASSWORD",
+ @"MASTER_PORT",
+ @"MASTER_SERVER_ID",
+ @"MASTER_SSL",
+ @"MASTER_SSL_CA",
+ @"MASTER_SSL_CAPATH",
+ @"MASTER_SSL_CERT",
+ @"MASTER_SSL_CIPHER",
+ @"MASTER_SSL_KEY",
+ @"MASTER_USER",
@"MATCH",
+ @"MAXVALUE",
+ @"MAX_CONNECTIONS_PER_HOUR",
+ @"MAX_QUERIES_PER_HOUR",
+ @"MAX_ROWS",
+ @"MAX_SIZE",
+ @"MAX_UPDATES_PER_HOUR",
+ @"MAX_USER_CONNECTIONS",
+ @"MEDIUM",
@"MEDIUMBLOB",
@"MEDIUMINT",
@"MEDIUMTEXT",
+ @"MEMORY",
+ @"MERGE",
+ @"MICROSECOND",
@"MIDDLEINT",
+ @"MIGRATE",
+ @"MINUTE",
@"MINUTE_MICROSECOND",
@"MINUTE_SECOND",
+ @"MIN_ROWS",
@"MOD",
+ @"MODE",
+ @"MODIFIES",
+ @"MODIFY",
+ @"MONTH",
+ @"MULTILINESTRING",
+ @"MULTIPOINT",
+ @"MULTIPOLYGON",
+ @"MUTEX",
+ @"NAME",
+ @"NAMES",
+ @"NATIONAL",
@"NATURAL",
+ @"NCHAR",
+ @"NDB",
+ @"NDBCLUSTER",
+ @"NEW",
+ @"NEXT",
+ @"NO",
+ @"NODEGROUP",
+ @"NONE",
@"NOT",
+ @"NO_WAIT",
@"NO_WRITE_TO_BINLOG",
@"NULL",
@"NUMERIC",
+ @"NVARCHAR",
+ @"OFFSET",
+ @"OLD_PASSWORD",
@"ON",
+ @"ONE",
+ @"ONE_SHOT",
+ @"OPEN",
@"OPTIMIZE",
+ @"OPTIMIZE TABLE",
@"OPTION",
@"OPTIONALLY",
+ @"OPTIONS",
+ @"OR",
@"ORDER",
@"OUT",
@"OUTER",
@"OUTFILE",
+ @"PACK_KEYS",
+ @"PARSER",
+ @"PARTIAL",
+ @"PARTITION",
+ @"PARTITIONING",
+ @"PARTITIONS",
+ @"PASSWORD",
+ @"PHASE",
+ @"PLUGIN",
+ @"PLUGINS",
+ @"POINT",
+ @"POLYGON",
@"PRECISION",
+ @"PREPARE",
+ @"PRESERVE",
+ @"PREV",
@"PRIMARY",
@"PRIVILEGES",
@"PROCEDURE",
+ @"PROCESS",
+ @"PROCESSLIST",
@"PURGE",
+ @"QUARTER",
+ @"QUERY",
+ @"QUICK",
+ @"RANGE",
@"READ",
+ @"READS",
+ @"READ_ONLY",
+ @"READ_WRITE",
@"REAL",
+ @"REBUILD",
+ @"RECOVER",
+ @"REDOFILE",
+ @"REDO_BUFFER_SIZE",
+ @"REDUNDANT",
@"REFERENCES",
@"REGEXP",
+ @"RELAY_LOG_FILE",
+ @"RELAY_LOG_POS",
+ @"RELAY_THREAD",
+ @"RELEASE",
+ @"RELOAD",
+ @"REMOVE",
@"RENAME",
+ @"RENAME DATABASE",
+ @"RENAME TABLE",
+ @"REORGANIZE",
+ @"REPAIR",
+ @"REPAIR TABLE",
@"REPEAT",
+ @"REPEATABLE",
@"REPLACE",
+ @"REPLICATION",
@"REQUIRE",
+ @"RESET",
+ @"RESET MASTER",
+ @"RESTORE",
+ @"RESTORE TABLE",
@"RESTRICT",
+ @"RESUME",
@"RETURN",
+ @"RETURNS",
@"REVOKE",
@"RIGHT",
@"RLIKE",
+ @"ROLLBACK",
+ @"ROLLUP",
+ @"ROUTINE",
+ @"ROW",
+ @"ROWS",
+ @"ROW_FORMAT",
+ @"RTREE",
+ @"SAVEPOINT",
+ @"SCHEDULE",
+ @"SCHEDULER",
+ @"SCHEMA",
+ @"SCHEMAS",
+ @"SECOND",
@"SECOND_MICROSECOND",
+ @"SECURITY",
@"SELECT",
@"SENSITIVE",
@"SEPARATOR",
+ @"SERIAL",
+ @"SERIALIZABLE",
+ @"SESSION",
@"SET",
- @"SHOW PROCEDURE STATUS",
- @"SHOW PROCESSLIST",
- @"SHOW SCHEMAS",
- @"SHOW SLAVE HOSTS",
- @"SHOW PRIVILEGES",
- @"SHOW OPEN TABLES",
- @"SHOW MASTER STATUS",
- @"SHOW SLAVE STATUS",
- @"SHOW PLUGIN",
- @"SHOW STORAGE ENGINES",
- @"SHOW VARIABLES",
- @"SHOW WARNINGS",
- @"SHOW TRIGGERS",
- @"SHOW TABLES",
- @"SHOW MASTER LOGS",
- @"SHOW TABLE STATUS",
- @"SHOW TABLE TYPES",
- @"SHOW STATUS",
- @"SHOW INNODB STATUS",
+ @"SET PASSWORD",
+ @"SHARE",
+ @"SHOW",
+ @"SHOW BINARY LOGS",
+ @"SHOW BINLOG EVENTS",
+ @"SHOW CHARACTER SET",
+ @"SHOW COLLATION",
+ @"SHOW COLUMNS",
+ @"SHOW CONTRIBUTORS",
@"SHOW CREATE DATABASE",
+ @"SHOW CREATE EVENT",
@"SHOW CREATE FUNCTION",
@"SHOW CREATE PROCEDURE",
@"SHOW CREATE SCHEMA",
- @"SHOW COLUMNS",
- @"SHOW COLLATION",
- @"SHOW BINARY LOGS",
- @"SHOW BINLOG EVENTS",
- @"SHOW CHARACTER SET",
@"SHOW CREATE TABLE",
+ @"SHOW CREATE TRIGGERS",
@"SHOW CREATE VIEW",
- @"SHOW FUNCTION STATUS",
- @"SHOW GRANTS",
- @"SHOW INDEX",
- @"SHOW FIELDS",
- @"SHOW ERRORS",
@"SHOW DATABASES",
@"SHOW ENGINE",
@"SHOW ENGINES",
+ @"SHOW ERRORS",
+ @"SHOW EVENTS",
+ @"SHOW FIELDS",
+ @"SHOW FUNCTION CODE",
+ @"SHOW FUNCTION STATUS",
+ @"SHOW GRANTS",
+ @"SHOW INDEX",
+ @"SHOW INNODB STATUS",
@"SHOW KEYS",
+ @"SHOW MASTER LOGS",
+ @"SHOW MASTER STATUS",
+ @"SHOW OPEN TABLES",
+ @"SHOW PLUGINS",
+ @"SHOW PRIVILEGES",
+ @"SHOW PROCEDURE CODE",
+ @"SHOW PROCEDURE STATUS",
+ @"SHOW PROFILE",
+ @"SHOW PROFILES",
+ @"SHOW PROCESSLIST",
+ @"SHOW SCHEDULER STATUS",
+ @"SHOW SCHEMAS",
+ @"SHOW SLAVE HOSTS",
+ @"SHOW SLAVE STATUS",
+ @"SHOW STATUS",
+ @"SHOW STORAGE ENGINES",
+ @"SHOW TABLE STATUS",
+ @"SHOW TABLE TYPES",
+ @"SHOW TABLES",
+ @"SHOW TRIGGERS",
+ @"SHOW VARIABLES",
+ @"SHOW WARNINGS",
+ @"SHUTDOWN",
+ @"SIGNED",
+ @"SIMPLE",
+ @"SLAVE",
@"SMALLINT",
+ @"SNAPSHOT",
+ @"SOME",
@"SONAME",
+ @"SOUNDS",
@"SPATIAL",
@"SPECIFIC",
@"SQL",
@@ -307,47 +1087,331 @@
@"SQLSTATE",
@"SQLWARNING",
@"SQL_BIG_RESULT",
+ @"SQL_BUFFER_RESULT",
+ @"SQL_CACHE",
@"SQL_CALC_FOUND_ROWS",
+ @"SQL_NO_CACHE",
@"SQL_SMALL_RESULT",
+ @"SQL_THREAD",
+ @"SQL_TSI_DAY",
+ @"SQL_TSI_FRAC_SECOND",
+ @"SQL_TSI_HOUR",
+ @"SQL_TSI_MINUTE",
+ @"SQL_TSI_MONTH",
+ @"SQL_TSI_QUARTER",
+ @"SQL_TSI_SECOND",
+ @"SQL_TSI_WEEK",
+ @"SQL_TSI_YEAR",
@"SSL",
+ @"START",
+ @"START TRANSACTION",
@"STARTING",
+ @"STARTS",
+ @"STATUS",
+ @"STOP",
+ @"STORAGE",
@"STRAIGHT_JOIN",
+ @"STRING",
+ @"SUBJECT",
+ @"SUBPARTITION",
+ @"SUBPARTITIONS",
+ @"SUPER",
+ @"SUSPEND",
@"TABLE",
@"TABLES",
+ @"TABLESPACE",
+ @"TEMPORARY",
+ @"TEMPTABLE",
@"TERMINATED",
+ @"TEXT",
+ @"THAN",
@"THEN",
+ @"TIME",
+ @"TIMESTAMP",
+ @"TIMESTAMPADD",
+ @"TIMESTAMPDIFF",
@"TINYBLOB",
@"TINYINT",
@"TINYTEXT",
+ @"TO",
@"TRAILING",
+ @"TRANSACTION",
@"TRIGGER",
+ @"TRIGGERS",
@"TRUE",
+ @"TRUNCATE",
+ @"TYPE",
+ @"TYPES",
+ @"UNCOMMITTED",
+ @"UNDEFINED",
@"UNDO",
+ @"UNDOFILE",
+ @"UNDO_BUFFER_SIZE",
+ @"UNICODE",
+ @"UNINSTALL",
+ @"UNINSTALL PLUGIN",
@"UNION",
@"UNIQUE",
+ @"UNKNOWN",
@"UNLOCK",
+ @"UNLOCK TABLES",
@"UNSIGNED",
+ @"UNTIL",
@"UPDATE",
+ @"UPGRADE",
@"USAGE",
@"USE",
+ @"USER",
+ @"USER_RESOURCES",
+ @"USE_FRM",
@"USING",
@"UTC_DATE",
@"UTC_TIME",
@"UTC_TIMESTAMP",
+ @"VALUE",
@"VALUES",
@"VARBINARY",
@"VARCHAR",
@"VARCHARACTER",
+ @"VARIABLES",
@"VARYING",
+ @"VIEW",
+ @"WAIT",
+ @"WARNINGS",
+ @"WEEK",
@"WHEN",
@"WHERE",
@"WHILE",
@"WITH",
+ @"WORK",
@"WRITE",
+ @"X509",
+ @"XA",
@"XOR",
+ @"YEAR",
@"YEAR_MONTH",
@"ZEROFILL",
nil];
}
+
+/*
+ * Set whether this text view should apply the indentation on the current line to new lines.
+ */
+- (void)setAutoindent:(BOOL)enableAutoindent
+{
+ autoindentEnabled = enableAutoindent;
+}
+
+/*
+ * Retrieve whether this text view applies indentation on the current line to new lines.
+ */
+- (BOOL)autoindent
+{
+ return autoindentEnabled;
+}
+
+/*
+ * Set whether this text view should not autoindent when the Enter key is used, as opposed
+ * to the return key. Also catches function-return.
+ */
+- (void)setAutoindentIgnoresEnter:(BOOL)enableAutoindentIgnoresEnter
+{
+ autoindentIgnoresEnter = enableAutoindentIgnoresEnter;
+}
+
+/*
+ * Retrieve whether this text view should not autoindent when the Enter key is used.
+ */
+- (BOOL)autoindentIgnoresEnter
+{
+ return autoindentIgnoresEnter;
+}
+
+/*
+ * Set whether this text view should automatically create the matching closing char for ", ', ` and ( chars.
+ */
+- (void)setAutopair:(BOOL)enableAutopair
+{
+ autopairEnabled = enableAutopair;
+}
+
+/*
+ * Retrieve whether this text view automatically creates the matching closing char for ", ', ` and ( chars.
+ */
+- (BOOL)autopair
+{
+ return autopairEnabled;
+}
+
+/*
+ * Set whether SQL keywords should be automatically uppercased.
+ */
+- (void)setAutouppercaseKeywords:(BOOL)enableAutouppercaseKeywords
+{
+ autouppercaseKeywordsEnabled = enableAutouppercaseKeywords;
+}
+
+/*
+ * Retrieve whether SQL keywords should be automaticallyuppercased.
+ */
+- (BOOL)autouppercaseKeywords
+{
+ return autouppercaseKeywordsEnabled;
+}
+
+
+/*******************
+SYNTAX HIGHLIGHTING!
+*******************/
+- (void)awakeFromNib
+/*
+ * Sets self as delegate for the textView's textStorage to enable syntax highlighting,
+ * and set defaults for general usage
+ */
+{
+ [[self textStorage] setDelegate:self];
+
+ autoindentEnabled = YES;
+ autopairEnabled = YES;
+ autoindentIgnoresEnter = NO;
+ autouppercaseKeywordsEnabled = YES;
+ delBackwardsWasPressed = NO;
+
+ lineNumberView = [[NoodleLineNumberView alloc] initWithScrollView:scrollView];
+ [scrollView setVerticalRulerView:lineNumberView];
+ [scrollView setHasHorizontalRuler:NO];
+ [scrollView setHasVerticalRuler:YES];
+ [scrollView setRulersVisible:YES];
+}
+
+- (void)textStorageDidProcessEditing:(NSNotification *)notification
+/*
+ * Performs syntax highlighting.
+ * This method recolors the entire text on every keypress. For performance reasons, this function does
+ * nothing if the text is more than 20 KB.
+ *
+ * The main bottleneck is the [NSTextStorage addAttribute:value:range:] method - the parsing itself is really fast!
+ *
+ * Some sample code from Andrew Choi ( http://members.shaw.ca/akochoi-old/blog/2003/11-09/index.html#3 ) has been reused.
+ */
+{
+ NSTextStorage *textStore = [notification object];
+
+ //make sure that the notification is from the correct textStorage object
+ if (textStore!=[self textStorage]) return;
+
+
+ NSColor *commentColor = [NSColor colorWithDeviceRed:0.000 green:0.455 blue:0.000 alpha:1.000];
+ NSColor *quoteColor = [NSColor colorWithDeviceRed:0.769 green:0.102 blue:0.086 alpha:1.000];
+ NSColor *keywordColor = [NSColor colorWithDeviceRed:0.200 green:0.250 blue:1.000 alpha:1.000];
+ NSColor *backtickColor = [NSColor colorWithDeviceRed:0.0 green:0.0 blue:0.658 alpha:1.000];
+ NSColor *numericColor = [NSColor colorWithDeviceRed:0.506 green:0.263 blue:0.0 alpha:1.000];
+ NSColor *variableColor = [NSColor colorWithDeviceRed:0.5 green:0.5 blue:0.5 alpha:1.000];
+
+ NSColor *tokenColor;
+
+ int token;
+ NSRange textRange, tokenRange;
+
+ textRange = NSMakeRange(0, [textStore length]);
+
+ //don't color texts longer than about 20KB. would be too slow
+ if (textRange.length > 20000) return;
+
+ //first remove the old colors
+ [textStore removeAttribute:NSForegroundColorAttributeName range:textRange];
+
+
+ //initialise flex
+ yyuoffset = 0; yyuleng = 0;
+ yy_switch_to_buffer(yy_scan_string([[textStore string] UTF8String]));
+
+ //now loop through all the tokens
+ while (token=yylex()){
+ switch (token) {
+ case SPT_SINGLE_QUOTED_TEXT:
+ case SPT_DOUBLE_QUOTED_TEXT:
+ tokenColor = quoteColor;
+ break;
+ case SPT_BACKTICK_QUOTED_TEXT:
+ tokenColor = backtickColor;
+ break;
+ case SPT_RESERVED_WORD:
+ tokenColor = keywordColor;
+ break;
+ case SPT_NUMERIC:
+ tokenColor = numericColor;
+ break;
+ case SPT_COMMENT:
+ tokenColor = commentColor;
+ break;
+ case SPT_VARIABLE:
+ tokenColor = variableColor;
+ break;
+ default:
+ tokenColor = nil;
+ }
+
+ if (!tokenColor) continue;
+
+ tokenRange = NSMakeRange(yyuoffset, yyuleng);
+
+ // make sure that tokenRange is valid (and therefore within textRange)
+ // otherwise a bug in the lex code could cause the the TextView to crash
+ tokenRange = NSIntersectionRange(tokenRange, textRange);
+ if (!tokenRange.length) continue;
+
+ // If the current token is marked as SQL keyword, uppercase it if required.
+ unsigned long tokenEnd = tokenRange.location+tokenRange.length-1;
+ // Check the end of the token
+ if (autouppercaseKeywordsEnabled && !delBackwardsWasPressed
+ && [[self textStorage] attribute:kSQLkeyword atIndex:tokenEnd effectiveRange:nil])
+ // check if next char is not a kSQLkeyword or current kSQLkeyword is at the end;
+ // if so then upper case keyword if not already done
+ // @try catch() for catching valid index esp. after deleteBackward:
+ {
+ NSString* curTokenString = [[self string] substringWithRange:tokenRange];
+ BOOL doIt = NO;
+ @try
+ {
+ doIt = ![[self textStorage] attribute:kSQLkeyword atIndex:tokenEnd+1 effectiveRange:nil];
+ } @catch(id ae) { doIt = YES; }
+
+ if(doIt && ![[curTokenString uppercaseString] isEqualToString:curTokenString])
+ {
+ // Register it for undo works only partly for now, at least the uppercased keyword will be selected
+ [self shouldChangeTextInRange:tokenRange replacementString:[curTokenString uppercaseString]];
+ [self replaceCharactersInRange:tokenRange withString:[curTokenString uppercaseString]];
+ }
+ }
+
+ [textStore addAttribute: NSForegroundColorAttributeName
+ value: tokenColor
+ range: tokenRange ];
+
+ // Add an attribute to be used in the auto-pairing (keyDown:)
+ // to disable auto-pairing if caret is inside of any token found by lex.
+ // For discussion: maybe change it later (only for quotes not keywords?)
+ [textStore addAttribute: kWQquoted
+ value: kWQval
+ range: tokenRange ];
+
+
+ // Mark each SQL keyword for auto-uppercasing and do it for the next textStorageDidProcessEditing: event.
+ // Performing it one token later allows words which start as reserved keywords to be entered.
+ if(token == SPT_RESERVED_WORD)
+ [textStore addAttribute: kSQLkeyword
+ value: kWQval
+ range: tokenRange ];
+ // Add an attribute to be used to distinguish quotes from keywords etc.
+ // used e.g. in completion suggestions
+ if(token == SPT_DOUBLE_QUOTED_TEXT || token == SPT_SINGLE_QUOTED_TEXT)
+ [textStore addAttribute: kQuote
+ value: kWQval
+ range: tokenRange ];
+ }
+
+}
+
@end
diff --git a/Source/CustomQuery.h b/Source/CustomQuery.h
index 8e81c7e5..c2eac75f 100644
--- a/Source/CustomQuery.h
+++ b/Source/CustomQuery.h
@@ -25,6 +25,7 @@
#import <Cocoa/Cocoa.h>
#import <MCPKit_bundled/MCPKit_bundled.h>
#import "CMCopyTable.h"
+#import "CMTextView.h"
#import "CMMCPConnection.h"
#import "CMMCPResult.h"
@@ -33,7 +34,7 @@
IBOutlet id tableWindow;
IBOutlet id queryFavoritesButton;
IBOutlet id queryHistoryButton;
- IBOutlet id textView;
+ IBOutlet CMTextView *textView;
IBOutlet CMCopyTable *customQueryView;
IBOutlet id errorText;
IBOutlet id affectedRowsText;
@@ -43,6 +44,17 @@
IBOutlet id queryFavoritesView;
IBOutlet id removeQueryFavoriteButton;
IBOutlet id copyQueryFavoriteButton;
+ IBOutlet id runSelectionButton;
+ IBOutlet id runAllButton;
+ IBOutlet NSMenuItem *runSelectionMenuItem;
+ IBOutlet NSMenuItem *clearHistoryMenuItem;
+ IBOutlet NSMenuItem *shiftLeftMenuItem;
+ IBOutlet NSMenuItem *shiftRightMenuItem;
+ IBOutlet NSMenuItem *completionListMenuItem;
+ IBOutlet NSMenuItem *editorFontMenuItem;
+ IBOutlet NSMenuItem *autoindentMenuItem;
+ IBOutlet NSMenuItem *autopairMenuItem;
+ IBOutlet NSMenuItem *autouppercaseKeywordsMenuItem;
NSArray *queryResult;
NSUserDefaults *prefs;
@@ -52,10 +64,12 @@
}
// IBAction methods
-- (IBAction)performQuery:(id)sender;
+- (IBAction)runAllQueries:(id)sender;
+- (IBAction)runSelectedQueries:(id)sender;
- (IBAction)chooseQueryFavorite:(id)sender;
- (IBAction)chooseQueryHistory:(id)sender;
- (IBAction)closeSheet:(id)sender;
+- (IBAction)gearMenuItemSelected:(id)sender;
// queryFavoritesSheet methods
- (IBAction)addQueryFavorite:(id)sender;
@@ -63,6 +77,10 @@
- (IBAction)copyQueryFavorite:(id)sender;
- (IBAction)closeQueryFavoritesSheet:(id)sender;
+// Query actions
+- (void)performQueries:(NSArray *)queries;
+- (NSString *)queryAtPosition:(long)position;
+
// Accessors
- (NSArray *)currentResult;
diff --git a/Source/CustomQuery.m b/Source/CustomQuery.m
index e917e984..41170b38 100644
--- a/Source/CustomQuery.m
+++ b/Source/CustomQuery.m
@@ -25,177 +25,82 @@
#import "CustomQuery.h"
#import "SPSQLParser.h"
#import "SPGrowlController.h"
+#import "SPStringAdditions.h"
+
@implementation CustomQuery
-//IBAction methods
-- (IBAction)performQuery:(id)sender;
+
+
+#pragma mark IBAction methods
+
+
/*
-performs the mysql-query given by the user
-sets the tableView columns corresponding to the mysql-result
-*/
-{
+ * Split all the queries in the text view, split them into individual queries,
+ * and run sequentially.
+ */
+- (IBAction)runAllQueries:(id)sender
+{
+ SPSQLParser *queryParser;
+ NSArray *queries;
+
// Fixes bug in key equivalents.
- if ([[NSApp currentEvent] type] == NSKeyUp)
- {
+ if ([[NSApp currentEvent] type] == NSKeyUp) {
return;
}
-
- NSArray *theColumns;
- NSTableColumn *theCol;
- CMMCPResult *theResult = nil;
- NSArray *queries;
- NSMutableArray *menuItems = [NSMutableArray array];
- NSMutableArray *tempResult = [NSMutableArray array];
- NSMutableString *errors = [NSMutableString string];
- SPSQLParser *queryParser;
- int i;
-
- // Notify listeners that a query has started
- [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self];
// Retrieve the custom query string and split it into separate SQL queries
queryParser = [[SPSQLParser alloc] initWithString:[textView string]];
queries = [queryParser splitStringByCharacter:';'];
[queryParser release];
- // Perform the queries in series
- for ( i = 0 ; i < [queries count] ; i++ ) {
- theResult = [mySQLConnection queryString:[queries objectAtIndex:i]];
- if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) {
-
- // If the query errored, append error to the error log for display at the end
- 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]];
- }
- }
- }
-
- //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];
- }
+ [self performQueries:queries];
-//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"];
+ // Invoke textStorageDidProcessEditing: for syntax highlighting and auto-uppercase
+ [textView setSelectedRange:NSMakeRange(0,0)];
+ [textView insertText:@""];
-//select the text of the query textView and set standard font
+ // Select the text of the query textView for re-editing
[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++;
- }
+/*
+ * Depending on selection, run either the query containing the selection caret (if the caret is
+ * at a single point within the text view), or run the selected text (if a text range is selected).
+ */
+- (IBAction)runSelectedQueries:(id)sender
+{
+ NSArray *queries;
+ NSString *query;
+ NSRange selectedRange = [textView selectedRange];
+ SPSQLParser *queryParser;
-//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]]];
+ // If the current selection is a single caret position, run the current query.
+ if (selectedRange.length == 0) {
+ query = [self queryAtPosition:selectedRange.location];
+ if (!query) {
+ NSBeep();
+ return;
}
- [dataCell setLineBreakMode:NSLineBreakByTruncatingTail];
- [theCol setDataCell:dataCell];
- [[theCol headerCell] setStringValue:[theColumns objectAtIndex:i]];
+ queries = [NSArray arrayWithObject:query];
- [customQueryView addTableColumn:theCol];
- [theCol release];
+ // Otherwise, run the selected text.
+ } else {
+ queryParser = [[SPSQLParser alloc] initWithString:[[textView string] substringWithRange:selectedRange]];
+ queries = [queryParser splitStringByCharacter:';'];
+ [queryParser 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"];
+ // Invoke textStorageDidProcessEditing: for syntax highlighting and auto-uppercase
+ // and preserve the selection
+ [textView setSelectedRange:NSMakeRange(0,0)];
+ [textView insertText:@""];
+ [textView setSelectedRange:selectedRange];
+
+ [self performQueries:queries];
}
+
- (IBAction)chooseQueryFavorite:(id)sender
/*
insert the choosen favorite query in the query textView or save query to favorites or opens window to edit favorites
@@ -233,7 +138,11 @@ insert the choosen favorite query in the query textView or save query to favorit
[queryFavoritesSheet orderOut:nil];
} else if ( [queryFavoritesButton indexOfSelectedItem] != 3) {
//choose favorite
+ // Register the next action for undo
+ [textView shouldChangeTextInRange:[textView selectedRange] replacementString:[queryFavoritesButton titleOfSelectedItem]];
[textView replaceCharactersInRange:[textView selectedRange] withString:[queryFavoritesButton titleOfSelectedItem]];
+ // invoke textStorageDidProcessEditing: for syntax highlighting and auto-uppercase
+ [textView insertText:@""];
}
}
@@ -242,7 +151,11 @@ insert the choosen favorite query in the query textView or save query to favorit
insert the choosen history query in the query textView
*/
{
+ // Register the next action for undo
+ [textView shouldChangeTextInRange:NSMakeRange(0,[[textView string] length]) replacementString:[queryHistoryButton titleOfSelectedItem]];
[textView setString:[queryHistoryButton titleOfSelectedItem]];
+ // Invoke textStorageDidProcessEditing: for syntax highlighting and auto-uppercase
+ [textView insertText:@""];
[textView selectAll:self];
}
@@ -255,7 +168,74 @@ closes the sheet
}
-//queryFavoritesSheet methods
+/*
+ * Perform simple actions (which don't require their own method), triggered by selecting the appropriate menu item
+ * in the "gear" action menu displayed beneath the cusotm query view.
+ */
+- (IBAction)gearMenuItemSelected:(id)sender
+{
+ // "Clear History" menu item - clear query history
+ if (sender == clearHistoryMenuItem) {
+ [queryHistoryButton removeAllItems];
+ [queryHistoryButton addItemWithTitle:NSLocalizedString(@"Query History…",@"Title of query history popup button")];
+ [prefs setObject:[NSArray array] forKey:@"queryHistory"];
+ }
+
+ // "Shift Right" menu item - indent the selection with an additional tab.
+ if (sender == shiftRightMenuItem) {
+ [textView shiftSelectionRight];
+ }
+
+ // "Shift Left" menu item - un-indent the selection by one tab if possible.
+ if (sender == shiftLeftMenuItem) {
+ [textView shiftSelectionLeft];
+ }
+
+ // "Completion List" menu item - used to autocomplete. Uses a different shortcut to avoid the menu button flickering
+ // on normal autocomplete usage.
+ if (sender == completionListMenuItem) {
+ [textView complete:self];
+ }
+
+ // "Editor font..." menu item to bring up the font panel
+ if (sender == editorFontMenuItem) {
+ [[NSFontPanel sharedFontPanel] setPanelFont:[textView font] isMultiple:NO];
+ [[NSFontPanel sharedFontPanel] makeKeyAndOrderFront:self];
+ }
+
+ // "Indent new lines" toggle
+ if (sender == autoindentMenuItem) {
+ BOOL enableAutoindent = ([autoindentMenuItem state] == NSOffState);
+ [prefs setBool:enableAutoindent forKey:@"CustomQueryAutoindent"];
+ [prefs synchronize];
+ [autoindentMenuItem setState:enableAutoindent?NSOnState:NSOffState];
+ [textView setAutoindent:enableAutoindent];
+ }
+
+ // "Auto-pair characters" toggle
+ if (sender == autopairMenuItem) {
+ BOOL enableAutopair = ([autopairMenuItem state] == NSOffState);
+ [prefs setBool:enableAutopair forKey:@"CustomQueryAutopair"];
+ [prefs synchronize];
+ [autopairMenuItem setState:enableAutopair?NSOnState:NSOffState];
+ [textView setAutopair:enableAutopair];
+ }
+
+ // "Auto-uppercase keywords" toggle
+ if (sender == autouppercaseKeywordsMenuItem) {
+ BOOL enableAutouppercaseKeywords = ([autouppercaseKeywordsMenuItem state] == NSOffState);
+ [prefs setBool:enableAutouppercaseKeywords forKey:@"CustomQueryAutouppercaseKeywords"];
+ [prefs synchronize];
+ [autouppercaseKeywordsMenuItem setState:enableAutouppercaseKeywords?NSOnState:NSOffState];
+ [textView setAutouppercaseKeywords:enableAutouppercaseKeywords];
+ }
+}
+
+
+#pragma mark -
+#pragma mark queryFavoritesSheet methods
+
+
- (IBAction)addQueryFavorite:(id)sender
/*
adds a query favorite
@@ -352,7 +332,239 @@ closes queryFavoritesSheet and saves favorites to preferences
}
-//getter methods
+#pragma mark -
+#pragma mark Query actions
+
+
+- (void)performQueries:(NSArray *)queries;
+/*
+performs the mysql-query given by the user
+sets the tableView columns corresponding to the mysql-result
+*/
+{
+
+ NSArray *theColumns;
+ NSTableColumn *theCol;
+ CMMCPResult *theResult = nil;
+ NSMutableArray *menuItems = [NSMutableArray array];
+ NSMutableArray *tempResult = [NSMutableArray array];
+ NSMutableString *errors = [NSMutableString string];
+ int i, totalQueriesRun = 0, totalAffectedRows = 0;
+ float executionTime = 0;
+
+ // Notify listeners that a query has started
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self];
+
+ // Reset the current table view as necessary to avoid redraw and reload issues.
+ // Restore the view position to the top left to be within the results for all datasets.
+ [customQueryView scrollRowToVisible:0];
+ [customQueryView scrollColumnToVisible:0];
+
+ // Remove all the columns
+ theColumns = [customQueryView tableColumns];
+ while ([theColumns count]) {
+ [customQueryView removeTableColumn:[theColumns objectAtIndex:0]];
+ }
+
+ // Perform the supplied queries in series
+ for ( i = 0 ; i < [queries count] ; i++ ) {
+
+ // Don't run blank queries, or queries which only contain whitespace.
+ if ([[[queries objectAtIndex:i] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length] == 0)
+ continue;
+
+ // Run the query, timing execution (note this also includes network and overhead)
+ theResult = [mySQLConnection queryString:[queries objectAtIndex:i]];
+ executionTime += [mySQLConnection lastQueryExecutionTime];
+ totalQueriesRun++;
+
+ // Record any affected rows
+ if ( [mySQLConnection affectedRows] != -1 )
+ totalAffectedRows += [mySQLConnection affectedRows];
+
+ // Store any error messages
+ if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) {
+
+ // If the query errored, append error to the error log for display at the end
+ 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]];
+ }
+ }
+ }
+
+ //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:[queries componentsJoinedByString:@"; "] atIndex:1];
+ while ( [queryHistoryButton numberOfItems] > [[prefs objectForKey:@"CustomQueryMaxHistoryItems"] intValue] + 1 ) {
+ [queryHistoryButton removeItemAtIndex:[queryHistoryButton numberOfItems]-1];
+ }
+ for ( i = 1 ; i < [queryHistoryButton numberOfItems] ; i++ )
+ {
+ [menuItems addObject:[queryHistoryButton itemTitleAtIndex:i]];
+ }
+ [prefs setObject:menuItems forKey:@"queryHistory"];
+
+ if ( [errors length] ) {
+ [errorText setStringValue:errors];
+ } else {
+ [errorText setStringValue:NSLocalizedString(@"There were no errors.", @"text shown when query was successfull")];
+ }
+
+ // Set up the status string
+ if ( totalQueriesRun > 1 ) {
+ if (totalAffectedRows==1) {
+ [affectedRowsText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"1 row affected in total, by %i queries taking %@", @"text showing one row has been affected by multiple queries"),
+ totalQueriesRun,
+ [NSString stringForTimeInterval:executionTime]
+ ]];
+
+ } else {
+ [affectedRowsText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%i rows affected in total, by %i queries taking %@", @"text showing how many rows have been affected by multiple queries"),
+ totalAffectedRows,
+ totalQueriesRun,
+ [NSString stringForTimeInterval:executionTime]
+ ]];
+
+ }
+ } else {
+ if (totalAffectedRows==1) {
+ [affectedRowsText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"1 row affected, taking %@", @"text showing one row has been affected by a single query"),
+ [NSString stringForTimeInterval:executionTime]
+ ]];
+ } else {
+ [affectedRowsText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%i rows affected, taking %@", @"text showing how many rows have been affected by a single query"),
+ totalAffectedRows,
+ [NSString stringForTimeInterval:executionTime]
+ ]];
+
+ }
+ }
+
+
+ // If no results were returned, redraw the empty table and post notifications before returning.
+ if ( !theResult || ![theResult numOfRows] ) {
+ [customQueryView reloadData];
+
+ // Notify any listeners that the query has completed
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self];
+
+ // Perform the Growl notification for query completion
+ [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Query Finished"
+ description:[NSString stringWithFormat:NSLocalizedString(@"%@",@"description for query finished growl notification"), [errorText stringValue]]
+ notificationName:@"Query Finished"];
+
+ return;
+ }
+
+
+ // Otherwise 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"];
+}
+
+/*
+ * Retrieve the query at a position specified within the custom query
+ * text view. This will return nil if the position specified is beyond
+ * the available string or if an empty query would be returned.
+ */
+- (NSString *)queryAtPosition:(long)position
+{
+ SPSQLParser *customQueryParser;
+ NSArray *queries;
+ NSString *query = nil;
+ int i, queryPosition = 0;
+
+ // If the supplied position is negative or beyond the end of the string, return nil.
+ if (position < 0 || position > [[textView string] length])
+ return nil;
+
+ // Split the current text into queries
+ customQueryParser = [[SPSQLParser alloc] initWithString:[textView string]];
+ queries = [[NSArray alloc] initWithArray:[customQueryParser splitStringByCharacter:';']];
+ [customQueryParser release];
+
+ // Walk along the array of queries to identify the current query - taking into account
+ // the extra semicolon at the end of each query
+ for (i = 0; i < [queries count]; i++ ) {
+ queryPosition += [[queries objectAtIndex:i] length];
+ if (queryPosition >= position) {
+ query = [NSString stringWithString:[queries objectAtIndex:i]];
+ break;
+ }
+ queryPosition++;
+ }
+
+ [queries release];
+
+ // Ensure the string isn't empty.
+ // (We could also strip comments for this check, but that prevents use of conditional comments)
+ if ([[query stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length] == 0)
+ return nil;
+
+ // Return the located string.
+ return query;
+}
+
+
+#pragma mark -
+#pragma mark Accessors
+
+
- (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
@@ -384,7 +596,10 @@ returns the current result (as shown in custom result view) as array, the first
}
-//additional methods
+#pragma mark -
+#pragma mark Additional methods
+
+
- (void)setConnection:(CMMCPConnection *)theConnection
/*
sets the connection (received from TableDocument) and makes things that have to be done only once
@@ -403,18 +618,21 @@ sets the connection (received from TableDocument) and makes things that have to
queryFavorites = [[NSMutableArray array] retain];
}
-//set up interface
+ // Set up the interface
[customQueryView setVerticalMotionCanBeginDrag:NO];
- if ( [prefs boolForKey:@"useMonospacedFonts"] ) {
- [textView setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]];
- } else {
- [textView setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
- }
+ [textView setFont:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:@"CustomQueryEditorFont"]]];
[textView setContinuousSpellCheckingEnabled:NO];
+ [autoindentMenuItem setState:([prefs boolForKey:@"CustomQueryAutoindent"]?NSOnState:NSOffState)];
+ [textView setAutoindent:[prefs boolForKey:@"CustomQueryAutoindent"]];
+ [textView setAutoindentIgnoresEnter:YES];
+ [autopairMenuItem setState:([prefs boolForKey:@"CustomQueryAutopair"]?NSOnState:NSOffState)];
+ [textView setAutopair:[prefs boolForKey:@"CustomQueryAutopair"]];
+ [autouppercaseKeywordsMenuItem setState:([prefs boolForKey:@"CustomQueryAutouppercaseKeywords"]?NSOnState:NSOffState)];
+ [textView setAutouppercaseKeywords:[prefs boolForKey:@"CustomQueryAutouppercaseKeywords"]];
[queryFavoritesView registerForDraggedTypes:[NSArray arrayWithObjects:@"SequelProPasteboard", nil]];
while ( (column = [enumerator nextObject]) )
{
- if ( [prefs boolForKey:@"useMonospacedFonts"] ) {
+ if ( [prefs boolForKey:@"UseMonospacedFonts"] ) {
[[column dataCell] setFont:[NSFont fontWithName:@"Monaco" size:10]];
} else {
[[column dataCell] setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
@@ -447,11 +665,14 @@ inserts the query in the textView and performs query
*/
{
[textView setString:query];
- [self performQuery:self];
+ [self runAllQueries:self];
}
-//tableView datasource methods
+#pragma mark -
+#pragma mark TableView datasource methods
+
+
- (int)numberOfRowsInTableView:(NSTableView *)aTableView
{
if ( aTableView == customQueryView ) {
@@ -487,7 +708,7 @@ inserts the query in the textView and performs query
return [tmp autorelease];
}
if ( [[theRow objectAtIndex:[theIdentifier intValue]] isMemberOfClass:[NSNull class]] )
- return [prefs objectForKey:@"nullValue"];
+ return [prefs objectForKey:@"NullValue"];
return [theRow objectAtIndex:[theIdentifier intValue]];
} else if ( aTableView == queryFavoritesView ) {
@@ -646,7 +867,7 @@ opens sheet with value when double clicking on a field
}
[theValue autorelease];
} else if ( [[theRow objectAtIndex:[theIdentifier intValue]] isMemberOfClass:[NSNull class]] ) {
- theValue = [prefs objectForKey:@"nullValue"];
+ theValue = [prefs objectForKey:@"NullValue"];
} else {
theValue = [theRow objectAtIndex:[theIdentifier intValue]];
}
@@ -668,7 +889,10 @@ opens sheet with value when double clicking on a field
}
-//splitView delegate methods
+#pragma mark -
+#pragma mark SplitView delegate methods
+
+
- (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview
/*
tells the splitView that it can collapse views
@@ -702,7 +926,10 @@ defines min position of splitView
}
-//textView delegate methods
+#pragma mark -
+#pragma mark TextView delegate methods
+
+
- (BOOL)textView:(NSTextView *)aTextView doCommandBySelector:(SEL)aSelector
/*
traps enter key and
@@ -714,7 +941,7 @@ traps enter key and
if ( [aTextView methodForSelector:aSelector] == [aTextView methodForSelector:@selector(insertNewline:)] &&
[[[NSApp currentEvent] characters] isEqualToString:@"\003"] )
{
- [self performQuery:self];
+ [self runAllQueries:self];
return YES;
} else {
return NO;
@@ -732,6 +959,88 @@ traps enter key and
}
/*
+ * A notification posted when the selection changes within the text view;
+ * used to control the run-currentrun-selection button state and action.
+ */
+- (void)textViewDidChangeSelection:(NSNotification *)aNotification
+{
+
+ // Ensure that the notification is from the custom query text view
+ if ( [aNotification object] != textView ) return;
+
+ // If no text is selected, disable the button and action menu.
+ if ( [textView selectedRange].location == NSNotFound ) {
+ [runSelectionButton setEnabled:NO];
+ [runSelectionMenuItem setEnabled:NO];
+ return;
+ }
+
+ // If the current selection is a single caret position, update the button based on
+ // whether the caret is inside a valid query.
+ if ([textView selectedRange].length == 0) {
+ int selectionPosition = [textView selectedRange].location;
+ int movedRangeStart, movedRangeLength;
+ NSRange oldSelection;
+
+ // Retrieve the old selection position
+ [[[aNotification userInfo] objectForKey:@"NSOldSelectedCharacterRange"] getValue:&oldSelection];
+
+ // Only process the query text if the selection previously had length, or moved more than 100 characters,
+ // or the intervening space contained a semicolon, or typing has been performed with no current query.
+ // This adds more checks to every keypress, but ensures the majority of the actions don't incur a
+ // parsing overhead - which is cheap on small text strings but heavy of large queries.
+ movedRangeStart = (selectionPosition < oldSelection.location)?selectionPosition:oldSelection.location;
+ movedRangeLength = abs(selectionPosition - oldSelection.location);
+ if (oldSelection.length > 0
+ || movedRangeLength > 100
+ || oldSelection.location > [[textView string] length]
+ || [[textView string] rangeOfString:@";" options:0 range:NSMakeRange(movedRangeStart, movedRangeLength)].location != NSNotFound
+ || (![runSelectionButton isEnabled] && selectionPosition > oldSelection.location
+ && [[[[textView string] substringWithRange:NSMakeRange(movedRangeStart, movedRangeLength)] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length])
+ ) {
+
+ [runSelectionButton setTitle:NSLocalizedString(@"Run Current", @"Title of button to run current query in custom query view")];
+ [runSelectionMenuItem setTitle:NSLocalizedString(@"Run Current Query", @"Title of action menu item to run current query in custom query view")];
+
+ // If a valid query is present at the cursor position, enable the button
+ if ([self queryAtPosition:selectionPosition]) {
+ [runSelectionButton setEnabled:YES];
+ [runSelectionMenuItem setEnabled:YES];
+ } else {
+ [runSelectionButton setEnabled:NO];
+ [runSelectionMenuItem setEnabled:NO];
+ }
+ }
+
+ // For selection ranges, enable the button.
+ } else {
+ [runSelectionButton setTitle:NSLocalizedString(@"Run Selection", @"Title of button to run selected text in custom query view")];
+ [runSelectionButton setEnabled:YES];
+ [runSelectionMenuItem setTitle:NSLocalizedString(@"Run Selected Text", @"Title of action menu item to run selected text in custom query view")];
+ [runSelectionMenuItem setEnabled:YES];
+ }
+}
+
+
+/*
+ * Save the custom query editor font if it is changed.
+ */
+- (void)textViewDidChangeTypingAttributes:(NSNotification *)aNotification
+{
+
+ // Only save the font if prefs have been loaded, ensuring the saved font has been applied once.
+ if (prefs) {
+ [prefs setObject:[NSArchiver archivedDataWithRootObject:[textView font]] forKey:@"CustomQueryEditorFont"];
+ }
+}
+
+
+
+#pragma mark -
+#pragma mark TableView notifications
+
+
+/*
* Updates various interface elements based on the current table view selection.
*/
- (void)tableViewSelectionDidChange:(NSNotification *)notification
@@ -744,10 +1053,15 @@ traps enter key and
}
}
+
+#pragma mark -
+
+
// Last but not least
- (id)init;
{
self = [super init];
+ prefs = nil;
return self;
}
diff --git a/Source/MainController.h b/Source/MainController.h
index b47bb457..a153964d 100644
--- a/Source/MainController.h
+++ b/Source/MainController.h
@@ -24,63 +24,30 @@
#import <Cocoa/Cocoa.h>
+@class SPPreferenceController;
+
@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;
+
+ SPPreferenceController *prefsController;
}
-//IBAction methods
+// 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
+// Services menu methods
- (void)doPerformQueryService:(NSPasteboard *)pboard userData:(NSString *)data error:(NSString **)error;
-//menu methods
+// Menu methods
- (IBAction)donate:(id)sender;
- (IBAction)visitWebsite:(id)sender;
- (IBAction)visitHelpWebsite:(id)sender;
-- (IBAction)checkForUpdates:(id)sender;
-//SSHTunnel methods
-- (id)authenticate:(NSScriptCommand *)command;
+// Getters
+- (SPPreferenceController *)preferenceController;
+
+// Other
- (id)handleQuitScriptCommand:(NSScriptCommand *)command;
@end
diff --git a/Source/MainController.m b/Source/MainController.m
index c5e2eeb0..a2dfcc36 100644
--- a/Source/MainController.m
+++ b/Source/MainController.m
@@ -25,737 +25,188 @@
#import "MainController.h"
#import "KeyChain.h"
#import "TableDocument.h"
+#import "SPPreferenceController.h"
+
+#define SEQUEL_PRO_HOME_PAGE_URL @"http://www.sequelpro.com/"
+#define SEQUEL_PRO_DONATIONS_URL @"http://www.sequelpro.com/donate.html"
+#define SEQUEL_PRO_FAQ_URL @"http://www.sequelpro.com/frequently-asked-questions.html"
@implementation MainController
-/*
-opens the preferences window
-*/
-- (IBAction)openPreferences:(id)sender
+/**
+ * Called even before init so we can register our preference defaults
+ */
++ (void)initialize
{
- //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];
+ // Register application defaults
+ [[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"PreferenceDefaults" ofType:@"plist"]]];
}
-/*
-adds a favorite
-*/
-- (IBAction)addFavorite:(id)sender
+/**
+ * Initialisation stuff upon nib awakening
+ */
+- (void)awakeFromNib
{
- 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];
+ prefsController = [[SPPreferenceController alloc] init];
- [NSApp endSheet:favoriteSheet];
- [favoriteSheet orderOut:nil];
+ // Register MainController as services provider
+ [NSApp setServicesProvider:self];
- 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];
- }
+ // Register MainController for AppleScript events
+ [[NSScriptExecutionContext sharedScriptExecutionContext] setTopLevelObject:self];
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;
+ // Ensure we're not being run on Leopard
+ int systemPrefix = 10, systemMajor = 0, systemMinor = 0;
+ NSString *systemVersion = [[NSDictionary dictionaryWithContentsOfFile:@"/System/Library/CoreServices/SystemVersion.plist"] objectForKey:@"ProductVersion"];
+ NSArray *systemVersionArray = [systemVersion componentsSeparatedByString:@"."];
+ if ([systemVersionArray count]) systemPrefix = [[systemVersionArray objectAtIndex:0] intValue];
+ if ([systemVersionArray count] > 1) systemMajor = [[systemVersionArray objectAtIndex:1] intValue];
+ if ([systemVersionArray count] > 2) systemMinor = [[systemVersionArray objectAtIndex:2] intValue];
+ if (systemPrefix == 10 && systemMajor > 4) {
+ NSAlert *alert = [NSAlert alertWithMessageText:@"This is the Tiger (10.4) version of Sequel Pro" defaultButton:@"Quit and open website" alternateButton:@"Run anyway" otherButton:@"Quit" informativeTextWithFormat:@"This version of Sequel Pro is only intended for use with Mac OS X Tiger (10.4.x). When run on your system, the interface will show incorrectly and buttons will be out of place. We recommend you visit the website to download a current version of Sequel Pro."];
+ int returncode = [alert runModal];
- 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];
+ // Quit and open website button selected
+ if (returncode == NSAlertDefaultReturn) {
+ [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.sequelpro.com/"]];
+ [NSApp terminate:self];
- [tableView reloadData];
-}
+ // Quit
+ } else if (returncode == NSAlertOtherReturn) {
+ [[NSApplication sharedApplication] terminate:self];
-/*
-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;
+ // Run normally, opening a window manually
+ } else {
+ TableDocument *tableDocument;
- //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 (tableDocument = [[NSDocumentController sharedDocumentController] makeUntitledDocumentOfType:@"DocumentType" error:nil]) {
+ if ([[NSUserDefaults standardUserDefaults] boolForKey:@"AutoConnectToDefault"]) {
+ [tableDocument setShouldAutomaticallyConnect:YES];
}
+ [[NSDocumentController sharedDocumentController] addDocument:tableDocument];
+ [tableDocument makeWindowControllers];
+ [tableDocument showWindows];
}
-/*
- 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++;
}
}
+}
+
+#pragma mark -
+#pragma mark IBAction methods
- [NSApp stopModalWithCode:[sender tag]];
+/**
+ * Opens the preferences window
+ */
+- (IBAction)openPreferences:(id)sender
+{
+ [prefsController showWindow:self];
}
-/*
-enables/disables ssh tunneling
-*/
-- (IBAction)toggleUseSSH:(id)sender
+#pragma mark -
+#pragma mark Getters
+
+/**
+ * Provide a method to retrieve the prefs controller
+ */
+- (SPPreferenceController *)preferenceController
{
- 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];
- }
+ return prefsController;
}
+
+#pragma mark -
#pragma mark Services menu methods
-/*
-passes the query to the last created document
-*/
+/**
+ * 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])) {
+
+ NSArray *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] ) {
+
+ // Check if at least one document exists
+ 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];
-
+
+ // Pass query to last created document
+ [[[[NSDocumentController sharedDocumentController] documents] objectAtIndex:([[[NSDocumentController sharedDocumentController] documents] count] - 1)] doPerformQueryService:pboardString];
+
return;
}
-
+#pragma mark -
#pragma mark Sequel Pro menu methods
-/*
-opens donate link in default browser
-*/
+/**
+ * Opens donate link in default browser
+ */
- (IBAction)donate:(id)sender
{
- [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.sequelpro.com/donate.html"]];
+ [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:SEQUEL_PRO_DONATIONS_URL]];
}
-/*
-opens website link in default browser
-*/
+/**
+ * Opens website link in default browser
+ */
- (IBAction)visitWebsite:(id)sender
{
- [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.sequelpro.com/"]];
+ [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:SEQUEL_PRO_HOME_PAGE_URL]];
}
-/*
-opens help link in default browser
-*/
+/**
+ * Opens help link in default browser
+ */
- (IBAction)visitHelpWebsite:(id)sender
{
- [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.sequelpro.com/frequently-asked-questions.html"]];
-}
-
-/*
-checks for updates and opens download page in default browser
-*/
-- (IBAction)checkForUpdates:(id)sender
-{
- NSLog(@"[MainController checkForUpdates:] is not currently functional.");
+ [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:SEQUEL_PRO_FAQ_URL]];
}
+#pragma mark -
+#pragma mark Other methods
-#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
+/**
+ * Override the default open-blank-document methods to automatically connect
+ * automatically opened windows.
*/
-- (BOOL)tableView:(NSTableView *)aTableView shouldEditTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex
+- (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
{
- 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];
+ TableDocument *firstTableDocument;
- 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"];
+ // Manually open a new document, setting MainController as sender to trigger autoconnection
+ if (firstTableDocument = [[NSDocumentController sharedDocumentController] makeUntitledDocumentOfType:@"DocumentType" error:nil]) {
+ if ([[NSUserDefaults standardUserDefaults] boolForKey:@"AutoConnectToDefault"]) {
+ [firstTableDocument setShouldAutomaticallyConnect:YES];
}
-
- //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];
+ [[NSDocumentController sharedDocumentController] addDocument:firstTableDocument];
+ [firstTableDocument makeWindowControllers];
+ [firstTableDocument showWindows];
}
+ // Return NO to the automatic opening
return NO;
}
-
-#pragma mark Window delegate methods
-
-/*
- saves the preferences
+/**
+ * What exactly is this for?
*/
-- (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
-{
- int currentVersionNumber;
-
- // Register MainController as services provider
- [NSApp setServicesProvider:self];
-
- // Register MainController for AppleScript events
- [[NSScriptExecutionContext sharedScriptExecutionContext] setTopLevelObject:self];
-
- // Get the current bundle version number (the SVN build number) for per-version upgrades
- currentVersionNumber = [[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"] intValue];
-
- prefs = [[NSUserDefaults standardUserDefaults] retain];
- isNewFavorite = NO;
- [prefs registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
- [NSNumber numberWithBool:YES], @"reloadAfterAdding",
- [NSNumber numberWithBool:YES], @"reloadAfterEditing",
- [NSNumber numberWithBool:NO], @"reloadAfterRemoving",
- [NSString stringWithString:@"NULL"], @"nullValue",
- [NSNumber numberWithBool:YES], @"showError",
- [NSNumber numberWithBool:NO], @"dontShowBlob",
- [NSString stringWithString:NSHomeDirectory()], @"savePath",
- [NSString stringWithString:NSHomeDirectory()], @"openPath",
- [NSString stringWithString:@"Autodetect"], @"encoding",
- [NSNumber numberWithBool:NO], @"useMonospacedFonts",
- [NSNumber numberWithBool:YES], @"fetchRowCount",
- [NSNumber numberWithBool:YES], @"limitRows",
- [NSNumber numberWithInt:1000], @"limitRowsValue",
- [NSNumber numberWithInt:60], @"keepAliveInterval",
- [NSNumber numberWithInt:0], @"lastUsedVersion",
- nil]];
-
- // For versions prior to r336, where column widths have been saved, walk through them and remove
- // any table widths set to 15 or less (fix for mangled columns caused by Issue #140)
- if ([[prefs objectForKey:@"lastUsedVersion"] intValue] < 336 && [prefs objectForKey:@"tableColumnWidths"] != nil) {
- NSEnumerator *databaseEnumerator, *tableEnumerator, *columnEnumerator;
- NSString *databaseKey, *tableKey, *columnKey;
- NSMutableDictionary *newDatabase, *newTable;
- float columnWidth;
- NSMutableDictionary *newTableColumnWidths = [[NSMutableDictionary alloc] init];
-
- databaseEnumerator = [[prefs objectForKey:@"tableColumnWidths"] keyEnumerator];
- while (databaseKey = [databaseEnumerator nextObject]) {
- newDatabase = [[NSMutableDictionary alloc] init];
- tableEnumerator = [[[prefs objectForKey:@"tableColumnWidths"] objectForKey:databaseKey] keyEnumerator];
- while (tableKey = [tableEnumerator nextObject]) {
- newTable = [[NSMutableDictionary alloc] init];
- columnEnumerator = [[[[prefs objectForKey:@"tableColumnWidths"] objectForKey:databaseKey] objectForKey:tableKey] keyEnumerator];
- while (columnKey = [columnEnumerator nextObject]) {
- columnWidth = [[[[[prefs objectForKey:@"tableColumnWidths"] objectForKey:databaseKey] objectForKey:tableKey] objectForKey:columnKey] floatValue];
- if (columnWidth >= 15) {
- [newTable setObject:[NSNumber numberWithFloat:columnWidth] forKey:[NSString stringWithString:columnKey]];
- }
- }
- if ([newTable count]) {
- [newDatabase setObject:[NSDictionary dictionaryWithDictionary:newTable] forKey:[NSString stringWithString:tableKey]];
- }
- [newTable release];
- }
- if ([newDatabase count]) {
- [newTableColumnWidths setObject:[NSDictionary dictionaryWithDictionary:newDatabase] forKey:[NSString stringWithString:databaseKey]];
- }
- [newDatabase release];
- }
- [prefs setObject:[NSDictionary dictionaryWithDictionary:newTableColumnWidths] forKey:@"tableColumnWidths"];
- [newTableColumnWidths release];
- }
-
- // Write the current bundle version to the prefs
- [prefs setObject:[NSNumber numberWithInt:currentVersionNumber] forKey:@"lastUsedVersion"];
-
- [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 @"";
-*/
-}
-
-// Method used for Applescript hooks to quit the application
- (id)handleQuitScriptCommand:(NSScriptCommand *)command
{
- [ NSApp terminate: self ];
+ [NSApp terminate:self];
+
+ // Suppress warning
return nil;
}
diff --git a/Source/NoodleLineNumberView.h b/Source/NoodleLineNumberView.h
new file mode 100644
index 00000000..ca734a56
--- /dev/null
+++ b/Source/NoodleLineNumberView.h
@@ -0,0 +1,60 @@
+//
+// NoodleLineNumberView.h
+// Line View Test
+//
+// Created by Paul Kim on 9/28/08.
+// Copyright (c) 2008 Noodlesoft, LLC. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#import <Cocoa/Cocoa.h>
+
+@class NoodleLineNumberMarker;
+
+@interface NoodleLineNumberView : NSRulerView
+{
+ // Array of character indices for the beginning of each line
+ NSMutableArray *lineIndices;
+ NSFont *font;
+ NSColor *textColor;
+ NSColor *alternateTextColor;
+ NSColor *backgroundColor;
+}
+
+- (id)initWithScrollView:(NSScrollView *)aScrollView;
+
+- (void)setFont:(NSFont *)aFont;
+- (NSFont *)font;
+
+- (void)setTextColor:(NSColor *)color;
+- (NSColor *)textColor;
+
+- (void)setAlternateTextColor:(NSColor *)color;
+- (NSColor *)alternateTextColor;
+
+- (void)setBackgroundColor:(NSColor *)color;
+- (NSColor *)backgroundColor;
+
+- (unsigned)lineNumberForLocation:(float)location;
+
+@end
diff --git a/Source/NoodleLineNumberView.m b/Source/NoodleLineNumberView.m
new file mode 100644
index 00000000..c5d76187
--- /dev/null
+++ b/Source/NoodleLineNumberView.m
@@ -0,0 +1,493 @@
+//
+// NoodleLineNumberView.m
+// Line View Test
+//
+// Created by Paul Kim on 9/28/08.
+// Copyright (c) 2008 Noodlesoft, LLC. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+// This version of the NoodleLineNumberView for Sequel Pro removes marker
+// functionality.
+
+#import "NoodleLineNumberView.h"
+
+#define DEFAULT_THICKNESS 22.0
+#define RULER_MARGIN 5.0
+
+@interface NoodleLineNumberView (Private)
+
+- (NSMutableArray *)lineIndices;
+- (void)invalidateLineIndices;
+- (void)calculateLines;
+- (unsigned)lineNumberForCharacterIndex:(unsigned)index inText:(NSString *)text;
+- (NSDictionary *)textAttributes;
+
+@end
+
+@implementation NoodleLineNumberView
+
+- (id)initWithScrollView:(NSScrollView *)aScrollView
+{
+ if ((self = [super initWithScrollView:aScrollView orientation:NSVerticalRuler]) != nil)
+ {
+ [self setClientView:[aScrollView documentView]];
+ }
+ return self;
+}
+
+- (void)awakeFromNib
+{
+ [self setClientView:[[self scrollView] documentView]];
+}
+
+- (void)dealloc
+{
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+
+ [lineIndices release];
+ [font release];
+
+ [super dealloc];
+}
+
+- (void)setFont:(NSFont *)aFont
+{
+ if (font != aFont)
+ {
+ [font autorelease];
+ font = [aFont retain];
+ }
+}
+
+- (NSFont *)font
+{
+ if (font == nil)
+ {
+ return [NSFont labelFontOfSize:[NSFont systemFontSizeForControlSize:NSMiniControlSize]];
+ }
+ return font;
+}
+
+- (void)setTextColor:(NSColor *)color
+{
+ if (textColor != color)
+ {
+ [textColor autorelease];
+ textColor = [color retain];
+ }
+}
+
+- (NSColor *)textColor
+{
+ if (textColor == nil)
+ {
+ return [NSColor colorWithCalibratedWhite:0.42 alpha:1.0];
+ }
+ return textColor;
+}
+
+- (void)setAlternateTextColor:(NSColor *)color
+{
+ if (alternateTextColor != color)
+ {
+ [alternateTextColor autorelease];
+ alternateTextColor = [color retain];
+ }
+}
+
+- (NSColor *)alternateTextColor
+{
+ if (alternateTextColor == nil)
+ {
+ return [NSColor whiteColor];
+ }
+ return alternateTextColor;
+}
+
+- (void)setBackgroundColor:(NSColor *)color
+{
+ if (backgroundColor != color)
+ {
+ [backgroundColor autorelease];
+ backgroundColor = [color retain];
+ }
+}
+
+- (NSColor *)backgroundColor
+{
+ return backgroundColor;
+}
+
+- (void)setClientView:(NSView *)aView
+{
+ id oldClientView;
+
+ oldClientView = [self clientView];
+
+ if ((oldClientView != aView) && [oldClientView isKindOfClass:[NSTextView class]])
+ {
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:NSTextStorageDidProcessEditingNotification object:[(NSTextView *)oldClientView textStorage]];
+ }
+ [super setClientView:aView];
+ if ((aView != nil) && [aView isKindOfClass:[NSTextView class]])
+ {
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textDidChange:) name:NSTextStorageDidProcessEditingNotification object:[(NSTextView *)aView textStorage]];
+
+ [self invalidateLineIndices];
+ }
+}
+
+- (NSMutableArray *)lineIndices
+{
+ if (lineIndices == nil)
+ {
+ [self calculateLines];
+ }
+ return lineIndices;
+}
+
+- (void)invalidateLineIndices
+{
+ [lineIndices release];
+ lineIndices = nil;
+}
+
+- (void)textDidChange:(NSNotification *)notification
+{
+ // Invalidate the line indices. They will be recalculated and recached on demand.
+ [self invalidateLineIndices];
+
+ [self setNeedsDisplay:YES];
+}
+
+- (unsigned)lineNumberForLocation:(float)location
+{
+ unsigned line, count, index, rectCount, i;
+ NSRectArray rects;
+ NSRect visibleRect;
+ NSLayoutManager *layoutManager;
+ NSTextContainer *container;
+ NSRange nullRange;
+ NSMutableArray *lines;
+ id view;
+
+ view = [self clientView];
+ visibleRect = [[[self scrollView] contentView] bounds];
+
+ lines = [self lineIndices];
+
+ location += NSMinY(visibleRect);
+
+ if ([view isKindOfClass:[NSTextView class]])
+ {
+ nullRange = NSMakeRange(NSNotFound, 0);
+ layoutManager = [view layoutManager];
+ container = [view textContainer];
+ count = [lines count];
+
+ for (line = 0; line < count; line++)
+ {
+ index = [[lines objectAtIndex:line] unsignedIntValue];
+
+ rects = [layoutManager rectArrayForCharacterRange:NSMakeRange(index, 0)
+ withinSelectedCharacterRange:nullRange
+ inTextContainer:container
+ rectCount:&rectCount];
+
+ for (i = 0; i < rectCount; i++)
+ {
+ if ((location >= NSMinY(rects[i])) && (location < NSMaxY(rects[i])))
+ {
+ return line + 1;
+ }
+ }
+ }
+ }
+ return NSNotFound;
+}
+
+- (void)calculateLines
+{
+ id view;
+
+ view = [self clientView];
+
+ if ([view isKindOfClass:[NSTextView class]])
+ {
+ unsigned index, numberOfLines, stringLength, lineEnd, contentEnd;
+ NSString *text;
+ float oldThickness, newThickness;
+
+ text = [view string];
+ stringLength = [text length];
+ [lineIndices release];
+ lineIndices = [[NSMutableArray alloc] init];
+
+ index = 0;
+ numberOfLines = 0;
+
+ do
+ {
+ [lineIndices addObject:[NSNumber numberWithUnsignedInt:index]];
+
+ index = NSMaxRange([text lineRangeForRange:NSMakeRange(index, 0)]);
+ numberOfLines++;
+ }
+ while (index < stringLength);
+
+ // Check if text ends with a new line.
+ [text getLineStart:NULL end:&lineEnd contentsEnd:&contentEnd forRange:NSMakeRange([[lineIndices lastObject] unsignedIntValue], 0)];
+ if (contentEnd < lineEnd)
+ {
+ [lineIndices addObject:[NSNumber numberWithUnsignedInt:index]];
+ }
+
+ oldThickness = [self ruleThickness];
+ newThickness = [self requiredThickness];
+ if (fabs(oldThickness - newThickness) > 1)
+ {
+ NSInvocation *invocation;
+
+ // Not a good idea to resize the view during calculations (which can happen during
+ // display). Do a delayed perform (using NSInvocation since arg is a float).
+ invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:@selector(setRuleThickness:)]];
+ [invocation setSelector:@selector(setRuleThickness:)];
+ [invocation setTarget:self];
+ [invocation setArgument:&newThickness atIndex:2];
+
+ [invocation performSelector:@selector(invoke) withObject:nil afterDelay:0.0];
+ }
+ }
+}
+
+- (unsigned)lineNumberForCharacterIndex:(unsigned)index inText:(NSString *)text
+{
+ unsigned left, right, mid, lineStart;
+ NSMutableArray *lines;
+
+ lines = [self lineIndices];
+
+ // Binary search
+ left = 0;
+ right = [lines count];
+
+ while ((right - left) > 1)
+ {
+ mid = (right + left) / 2;
+ lineStart = [[lines objectAtIndex:mid] unsignedIntValue];
+
+ if (index < lineStart)
+ {
+ right = mid;
+ }
+ else if (index > lineStart)
+ {
+ left = mid;
+ }
+ else
+ {
+ return mid;
+ }
+ }
+ return left;
+}
+
+- (NSDictionary *)textAttributes
+{
+ return [NSDictionary dictionaryWithObjectsAndKeys:
+ [self font], NSFontAttributeName,
+ [self textColor], NSForegroundColorAttributeName,
+ nil];
+}
+
+- (float)requiredThickness
+{
+ unsigned lineCount, digits, i;
+ NSMutableString *sampleString;
+ NSSize stringSize;
+
+ lineCount = [[self lineIndices] count];
+ digits = (unsigned)log10(lineCount) + 1;
+ sampleString = [NSMutableString string];
+ for (i = 0; i < digits; i++)
+ {
+ // Use "8" since it is one of the fatter numbers. Anything but "1"
+ // will probably be ok here. I could be pedantic and actually find the fattest
+ // number for the current font but nah.
+ [sampleString appendString:@"8"];
+ }
+
+ stringSize = [sampleString sizeWithAttributes:[self textAttributes]];
+
+ // Round up the value. There is a bug on 10.4 where the display gets all wonky when scrolling if you don't
+ // return an integral value here.
+ return ceilf(MAX(DEFAULT_THICKNESS, stringSize.width + RULER_MARGIN * 2));
+}
+
+- (void)drawHashMarksAndLabelsInRect:(NSRect)aRect
+{
+ id view;
+ NSRect bounds;
+
+ bounds = [self bounds];
+
+ if (backgroundColor != nil)
+ {
+ [backgroundColor set];
+ NSRectFill(bounds);
+
+ [[NSColor colorWithCalibratedWhite:0.58 alpha:1.0] set];
+ [NSBezierPath strokeLineFromPoint:NSMakePoint(NSMaxX(bounds) - 0/5, NSMinY(bounds)) toPoint:NSMakePoint(NSMaxX(bounds) - 0.5, NSMaxY(bounds))];
+ }
+
+ view = [self clientView];
+
+ if ([view isKindOfClass:[NSTextView class]])
+ {
+ NSLayoutManager *layoutManager;
+ NSTextContainer *container;
+ NSRect visibleRect;
+ NSRange range, glyphRange, nullRange;
+ NSString *text, *labelText;
+ unsigned rectCount, index, line, count;
+ NSRectArray rects;
+ float ypos, yinset;
+ NSDictionary *textAttributes, *currentTextAttributes;
+ NSSize stringSize;
+ NSMutableArray *lines;
+
+ layoutManager = [view layoutManager];
+ container = [view textContainer];
+ text = [view string];
+ nullRange = NSMakeRange(NSNotFound, 0);
+
+ yinset = [view textContainerInset].height;
+ visibleRect = [[[self scrollView] contentView] bounds];
+
+ textAttributes = [self textAttributes];
+
+ lines = [self lineIndices];
+
+ // Find the characters that are currently visible
+ glyphRange = [layoutManager glyphRangeForBoundingRect:visibleRect inTextContainer:container];
+ range = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
+
+ // Fudge the range a tad in case there is an extra new line at end.
+ // It doesn't show up in the glyphs so would not be accounted for.
+ range.length++;
+
+ count = [lines count];
+ index = 0;
+
+ for (line = [self lineNumberForCharacterIndex:range.location inText:text]; line < count; line++)
+ {
+ index = [[lines objectAtIndex:line] unsignedIntValue];
+
+ if (NSLocationInRange(index, range))
+ {
+ rects = [layoutManager rectArrayForCharacterRange:NSMakeRange(index, 0)
+ withinSelectedCharacterRange:nullRange
+ inTextContainer:container
+ rectCount:&rectCount];
+
+ if (rectCount > 0)
+ {
+ // Note that the ruler view is only as tall as the visible
+ // portion. Need to compensate for the clipview's coordinates.
+ ypos = yinset + NSMinY(rects[0]) - NSMinY(visibleRect);
+
+ // Line numbers are internally stored starting at 0
+ labelText = [NSString stringWithFormat:@"%d", line + 1];
+
+ stringSize = [labelText sizeWithAttributes:textAttributes];
+
+ currentTextAttributes = textAttributes;
+
+ // Draw string flush right, centered vertically within the line
+ [labelText drawInRect:
+ NSMakeRect(NSWidth(bounds) - stringSize.width - RULER_MARGIN,
+ ypos + (NSHeight(rects[0]) - stringSize.height) / 2.0,
+ NSWidth(bounds) - RULER_MARGIN * 2.0, NSHeight(rects[0]))
+ withAttributes:currentTextAttributes];
+ }
+ }
+ if (index > NSMaxRange(range))
+ {
+ break;
+ }
+ }
+ }
+}
+
+
+#pragma mark NSCoding methods
+
+#define NOODLE_FONT_CODING_KEY @"font"
+#define NOODLE_TEXT_COLOR_CODING_KEY @"textColor"
+#define NOODLE_ALT_TEXT_COLOR_CODING_KEY @"alternateTextColor"
+#define NOODLE_BACKGROUND_COLOR_CODING_KEY @"backgroundColor"
+
+- (id)initWithCoder:(NSCoder *)decoder
+{
+ if ((self = [super initWithCoder:decoder]) != nil)
+ {
+ if ([decoder allowsKeyedCoding])
+ {
+ font = [[decoder decodeObjectForKey:NOODLE_FONT_CODING_KEY] retain];
+ textColor = [[decoder decodeObjectForKey:NOODLE_TEXT_COLOR_CODING_KEY] retain];
+ alternateTextColor = [[decoder decodeObjectForKey:NOODLE_ALT_TEXT_COLOR_CODING_KEY] retain];
+ backgroundColor = [[decoder decodeObjectForKey:NOODLE_BACKGROUND_COLOR_CODING_KEY] retain];
+ }
+ else
+ {
+ font = [[decoder decodeObject] retain];
+ textColor = [[decoder decodeObject] retain];
+ alternateTextColor = [[decoder decodeObject] retain];
+ backgroundColor = [[decoder decodeObject] retain];
+ }
+ }
+ return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)encoder
+{
+ [super encodeWithCoder:encoder];
+
+ if ([encoder allowsKeyedCoding])
+ {
+ [encoder encodeObject:font forKey:NOODLE_FONT_CODING_KEY];
+ [encoder encodeObject:textColor forKey:NOODLE_TEXT_COLOR_CODING_KEY];
+ [encoder encodeObject:alternateTextColor forKey:NOODLE_ALT_TEXT_COLOR_CODING_KEY];
+ [encoder encodeObject:backgroundColor forKey:NOODLE_BACKGROUND_COLOR_CODING_KEY];
+ }
+ else
+ {
+ [encoder encodeObject:font];
+ [encoder encodeObject:textColor];
+ [encoder encodeObject:alternateTextColor];
+ [encoder encodeObject:backgroundColor];
+ }
+}
+
+@end
diff --git a/Source/SPArrayAdditions.h b/Source/SPArrayAdditions.h
new file mode 100644
index 00000000..d1084ad7
--- /dev/null
+++ b/Source/SPArrayAdditions.h
@@ -0,0 +1,29 @@
+//
+// SPArrayAdditions.h
+// sequel-pro
+//
+// Created by Jakob Egger on March 24, 2009
+//
+// 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>
+
+@interface NSArray (SPArrayAdditions)
+
+- (NSString *)componentsJoinedAndBacktickQuoted;
+
+@end
diff --git a/Source/SPArrayAdditions.m b/Source/SPArrayAdditions.m
new file mode 100644
index 00000000..3115eb47
--- /dev/null
+++ b/Source/SPArrayAdditions.m
@@ -0,0 +1,45 @@
+//
+// SPArrayAdditions.m
+// sequel-pro
+//
+// Created by Jakob Egger on March 24, 2009
+//
+// 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 "SPArrayAdditions.h"
+#import "SPStringAdditions.h"
+
+@implementation NSArray (SPArrayAdditions)
+
+- (NSString *)componentsJoinedAndBacktickQuoted;
+/*
+ * This method quotes all elements with backticks and then joins them with
+ * commas. Use it for field lists as in "SELECT (...) FROM somewhere"
+ */
+{
+ NSString *result = [NSString string];
+ int i;
+ for (i = 0; i < [self count]; i++)
+ {
+ NSString *component = [self objectAtIndex:i];
+ if ([result length]) result = [result stringByAppendingString: @","];
+ result = [result stringByAppendingString: [component backtickQuotedString] ];
+ }
+ return result;
+}
+
+@end
diff --git a/Source/SPConsoleMessage.h b/Source/SPConsoleMessage.h
new file mode 100644
index 00000000..233c19b8
--- /dev/null
+++ b/Source/SPConsoleMessage.h
@@ -0,0 +1,44 @@
+//
+// SPConsoleMessage.h
+// sequel-pro
+//
+// Created by Stuart Connolly (stuconnolly.com) on Mar 12, 2009
+//
+// 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>
+
+@interface SPConsoleMessage : NSObject
+{
+ BOOL isError;
+ NSDate *messageDate;
+ NSString *message;
+}
+
++ (SPConsoleMessage *)consoleMessageWithMessage:(NSString *)consoleMessage date:(NSDate *)date;
+
+- (id)initWithMessage:(NSString *)message date:(NSDate *)date;
+
+- (BOOL)isError;
+- (NSDate *)messageDate;
+- (NSString *)message;
+
+- (void)setIsError:(BOOL)error;
+- (void)setMessageDate:(NSDate *)theDate;
+- (void)setMessage:(NSString *)theMessage;
+
+@end
diff --git a/Source/SPConsoleMessage.m b/Source/SPConsoleMessage.m
new file mode 100644
index 00000000..d5bdfc41
--- /dev/null
+++ b/Source/SPConsoleMessage.m
@@ -0,0 +1,84 @@
+//
+// SPConsoleMessage.m
+// sequel-pro
+//
+// Created by Stuart Connolly (stuconnolly.com) on Mar 12, 2009
+//
+// 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 "SPConsoleMessage.h"
+
+@implementation SPConsoleMessage
+
++ (SPConsoleMessage *)consoleMessageWithMessage:(NSString *)message date:(NSDate *)date
+{
+ return [[[SPConsoleMessage alloc] initWithMessage:message date:date] autorelease];
+}
+
+- (id)initWithMessage:(NSString *)consoleMessage date:(NSDate *)date
+{
+ if ((self = [super init])) {
+ isError = NO;
+ messageDate = [date copy];
+ message = [[NSString alloc] initWithString:consoleMessage];
+ }
+
+ return self;
+}
+
+
+- (BOOL)isError
+{
+ return isError;
+}
+
+- (NSDate *)messageDate
+{
+ return messageDate;
+}
+
+- (NSString *)message
+{
+ return message;
+}
+
+- (void)setIsError:(BOOL)error
+{
+ isError = error;
+}
+
+- (void)setMessageDate:(NSDate *)theDate
+{
+ if (messageDate) [messageDate release];
+ messageDate = [theDate copy];
+}
+
+- (void)setMessage:(NSString *)theMessage
+{
+ if (message) [message release];
+ message = [[NSString alloc] initWithString:theMessage];
+}
+
+- (void)dealloc
+{
+ [message release], message = nil;
+ [messageDate release], messageDate = nil;
+
+ [super dealloc];
+}
+
+@end
diff --git a/Source/SPEditorTokens.h b/Source/SPEditorTokens.h
new file mode 100644
index 00000000..44d0340f
--- /dev/null
+++ b/Source/SPEditorTokens.h
@@ -0,0 +1,19 @@
+/*
+ * SPEditorTokens.h
+ * sequel-pro
+ *
+ * Created by Jakob on 3/15/09.
+ *
+ * This file defines all the tokens used for parsing the source code
+ */
+
+#define SPT_DOUBLE_QUOTED_TEXT 1
+#define SPT_SINGLE_QUOTED_TEXT 2
+#define SPT_BACKTICK_QUOTED_TEXT 3
+#define SPT_RESERVED_WORD 4
+#define SPT_COMMENT 5
+#define SPT_WHITESPACE 6
+#define SPT_WORD 7
+#define SPT_OTHER 8
+#define SPT_NUMERIC 9
+#define SPT_VARIABLE 10
diff --git a/Source/SPEditorTokens.l b/Source/SPEditorTokens.l
new file mode 100644
index 00000000..04cf1660
--- /dev/null
+++ b/Source/SPEditorTokens.l
@@ -0,0 +1,666 @@
+%{
+
+/*
+ * SPEditorTokens.l - created by Jakob on 3/15/09 for Sequel Pro
+ *
+ * This is the lex file used for syntax coloring.
+ * To add new keywords, just add a line where the other
+ * keywords are and replace spaces with {s}
+ *
+ * If you're new to lex and interested what the code below does, I found
+ * "The Lex And Yacc Page" at http://dinosaur.compilertools.net/ to be
+ * very helpful. Keep in mind that Xcode actually uses flex, the GNU
+ * version of lex. There's a very thorough Texinfo manual for flex
+ * available. (type 'info flex' in the Terminal)
+ */
+
+#import "SPEditorTokens.h"
+int utf8strlen(const char * _s);
+int yyuoffset, yyuleng;
+
+#define YY_NO_UNPUT
+
+//keep track of the current utf-8 character (not byte) offset and token length
+#define YY_USER_ACTION { yyuoffset += yyuleng; yyuleng = utf8strlen(yytext); }
+%}
+%option noyywrap
+%option case-insensitive
+
+s [ \t\n]+
+alpha [a-z_\.À-゚]
+numeric ([+-]?(([0-9]+\.[0-9]+)|([0-9]*\.[0-9]+)|([0-9]+))(e[+-]?[0-9]+)?)
+ops "+"|"-"|"*"|"/"
+word [a-z_\.0-9À-゚@]
+variable @{1,2}[a-z_\.0-9À-゚$]+
+nonword [^a-z_0-9À-゚#\n\t]
+keyworda (G(R(OUP{s}BY|ANT(S)?)|E(T_FORMAT|OMETRY(COLLECTION)?)|LOBAL)|B(Y(TE)?|TREE|I(GINT|N(LOG|ARY)|T)|O(TH|OL(EAN)?)|E(GIN|TWEEN|FORE)|LOB|ACKUP{s}TABLE)|H(IGH_PRIORITY|O(STS|UR(_(MI(NUTE|CROSECOND)|SECOND))?)|ELP|A(SH|NDLER|VING))|C(R(OSS|EATE)|H(ECK(SUM)?|A(R(SET|ACTER)?|NGE(D)?|IN))|IPHER|O(M(M(IT(TED)?|ENT)|P(RESSED|LETION|ACT))|N(S(TRAINT|ISTENT)|NECTION|CURRENT|T(RIBUTORS|INUE|AINS)|DITION|VERT)|DE|L(UMN(_FORMAT)?|LATE)|ALESCE{s}PARTITION)|U(R(RENT_(TIME(STAMP)?|DATE|USER)|SOR)|BE)|L(IENT|OSE)|A(S(CADE(D)?|E)|CHE{s}INDEX|LL))|I(GNORE|MPORT{s}TABLESPACE|S(SUER|OLATION)?|N(S(TALL|E(RT(_METHOD)?|NSITIVE))|N(O(BASE|DB)|ER)|T(1|2|8|3|O({s}(DUMP|OUT)FILE)?|4|E(RVAL|GER))?|ITIAL_SIZE|OUT|DEX(ES)?|VOKER|FILE)?|TERATE|O_THREAD|DENTIFIED|F)|D(ROP|YNAMIC|I(RECTORY|S(CARD{s}TABLESPACE|TINCT(ROW)?|K|ABLE{s}KEYS)|V)|O(UBLE)?|U(MPFILE|PLICATE|AL)|E(S(C(RIBE)?|_KEY_FILE)|C(IMAL|LARE)?|TERMINISTIC|F(INER|AULT)|L(ETE|AY(_KEY_WRITE|ED))|ALLOCATE)|A(Y(_(MI(NUTE|CROSECOND)|SECOND|HOUR))?|T(E(TIME)?|A(BASE(S)?|FILE)?)))|JOIN|E(RRORS|X(TEN(T_SIZE|DED)|I(STS|T)|P(LAIN|ANSION)|ECUTE)|SCAPE(D{s}BY)?|N(GINE(S)?|CLOSED{s}BY|D(S)?|UM|ABLE{s}KEYS)|VE(RY|NT)|LSE(IF)?|ACH)|K(ILL({s}(CONNECTION|QUERY))?|EY(S|_BLOCK_SIZE)?)|F(R(OM|AC_SECOND)|I(RST|XED|LE)|O(R(CE|EIGN)?|UND)|U(NCTION|LL(TEXT)?)|ETCH|L(OAT(8|4)?|USH)|A(ST|LSE))|A(G(GREGATE|AINST)|S(C(II)?|ENSITIVE)?|N(Y|D|ALYZE)|C(CESSIBLE|TION)|T|DD|UT(HORS|O(_INCREMENT|EXTEND_SIZE))|VG(_ROW_LENGTH)?|FTER|L(GORITHM|TER|L)))
+keywordl (R(TREE|IGHT|O(UTINE|W(S|_FORMAT)?|LL(BACK|UP))|E(GEXP|MOVE{s}PARTITIONING|BUILD{s}PARTITION|S(T(RICT|ORE{s}TABLE)|UME|ET)|NAME|COVER|TURN(S)?|ORGANIZE{s}PARTITION|D(O(_BUFFER_SIZE|FILE)|UNDANT)|P(EAT(ABLE)?|L(ICATION|ACE)|AIR)|VOKE|QUIRE|FERENCES|L(OAD|EASE|AY_(THREAD|LOG_(POS|FILE)))|A(D(S|_(ONLY|WRITE))?|L))|LIKE|ANGE)|M(I(GRATE|N(_ROWS|UTE(_(MICROSECOND|SECOND))?)|CROSECOND|DDLEINT)|O(NTH|D(IF(Y|IES)|E)?)|U(TEX|LTI(PO(INT|LYGON)|LINESTRING))|E(RGE|MORY|DIUM(BLOB|TEXT|INT)?)|A(X(_(ROWS|SIZE|CONNECTIONS_PER_HOUR|U(SER_CONNECTIONS|PDATES_PER_HOUR)|QUERIES_PER_HOUR)|VALUE)|STER(_(S(SL(_(C(IPHER|ERT|A(PATH)?)|VERIFY_SERVER_CERT|KEY))?|ERVER_ID)|HOST|CONNECT_RETRY|USER|P(ORT|ASSWORD)|LOG_(POS|FILE)))?|TCH))|N(CHAR|O(NE|_W(RITE_TO_BINLOG|AIT)|T|DEGROUP)?|DB(CLUSTER)?|U(MERIC|LL)|E(XT|W)|VARCHAR|A(ME(S)?|T(IONAL|URAL)))|O(R(DER{s}BY)?|N({s}(DUPLICATE{s}KEY{s}UPDATE)?|E(_SHOT)?|LINE)|UT(ER|FILE)?|P(TI(MIZE|ON(S|ALLY)?)|EN)|FF(SET|LINE)|LD_PASSWORD)|P(R(I(MARY|VILEGES)|OCE(SS|DURE)|E(SERVE|CISION|PARE|V))|HASE|O(INT|LYGON)|URGE|A(R(SER|TI(TION(S|ING)?|AL))|SSWORD|CK_KEYS))|QU(ICK|ERY|ARTER)|L(I(MIT|ST|NE(S(TRING)?|AR)|KE)|O(G(S|FILE({s}GROUP))|NG(BLOB|TEXT)?|C(K(S)?|AL(TIME(STAMP)?)?)|OP|W_PRIORITY|AD{s}(DATA|INDEX{s}INTO{s}CACHE))|E(SS|VEL|FT|A(DING|VE(S)?))|A(ST|NGUAGE)))
+keywords (X(OR|509|A)|S(MALLINT|SL|H(OW({s}(E(NGINE(S)?|RRORS)|M(ASTER|UTEX)|BINLOG|GRANTS|INNODB|P(RIVILEGES|ROFILE(S)?|ROCEDURE{s}CODE)|SLAVE{s}(HOSTS|STATUS)|TRIGGERS|VARIABLES|WARNINGS|PROCESSLIST|FIELDS|PLUGIN(S)?|STORAGE{s}ENGINES|TABLE{s}TYPES|CO(LUMNS|LLATION)|BINLOG{s}EVENTS))?|UTDOWN|ARE)|NAPSHOT|CHE(MA(S)?|DULE(R)?)|T(R(ING|AIGHT_JOIN)|O(RAGE|P)|A(RT(S|ING{s}BY)?|TUS))|I(GNED|MPLE)|O(ME|NAME|UNDS)|U(B(JECT|PARTITION(S)?)|SPEND|PER)|P(ECIFIC|ATIAL)|E(RIAL(IZABLE)?|SSION|NSITIVE|C(OND(_MICROSECOND)?|URITY)|T({s}(PASSWORD|NAMES|ONE_SHOT))?|PARATOR|LECT)|QL(STATE|_(B(IG_RESULT|UFFER_RESULT)|SMALL_RESULT|NO_CACHE|CA(CHE|LC_FOUND_ROWS)|T(SI_(M(INUTE|ONTH)|SECOND|HOUR|YEAR|DAY|QUARTER|FRAC_SECOND|WEEK)|HREAD))|EXCEPTION|WARNING)?|LAVE|AVEPOINT)|YEAR(_MONTH)?|T(R(IGGER(S)?|U(NCATE|E)|A(NSACTION|ILING))|H(EN|AN)|YPE|I(ME(STAMP(DIFF|ADD)?)?|NY(BLOB|TEXT|INT))|O|E(RMINATED{s}BY|XT|MP(TABLE|ORARY))|ABLE(S(PACE)?)?)|ZEROFILL|U(S(ING|E(R(_RESOURCES)?|_FRM)?|AGE)|N(SIGNED|COMMITTED|TIL|I(NSTALL|CODE|ON|QUE)|D(O(_BUFFER_SIZE|FILE)?|EFINED)|KNOWN|LOCK)|TC_(TIME(STAMP)?|DATE)|P(GRADE|DATE))|V(IEW|A(R(BINARY|YING|CHAR(ACTER)?|IABLES)|LUE(S)?))|W(RITE|H(ILE|E(RE|N))|ITH({s}PARSER)?|ORK|EEK|A(RNINGS|IT)))
+
+
+%x comment
+%x equation
+%x varequation
+%%
+\"([^"\\]|\\(.|\n))*\"? { return SPT_DOUBLE_QUOTED_TEXT; } /* double quoted strings */
+'([^'\\]|\\(.|\n))*'? { return SPT_SINGLE_QUOTED_TEXT; } /* single quoted strings */
+`[^`]*`? { return SPT_BACKTICK_QUOTED_TEXT; } /* identifier quoting */
+
+"/*" { BEGIN(comment); return SPT_COMMENT; } /* beginning of a c style comment */
+<comment>[^*]* { return SPT_COMMENT; } /* anything except * in a c cmnt */
+<comment>"*"+ { return SPT_COMMENT; } /* a range of * */
+<comment>"*"+"/" { BEGIN(INITIAL); return SPT_COMMENT; } /* a range of * with trailing /
+ Thanks to John Dickinson for publishing
+ this method of parsing C comments on
+ http://www.stillhq.com/pdfdb/000561/data.pdf
+ */
+
+#[^\n]*\n? | /* # Comments */
+--[ \t][^\n]*\n? { return SPT_COMMENT; } /* -- Comments */
+
+{variable}/{ops} { BEGIN(varequation); return SPT_VARIABLE; }/* SQL variables before operator*/
+<varequation>{ops} { BEGIN(INITIAL); return SPT_OTHER; }
+{variable} { return SPT_VARIABLE; } /* SQL variables */
+
+{numeric}/{ops} { BEGIN(equation); return SPT_NUMERIC; } /* numeric before operator */
+<equation>{ops} { BEGIN(INITIAL); return SPT_OTHER; } /* set operator after a numeric */
+{numeric}/{alpha} { return SPT_WORD; } /* catch numeric followed by char */
+
+{s}+ { return SPT_WHITESPACE; } /* ignore spaces */
+
+{keyworda} { return SPT_RESERVED_WORD; } /* all the mysql reserved words */
+{keywordl} { return SPT_RESERVED_WORD; } /* all the mysql reserved words */
+{keywords} { return SPT_RESERVED_WORD; } /* all the mysql reserved words */
+
+
+{numeric} { return SPT_NUMERIC; } /* single numeric value */
+
+{word}+ { return SPT_WORD; } /* return any word */
+
+{nonword} { return SPT_OTHER; } /* return anything else */
+
+
+
+<<EOF>> {
+ BEGIN(INITIAL); /* make sure we return to initial state when finished! */
+ yy_delete_buffer(YY_CURRENT_BUFFER);
+ return 0;
+ }
+%%
+
+#define ONEMASK ((size_t)(-1) / 0xFF)
+// adapted from http://www.daemonology.net/blog/2008-06-05-faster-utf8-strlen.html
+int utf8strlen(const char * _s)
+{
+ const char * s;
+ size_t count = 0;
+ size_t u;
+ unsigned char b;
+
+ /* Handle any initial misaligned bytes. */
+ for (s = _s; (uintptr_t)(s) & (sizeof(size_t) - 1); s++) {
+ b = *s;
+
+ /* Exit if we hit a zero byte. */
+ if (b == '\0')
+ goto done;
+
+ /* Is this byte NOT the first byte of a character? */
+ count += (b >> 7) & ((~b) >> 6);
+ }
+
+ /* Handle complete blocks. */
+ for (; ; s += sizeof(size_t)) {
+ /* Prefetch 256 bytes ahead. */
+ __builtin_prefetch(&s[256], 0, 0);
+
+ /* Grab 4 or 8 bytes of UTF-8 data. */
+ u = *(size_t *)(s);
+
+ /* Exit the loop if there are any zero bytes. */
+ if ((u - ONEMASK) & (~u) & (ONEMASK * 0x80))
+ break;
+
+ /* Count bytes which are NOT the first byte of a character. */
+ u = ((u & (ONEMASK * 0x80)) >> 7) & ((~u) >> 6);
+ count += (u * ONEMASK) >> ((sizeof(size_t) - 1) * 8);
+ }
+
+ /* Take care of any left-over bytes. */
+ for (; ; s++) {
+ b = *s;
+
+ /* Exit if we hit a zero byte. */
+ if (b == '\0')
+ break;
+
+ /* Is this byte NOT the first byte of a character? */
+ count += (b >> 7) & ((~b) >> 6);
+ }
+
+done:
+ return ((s - _s) - count);
+}
+
+/* un-optimized keywords:
+ACCESSIBLE
+ACTION
+ADD
+AFTER
+AGAINST
+AGGREGATE
+ALGORITHM
+ALL
+ALTER
+ANALYZE
+AND
+ANY
+AS
+ASC
+ASCII
+ASENSITIVE
+AT
+AUTHORS
+AUTOEXTEND_SIZE
+AUTO_INCREMENT
+AVG
+AVG_ROW_LENGTH
+BACKUP{s}TABLE
+BEFORE
+BEGIN
+BETWEEN
+BIGINT
+BINARY
+BINLOG
+BIT
+BLOB
+BOOL
+BOOLEAN
+BOTH
+BTREE
+BY
+BYTE
+CACHE{s}INDEX
+CALL
+CASCADE
+CASCADED
+CASE
+CHAIN
+CHANGE
+CHANGED
+CHAR
+CHARACTER
+CHARSET
+CHECK
+CHECKSUM
+CIPHER
+CLIENT
+CLOSE
+COALESCE{s}PARTITION
+CODE
+COLLATE
+COLUMN
+COLUMN_FORMAT
+COMMENT
+COMMIT
+COMMITTED
+COMPACT
+COMPLETION
+COMPRESSED
+CONCURRENT
+CONDITION
+CONNECTION
+CONSISTENT
+CONSTRAINT
+CONTAINS
+CONTINUE
+CONTRIBUTORS
+CONVERT
+CREATE
+CROSS
+CUBE
+CURRENT_DATE
+CURRENT_TIME
+CURRENT_TIMESTAMP
+CURRENT_USER
+CURSOR
+DATA
+DATABASE
+DATABASES
+DATAFILE
+DATE
+DATETIME
+DAY
+DAY_HOUR
+DAY_MICROSECOND
+DAY_MINUTE
+DAY_SECOND
+DEALLOCATE
+DEC
+DECIMAL
+DECLARE
+DEFAULT
+DEFINER
+DELAYED
+DELAY_KEY_WRITE
+DELETE
+DESC
+DESCRIBE
+DES_KEY_FILE
+DETERMINISTIC
+DIRECTORY
+DISABLE{s}KEYS
+DISCARD{s}TABLESPACE
+DISK
+DISTINCT
+DISTINCTROW
+DIV
+DO
+DOUBLE
+DROP
+DUAL
+DUMPFILE
+DUPLICATE
+DYNAMIC
+EACH
+ELSE
+ELSEIF
+ENABLE{s}KEYS
+ENCLOSED{s}BY
+END
+ENDS
+ENGINE
+ENGINES
+ENUM
+ERRORS
+ESCAPE
+ESCAPED{s}BY
+EVENT
+EVERY
+EXECUTE
+EXISTS
+EXIT
+EXPANSION
+EXPLAIN
+EXTENDED
+EXTENT_SIZE
+FALSE
+FAST
+FETCH
+FILE
+FIRST
+FIXED
+FLOAT
+FLOAT4
+FLOAT8
+FLUSH
+FOR
+FORCE
+FOREIGN
+FOUND
+FRAC_SECOND
+FROM
+FULL
+FULLTEXT
+FUNCTION
+GEOMETRY
+GEOMETRYCOLLECTION
+GET_FORMAT
+GLOBAL
+GRANT
+GRANTS
+GROUP{s}BY
+HANDLER
+HASH
+HAVING
+HELP
+HIGH_PRIORITY
+HOSTS
+HOUR
+HOUR_MICROSECOND
+HOUR_MINUTE
+HOUR_SECOND
+IDENTIFIED
+IF
+IGNORE
+IMPORT{s}TABLESPACE
+IN
+INDEX
+INDEXES
+INFILE
+INITIAL_SIZE
+INNER
+INNOBASE
+INNODB
+INOUT
+INSENSITIVE
+INSERT
+INSERT_METHOD
+INSTALL
+INT
+INT1
+INT2
+INT3
+INT4
+INT8
+INTEGER
+INTERVAL
+INTO({s}(DUMP|OUT)FILE)?
+INVOKER
+IO_THREAD
+IS
+ISOLATION
+ISSUER
+ITERATE
+JOIN
+KEY
+KEYS
+KEY_BLOCK_SIZE
+KILL({s}(CONNECTION|QUERY))?
+LANGUAGE
+LAST
+LEADING
+LEAVE
+LEAVES
+LEFT
+LESS
+LEVEL
+LIKE
+LIMIT
+LINEAR
+LINES
+LINESTRING
+LIST
+LOAD{s}(DATA|INDEX{s}INTO{s}CACHE)
+LOCAL
+LOCALTIME
+LOCALTIMESTAMP
+LOCK
+LOCKS
+LOGFILE({s}GROUP)
+LOGS
+LONG
+LONGBLOB
+LONGTEXT
+LOOP
+LOW_PRIORITY
+MASTER
+MASTER_CONNECT_RETRY
+MASTER_HOST
+MASTER_LOG_FILE
+MASTER_LOG_POS
+MASTER_PASSWORD
+MASTER_PORT
+MASTER_SERVER_ID
+MASTER_SSL
+MASTER_SSL_CA
+MASTER_SSL_CAPATH
+MASTER_SSL_CERT
+MASTER_SSL_CIPHER
+MASTER_SSL_KEY
+MASTER_SSL_VERIFY_SERVER_CERT
+MASTER_USER
+MATCH
+MAXVALUE
+MAX_CONNECTIONS_PER_HOUR
+MAX_QUERIES_PER_HOUR
+MAX_ROWS
+MAX_SIZE
+MAX_UPDATES_PER_HOUR
+MAX_USER_CONNECTIONS
+MEDIUM
+MEDIUMBLOB
+MEDIUMINT
+MEDIUMTEXT
+MEMORY
+MERGE
+MICROSECOND
+MIDDLEINT
+MIGRATE
+MINUTE
+MINUTE_MICROSECOND
+MINUTE_SECOND
+MIN_ROWS
+MOD
+MODE
+MODIFIES
+MODIFY
+MONTH
+MULTILINESTRING
+MULTIPOINT
+MULTIPOLYGON
+MUTEX
+NAME
+NAMES
+NATIONAL
+NATURAL
+NCHAR
+NDB
+NDBCLUSTER
+NEW
+NEXT
+NO
+NODEGROUP
+NONE
+NOT
+NO_WAIT
+NO_WRITE_TO_BINLOG
+NULL
+NUMERIC
+NVARCHAR
+OFFLINE
+OFFSET
+OLD_PASSWORD
+ONE
+ONE_SHOT
+ONLINE
+ON{s}(DUPLICATE{s}KEY{s}UPDATE)?
+OPEN
+OPTIMIZE
+OPTION
+OPTIONS
+OPTIONALLY
+OR
+ORDER{s}BY
+OUT
+OUTER
+OUTFILE
+PACK_KEYS
+PARSER
+PARTIAL
+PARTITION
+PARTITIONING
+PARTITIONS
+PASSWORD
+PHASE
+POINT
+POLYGON
+PRECISION
+PREPARE
+PRESERVE
+PREV
+PRIMARY
+PRIVILEGES
+PROCEDURE
+PROCESS
+PURGE
+QUARTER
+QUERY
+QUICK
+RANGE
+READ
+READS
+READ_ONLY
+READ_WRITE
+REAL
+REBUILD{s}PARTITION
+RECOVER
+REDOFILE
+REDO_BUFFER_SIZE
+REDUNDANT
+REFERENCES
+REGEXP
+RELAY_LOG_FILE
+RELAY_LOG_POS
+RELAY_THREAD
+RELEASE
+RELOAD
+REMOVE{s}PARTITIONING
+RENAME
+REORGANIZE{s}PARTITION
+REPAIR
+REPEAT
+REPEATABLE
+REPLACE
+REPLICATION
+REQUIRE
+RESET
+RESTORE{s}TABLE
+RESTRICT
+RESUME
+RETURN
+RETURNS
+REVOKE
+RIGHT
+RLIKE
+ROLLBACK
+ROLLUP
+ROUTINE
+ROW
+ROWS
+ROW_FORMAT
+RTREE
+SAVEPOINT
+SCHEDULE
+SCHEDULER
+SCHEMA
+SCHEMAS
+SECOND
+SECOND_MICROSECOND
+SECURITY
+SELECT
+SENSITIVE
+SEPARATOR
+SERIAL
+SERIALIZABLE
+SESSION
+SET({s}(PASSWORD|NAMES|ONE_SHOT))?
+SHARE
+SHOW({s}(E(NGINE(S)?|RRORS)|M(ASTER|UTEX)|BINLOG|GRANTS|INNODB|P(RIVILEGES|ROFILE(S)?|ROCEDURE{s}CODE)|SLAVE{s}(HOSTS|STATUS)|TRIGGERS|VARIABLES|WARNINGS|PROCESSLIST|FIELDS|PLUGIN(S)?|STORAGE{s}ENGINES|TABLE{s}TYPES|CO(LUMNS|LLATION)|BINLOG{s}EVENTS))?
+SHUTDOWN
+SIGNED
+SIMPLE
+SLAVE
+SMALLINT
+SNAPSHOT
+SOME
+SONAME
+SOUNDS
+SPATIAL
+SPECIFIC
+SQL
+SQLEXCEPTION
+SQLSTATE
+SQLWARNING
+SQL_BIG_RESULT
+SQL_BUFFER_RESULT
+SQL_CACHE
+SQL_CALC_FOUND_ROWS
+SQL_NO_CACHE
+SQL_SMALL_RESULT
+SQL_THREAD
+SQL_TSI_DAY
+SQL_TSI_FRAC_SECOND
+SQL_TSI_HOUR
+SQL_TSI_MINUTE
+SQL_TSI_MONTH
+SQL_TSI_QUARTER
+SQL_TSI_SECOND
+SQL_TSI_WEEK
+SQL_TSI_YEAR
+SSL
+START
+STARTING{s}BY
+STARTS
+STATUS
+STOP
+STORAGE
+STRAIGHT_JOIN
+STRING
+SUBJECT
+SUBPARTITION
+SUBPARTITIONS
+SUPER
+SUSPEND
+TABLE
+TABLES
+TABLESPACE
+TEMPORARY
+TEMPTABLE
+TERMINATED{s}BY
+TEXT
+THAN
+THEN
+TIME
+TIMESTAMP
+TIMESTAMPADD
+TIMESTAMPDIFF
+TINYBLOB
+TINYINT
+TINYTEXT
+TO
+TRAILING
+TRANSACTION
+TRIGGER
+TRIGGERS
+TRUE
+TRUNCATE
+TYPE
+UNCOMMITTED
+UNDEFINED
+UNDO
+UNDOFILE
+UNDO_BUFFER_SIZE
+UNICODE
+UNINSTALL
+UNION
+UNIQUE
+UNKNOWN
+UNLOCK
+UNSIGNED
+UNTIL
+UPDATE
+UPGRADE
+USAGE
+USE
+USER
+USER_RESOURCES
+USE_FRM
+USING
+UTC_DATE
+UTC_TIME
+UTC_TIMESTAMP
+VALUE
+VALUES
+VARBINARY
+VARCHAR
+VARCHARACTER
+VARIABLES
+VARYING
+VIEW
+WAIT
+WARNINGS
+WEEK
+WHEN
+WHERE
+WHILE
+WITH({s}PARSER)?
+WORK
+WRITE
+X509
+XA
+XOR
+YEAR
+YEAR_MONTH
+ZEROFILL
+*/
diff --git a/Source/SPExportController.h b/Source/SPExportController.h
new file mode 100644
index 00000000..ee2b651e
--- /dev/null
+++ b/Source/SPExportController.h
@@ -0,0 +1,91 @@
+//
+// SPExportController.h
+// sequel-pro
+//
+// Created by Ben Perry (benperry.com.au) on 21/02/09.
+//
+// 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 "CMMCPConnection.h"
+#import "CMMCPResult.h"
+
+@interface SPExportController : NSObject {
+
+ // Table Document
+ IBOutlet id tableDocumentInstance;
+ IBOutlet id tableWindow;
+
+ // Tables List
+ IBOutlet id tablesListInstance;
+
+ // Export Window
+ IBOutlet id exportWindow;
+ IBOutlet id exportToolbar;
+ IBOutlet id exportTableList;
+ IBOutlet id exportTabBar;
+ IBOutlet id exportInputMatrix;
+ IBOutlet id exportFilePerTableCheck;
+ IBOutlet id exportFilePerTableNote;
+
+ // SQL
+ IBOutlet id exportSQLIncludeStructureCheck;
+ IBOutlet id exportSQLIncludeDropSyntaxCheck;
+ IBOutlet id exportSQLIncludeErrorsCheck;
+
+ // Excel
+ IBOutlet id exportExcelSheetOrFilePerTableMatrix;
+
+ // CSV
+ IBOutlet id exportCSVIncludeFieldNamesCheck;
+ IBOutlet id exportCSVFieldsTerminatedField;
+ IBOutlet id exportCSVFieldsWrappedField;
+ IBOutlet id exportCSVFieldsEscapedField;
+ IBOutlet id exportCSVLinesTerminatedField;
+
+ // HTML
+ IBOutlet id exportHTMLIncludeStructureCheck;
+ IBOutlet id exportHTMLIncludeHeadAndBodyTagsCheck;
+
+ // XML
+ IBOutlet id exportXMLIncludeStructureCheck;
+
+ // PDF
+ IBOutlet id exportPDFIncludeStructureCheck;
+
+ // Token Name View
+ IBOutlet id tokenNameView;
+ IBOutlet id tokenNameField;
+ IBOutlet id tokenNameTokensField;
+ IBOutlet id exampleNameLabel;
+
+ // Local Variables
+ CMMCPConnection *mySQLConnection;
+ NSMutableArray *tables;
+}
+
+// Export Methods
+- (void)export;
+- (IBAction)closeSheet:(id)sender;
+
+// Utility Methods
+- (void)setConnection:(CMMCPConnection *)theConnection;
+- (void)loadTables;
+- (IBAction)switchTab:(id)sender;
+- (IBAction)switchInput:(id)sender;
+
+@end
diff --git a/Source/SPExportController.m b/Source/SPExportController.m
new file mode 100644
index 00000000..5979d166
--- /dev/null
+++ b/Source/SPExportController.m
@@ -0,0 +1,178 @@
+//
+// SPExportController.m
+// sequel-pro
+//
+// Created by Ben Perry (benperry.com.au) on 21/02/09.
+//
+// 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 "SPExportController.h"
+#import "TablesList.h"
+
+@implementation SPExportController
+
+#pragma mark -
+#pragma mark Export Methods
+
+-(void)export
+{
+ if ([NSBundle loadNibNamed:@"ExportDialog" owner:self]) {
+ [self loadTables];
+ [NSApp beginSheet:exportWindow modalForWindow:tableWindow modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:nil];
+ }
+}
+
+- (IBAction)closeSheet:(id)sender
+{
+ [NSApp endSheet:exportWindow];
+ [NSApp stopModalWithCode:[sender tag]];
+}
+
+- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo
+{
+ [sheet orderOut:self];
+}
+
+#pragma mark -
+#pragma mark Utility Methods
+
+- (void)setConnection:(CMMCPConnection *)theConnection
+{
+ mySQLConnection = theConnection;
+}
+
+- (void)loadTables
+{
+ CMMCPResult *queryResult;
+ int i;
+
+ [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
+ ]];
+ }
+
+ [exportTableList reloadData];
+}
+
+- (IBAction)switchTab:(id)sender
+{
+ if ([sender isKindOfClass:[NSToolbarItem class]]) {
+ [exportTabBar selectTabViewItemWithIdentifier:[[sender label] lowercaseString]];
+
+ [exportFilePerTableCheck setHidden:[[sender label] isEqualToString:@"Excel"]];
+ [exportFilePerTableNote setHidden:[[sender label] isEqualToString:@"Excel"]];
+ }
+}
+
+- (IBAction)switchInput:(id)sender
+{
+ if ([sender isKindOfClass:[NSMatrix class]]) {
+ [exportTableList setEnabled:([[sender selectedCell] tag] == 3)];
+ }
+}
+
+#pragma mark -
+#pragma mark Table View Datasource methods
+
+- (int)numberOfRowsInTableView:(NSTableView *)aTableView;
+{
+ return [tables count];
+}
+
+- (id)tableView:(NSTableView *)aTableView
+objectValueForTableColumn:(NSTableColumn *)aTableColumn
+ row:(int)rowIndex
+{
+ id returnObject = nil;
+
+ if ( [[aTableColumn identifier] isEqualToString:@"switch"] ) {
+ returnObject = [[tables objectAtIndex:rowIndex] objectAtIndex:0];
+ } else {
+ returnObject = [[tables objectAtIndex:rowIndex] objectAtIndex:1];
+ }
+
+ return returnObject;
+}
+
+- (void)tableView:(NSTableView *)aTableView
+ setObjectValue:(id)anObject
+ forTableColumn:(NSTableColumn *)aTableColumn
+ row:(int)rowIndex
+{
+ [[tables objectAtIndex:rowIndex] replaceObjectAtIndex:0 withObject:anObject];
+}
+
+#pragma mark -
+#pragma mark Table View Delegate methods
+
+- (BOOL)tableView:(NSTableView *)aTableView shouldSelectRow:(int)rowIndex
+{
+ return (aTableView != exportTableList);
+}
+
+- (BOOL)tableView:(NSTableView *)aTableView shouldTrackCell:(NSCell *)cell forTableColumn:(NSTableColumn *)tableColumn row:(int)row
+{
+ return (aTableView == exportTableList);
+}
+
+- (void)tableView:(NSTableView *)aTableView
+ willDisplayCell:(id)aCell
+ forTableColumn:(NSTableColumn *)aTableColumn
+ row:(int)rowIndex
+{
+ [aCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
+}
+
+#pragma mark -
+#pragma mark Toolbar Delegate Methods
+
+- (NSArray *)toolbarSelectableItemIdentifiers:(NSToolbar *)toolbar
+{
+ NSArray *array = [toolbar items];
+ NSMutableArray *items = [NSMutableArray arrayWithCapacity:6];
+ int i;
+
+ for (i = 0; i < [array count]; i++) {
+ NSToolbarItem *item = [array objectAtIndex:i];
+ [items addObject:[item itemIdentifier]];
+ }
+
+ return items;
+}
+
+- (BOOL)validateToolbarItem:(NSToolbarItem *)toolbarItem
+{
+ return YES;
+}
+
+- (id)init;
+{
+ self = [super init];
+ tables = [[NSMutableArray alloc] init];
+ return self;
+}
+
+@end
diff --git a/Source/SPFavoriteTextFieldCell.h b/Source/SPFavoriteTextFieldCell.h
new file mode 100644
index 00000000..c6c597fa
--- /dev/null
+++ b/Source/SPFavoriteTextFieldCell.h
@@ -0,0 +1,44 @@
+//
+// SPFavoriteTextFieldCell.h
+// sequel-pro
+//
+// Created by Stuart Connolly (stuconnolly.com) on Dec 29, 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 "ImageAndTextCell.h"
+
+@interface SPFavoriteTextFieldCell : ImageAndTextCell
+{
+ NSString *favoriteName;
+ NSString *favoriteHost;
+
+ NSColor *mainStringColor;
+ NSColor *subStringColor;
+}
+
+- (NSString *)favoriteName;
+- (void)setFavoriteName:(NSString *)name;
+
+- (NSString *)favoriteHost;
+- (void)setFavoriteHost:(NSString *)host;
+
+- (void)invertFontColors;
+- (void)restoreFontColors;
+
+@end
diff --git a/Source/SPFavoriteTextFieldCell.m b/Source/SPFavoriteTextFieldCell.m
new file mode 100644
index 00000000..897ad278
--- /dev/null
+++ b/Source/SPFavoriteTextFieldCell.m
@@ -0,0 +1,239 @@
+//
+// SPFavoriteTextFieldCell.m
+// sequel-pro
+//
+// Created by Stuart Connolly (stuconnolly.com) on Dec 29, 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 "SPFavoriteTextFieldCell.h"
+
+#define FAVORITE_NAME_FONT_SIZE 12.0
+
+@interface SPFavoriteTextFieldCell (PrivateAPI)
+
+- (NSAttributedString *)constructSubStringAttributedString;
+- (NSAttributedString *)attributedStringForFavoriteName;
+- (NSDictionary *)mainStringAttributedStringAttributes;
+- (NSDictionary *)subStringAttributedStringAttributes;
+
+@end
+
+@implementation SPFavoriteTextFieldCell
+
+// -------------------------------------------------------------------------------
+// init
+// -------------------------------------------------------------------------------
+- (id)init
+{
+ if ((self = [super init])) {
+ mainStringColor = [NSColor blackColor];
+ subStringColor = [NSColor grayColor];
+ }
+
+ return self;
+}
+
+// -------------------------------------------------------------------------------
+// copyWithZone:
+// -------------------------------------------------------------------------------
+- (id)copyWithZone:(NSZone *)zone
+{
+ SPFavoriteTextFieldCell *cell = (SPFavoriteTextFieldCell *)[super copyWithZone:zone];
+
+ cell->favoriteName = nil;
+
+ cell->favoriteName = [favoriteName retain];
+
+ return cell;
+}
+
+// -------------------------------------------------------------------------------
+// favoriteName
+//
+// Get the cell's favorite name.
+// -------------------------------------------------------------------------------
+- (NSString *)favoriteName
+{
+ return favoriteName;
+}
+
+// -------------------------------------------------------------------------------
+// setFavoriteName:
+//
+// Set the cell's favorite name to the supplied name.
+// -------------------------------------------------------------------------------
+- (void)setFavoriteName:(NSString *)name
+{
+ if (favoriteName != name) {
+ [favoriteName release];
+ favoriteName = [name retain];
+ }
+}
+
+// -------------------------------------------------------------------------------
+// favoriteHost
+//
+// Get the cell's favorite host.
+// -------------------------------------------------------------------------------
+- (NSString *)favoriteHost
+{
+ return favoriteHost;
+}
+
+// -------------------------------------------------------------------------------
+// setFavoriteHost:
+//
+// Set the cell's favorite host to the supplied name.
+// -------------------------------------------------------------------------------
+- (void)setFavoriteHost:(NSString *)host
+{
+ if (favoriteHost != host) {
+ [favoriteHost release];
+ favoriteHost = [host retain];
+ }
+}
+
+// -------------------------------------------------------------------------------
+// drawInteriorWithFrame:inView:
+//
+// Draws the actual cell.
+// -------------------------------------------------------------------------------
+- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
+{
+ (([self isHighlighted]) && (![[self highlightColorWithFrame:cellFrame inView:controlView] isEqualTo:[NSColor secondarySelectedControlColor]])) ? [self invertFontColors] : [self restoreFontColors];
+
+ // Construct and get the sub text attributed string
+ NSAttributedString *mainString = [[self attributedStringForFavoriteName] autorelease];
+ NSAttributedString *subString = [[self constructSubStringAttributedString] autorelease];
+
+ NSRect subFrame = NSMakeRect(0.0, 0.0, [subString size].width, [subString size].height);
+
+ // Total height of both strings with a 2 pixel separation space
+ float totalHeight = [mainString size].height + [subString size].height + 1.0;
+
+ cellFrame.origin.y += (cellFrame.size.height - totalHeight) / 2.0;
+ cellFrame.origin.x += 10.0; // Indent main string from image
+
+ // Position the sub text's frame rect
+ subFrame.origin.y = [mainString size].height + cellFrame.origin.y + 1.0;
+ subFrame.origin.x = cellFrame.origin.x;
+
+ cellFrame.size.height = totalHeight;
+
+ int i;
+ float maxWidth = cellFrame.size.width;
+ float mainStringWidth = [mainString size].width;
+ float subStringWidth = [subString size].width;
+
+ if (maxWidth < mainStringWidth) {
+ for (i = 0; i <= [mainString length]; i++) {
+ if ([[mainString attributedSubstringFromRange:NSMakeRange(0, i)] size].width >= maxWidth) {
+ mainString = [[[NSMutableAttributedString alloc] initWithString:[[[mainString attributedSubstringFromRange:NSMakeRange(0, i - 3)] string] stringByAppendingString:@"..."] attributes:[self mainStringAttributedStringAttributes]] autorelease];
+ }
+ }
+ }
+
+ if (maxWidth < subStringWidth) {
+ for (i = 0; i <= [subString length]; i++) {
+ if ([[subString attributedSubstringFromRange:NSMakeRange(0, i)] size].width >= maxWidth) {
+ subString = [[[NSMutableAttributedString alloc] initWithString:[[[subString attributedSubstringFromRange:NSMakeRange(0, i - 3)] string] stringByAppendingString:@"..."] attributes:[self subStringAttributedStringAttributes]] autorelease];
+ }
+ }
+ }
+
+ [mainString drawInRect:cellFrame];
+ [subString drawInRect:subFrame];
+}
+
+// -------------------------------------------------------------------------------
+// invertFontColors
+//
+// Inverts the displayed font colors when the cell is selected.
+// -------------------------------------------------------------------------------
+- (void)invertFontColors
+{
+ mainStringColor = [NSColor whiteColor];
+ subStringColor = [NSColor whiteColor];
+}
+
+// -------------------------------------------------------------------------------
+// restoreFontColors
+//
+// Restores the displayed font colors once the cell is no longer selected.
+// -------------------------------------------------------------------------------
+- (void)restoreFontColors
+{
+ mainStringColor = [NSColor blackColor];
+ subStringColor = [NSColor grayColor];
+}
+
+// -------------------------------------------------------------------------------
+// dealloc
+// -------------------------------------------------------------------------------
+- (void)dealloc
+{
+ [favoriteName release], favoriteName = nil;
+
+ [super dealloc];
+}
+
+@end
+
+@implementation SPFavoriteTextFieldCell (PrivateAPI)
+
+// -------------------------------------------------------------------------------
+// constructSubStringAttributedString
+//
+// Constructs the attributed string to be used as the cell's substring.
+// -------------------------------------------------------------------------------
+- (NSAttributedString *)constructSubStringAttributedString
+{
+ return [[NSAttributedString alloc] initWithString:favoriteHost attributes:[self subStringAttributedStringAttributes]];
+}
+
+// -------------------------------------------------------------------------------
+// attributedStringForFavoriteName
+//
+// Constructs the attributed string for the cell's favorite name.
+// -------------------------------------------------------------------------------
+- (NSAttributedString *)attributedStringForFavoriteName
+{
+ return [[NSAttributedString alloc] initWithString:favoriteName attributes:[self mainStringAttributedStringAttributes]];
+}
+
+// -------------------------------------------------------------------------------
+// mainStringAttributedStringAttributes
+//
+// Returns the attributes of the cell's main string.
+// -------------------------------------------------------------------------------
+- (NSDictionary *)mainStringAttributedStringAttributes
+{
+ return [NSDictionary dictionaryWithObjectsAndKeys:mainStringColor, NSForegroundColorAttributeName, [NSFont systemFontOfSize:FAVORITE_NAME_FONT_SIZE], NSFontAttributeName, nil];
+}
+
+// -------------------------------------------------------------------------------
+// subStringAttributedStringAttributes
+//
+// Returns the attributes of the cell's sub string.
+// -------------------------------------------------------------------------------
+- (NSDictionary *)subStringAttributedStringAttributes
+{
+ return [NSDictionary dictionaryWithObjectsAndKeys:subStringColor, NSForegroundColorAttributeName, [NSFont systemFontOfSize:[NSFont smallSystemFontSize]], NSFontAttributeName, nil];
+}
+
+@end
diff --git a/Source/SPGrowlController.h b/Source/SPGrowlController.h
index 2ad73e8b..7b604e30 100644
--- a/Source/SPGrowlController.h
+++ b/Source/SPGrowlController.h
@@ -30,5 +30,6 @@
// Post notification
- (void)notifyWithTitle:(NSString *)title description:(NSString *)description notificationName:(NSString *)name;
+- (void)notifyWithTitle:(NSString *)title description:(NSString *)description notificationName:(NSString *)name iconData:(NSData *)data priority:(int)priority isSticky:(BOOL)sticky clickContext:(id)clickContext;
@end
diff --git a/Source/SPGrowlController.m b/Source/SPGrowlController.m
index 3f429067..853619ff 100644
--- a/Source/SPGrowlController.m
+++ b/Source/SPGrowlController.m
@@ -26,11 +26,9 @@ static SPGrowlController *sharedGrowlController = nil;
@implementation SPGrowlController
-// -------------------------------------------------------------------------------
-// sharedGrowlController
-//
-// Returns the shared Growl controller.
-// -------------------------------------------------------------------------------
+/*
+ * Returns the shared Growl controller.
+ */
+ (SPGrowlController *)sharedGrowlController
{
@synchronized(self) {
@@ -42,9 +40,6 @@ static SPGrowlController *sharedGrowlController = nil;
return sharedGrowlController;
}
-// -------------------------------------------------------------------------------
-// allocWithZone:
-// -------------------------------------------------------------------------------
+ (id)allocWithZone:(NSZone *)zone
{
@synchronized(self) {
@@ -58,9 +53,6 @@ static SPGrowlController *sharedGrowlController = nil;
return nil; // On subsequent allocation attempts return nil
}
-// -------------------------------------------------------------------------------
-// init
-// -------------------------------------------------------------------------------
- (id)init
{
if (self = [super init]) {
@@ -70,10 +62,9 @@ static SPGrowlController *sharedGrowlController = nil;
return self;
}
-// -------------------------------------------------------------------------------
-// The following base protocol methods are implemented to ensure the singleton
-// status of this class.
-// -------------------------------------------------------------------------------
+/*
+ * The following base protocol methods are implemented to ensure the singleton status of this class.
+ */
- (id)copyWithZone:(NSZone *)zone { return self; }
@@ -85,39 +76,35 @@ static SPGrowlController *sharedGrowlController = nil;
- (void)release { }
-// -------------------------------------------------------------------------------
-// notifyWithTitle:description:notificationName:
-//
-// Posts a Growl notification using the supplied details and default values.
-// -------------------------------------------------------------------------------
+/*
+ * 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];
+ [self 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.
-// -------------------------------------------------------------------------------
+/*
+ * 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];
+ // Post notification only if preference is set
+ if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GrowlEnabled"]) {
+ [GrowlApplicationBridge notifyWithTitle:title
+ description:description
+ notificationName:name
+ iconData:data
+ priority:priority
+ isSticky:sticky
+ clickContext:clickContext];
+ }
}
@end
diff --git a/Source/SPPreferenceController.h b/Source/SPPreferenceController.h
new file mode 100644
index 00000000..cb12cde1
--- /dev/null
+++ b/Source/SPPreferenceController.h
@@ -0,0 +1,84 @@
+//
+// SPPreferenceController.h
+// sequel-pro
+//
+// Created by Stuart Connolly (stuconnolly.com) on Dec 10, 2008
+// Modified by Ben Perry (benperry.com.au) on Mar 28, 2009
+//
+// 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>
+
+@class KeyChain;
+
+@interface SPPreferenceController : NSWindowController
+{
+ IBOutlet NSWindow *preferencesWindow;
+
+ IBOutlet NSView *generalView;
+ IBOutlet NSView *notificationsView;
+ IBOutlet NSView *tablesView;
+ IBOutlet NSView *favoritesView;
+ IBOutlet NSView *autoUpdateView;
+ IBOutlet NSView *networkView;
+
+ IBOutlet NSPopUpButton *defaultFavoritePopup;
+
+ IBOutlet NSTableView *favoritesTableView;
+ IBOutlet NSArrayController *favoritesController;
+
+ IBOutlet NSTextField *nameField;
+ IBOutlet NSTextField *hostField;
+ IBOutlet NSTextField *userField;
+ IBOutlet NSTextField *databaseField;
+ IBOutlet NSSecureTextField *passwordField;
+ KeyChain *keychain;
+
+ NSToolbar *toolbar;
+
+ NSToolbarItem *generalItem;
+ NSToolbarItem *notificationsItem;
+ NSToolbarItem *tablesItem;
+ NSToolbarItem *favoritesItem;
+ NSToolbarItem *autoUpdateItem;
+ NSToolbarItem *networkItem;
+
+ NSUserDefaults *prefs;
+}
+
+- (void)applyRevisionChanges;
+
+// IBAction methods
+- (IBAction)addFavorite:(id)sender;
+- (IBAction)removeFavorite:(id)sender;
+- (IBAction)duplicateFavorite:(id)sender;
+- (IBAction)saveFavorite:(id)sender;
+- (IBAction)updateDefaultFavorite:(id)sender;
+
+// Toolbar item IBAction methods
+- (IBAction)displayGeneralPreferences:(id)sender;
+- (IBAction)displayTablePreferences:(id)sender;
+- (IBAction)displayFavoritePreferences:(id)sender;
+- (IBAction)displayNotificationPreferences:(id)sender;
+- (IBAction)displayAutoUpdatePreferences:(id)sender;
+- (IBAction)displayNetworkPreferences:(id)sender;
+
+// Other
+- (void)updateDefaultFavoritePopup;
+- (void)selectFavorites:(NSArray *)favorite;
+
+@end
diff --git a/Source/SPPreferenceController.m b/Source/SPPreferenceController.m
new file mode 100644
index 00000000..dbe9fb82
--- /dev/null
+++ b/Source/SPPreferenceController.m
@@ -0,0 +1,842 @@
+//
+// SPPreferenceController.m
+// sequel-pro
+//
+// Created by Stuart Connolly (stuconnolly.com) on Dec 10, 2008
+// Modified by Ben Perry (benperry.com.au) on Mar 28, 2009
+//
+// 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 "SPPreferenceController.h"
+#import "SPWindowAdditions.h"
+#import "SPFavoriteTextFieldCell.h"
+#import "KeyChain.h"
+
+#define FAVORITES_PB_DRAG_TYPE @"SequelProPreferencesPasteboard"
+
+#define PREFERENCE_TOOLBAR_GENERAL @"Preference Toolbar General"
+#define PREFERENCE_TOOLBAR_TABLES @"Preference Toolbar Tables"
+#define PREFERENCE_TOOLBAR_FAVORITES @"Preference Toolbar Favorites"
+#define PREFERENCE_TOOLBAR_NOTIFICATIONS @"Preference Toolbar Notifications"
+#define PREFERENCE_TOOLBAR_AUTOUPDATE @"Preference Toolbar Auto Update"
+#define PREFERENCE_TOOLBAR_NETWORK @"Preference Toolbar Network"
+
+#pragma mark -
+
+@interface SPPreferenceController (PrivateAPI)
+
+- (void)_setupToolbar;
+- (void)_resizeWindowForContentView:(NSView *)view;
+
+@end
+
+#pragma mark -
+
+@implementation SPPreferenceController
+
+// -------------------------------------------------------------------------------
+// init
+// -------------------------------------------------------------------------------
+- (id)init
+{
+ if (self = [super initWithWindowNibName:@"Preferences"]) {
+ prefs = [NSUserDefaults standardUserDefaults];
+ [self applyRevisionChanges];
+ }
+ return self;
+}
+
+// -------------------------------------------------------------------------------
+// windowDidLoad
+// -------------------------------------------------------------------------------
+- (void)windowDidLoad
+{
+ [self _setupToolbar];
+
+ keychain = [[KeyChain alloc] init];
+
+ SPFavoriteTextFieldCell *tableCell = [[[SPFavoriteTextFieldCell alloc] init] autorelease];
+
+ [tableCell setImage:[NSImage imageNamed:@"database"]];
+
+ // Replace column's NSTextFieldCell with custom SWProfileTextFieldCell
+ [[[favoritesTableView tableColumns] objectAtIndex:0] setDataCell:tableCell];
+
+ [favoritesTableView registerForDraggedTypes:[NSArray arrayWithObject:FAVORITES_PB_DRAG_TYPE]];
+
+ [favoritesTableView selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
+ [favoritesTableView reloadData];
+
+ [self updateDefaultFavoritePopup];
+}
+
+#pragma mark -
+#pragma mark Preferences upgrade routine
+
+// -------------------------------------------------------------------------------
+// applyRevisionChanges
+// Checks the revision number, applies any preference upgrades, and updates to
+// latest revision.
+// Currently uses both lastUsedVersion and LastUsedVersion for <0.9.5 compatibility.
+// -------------------------------------------------------------------------------
+- (void)applyRevisionChanges
+{
+ int currentVersionNumber, recordedVersionNumber = 0;
+
+ // Get the current bundle version number (the SVN build number) for per-version upgrades
+ currentVersionNumber = [[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"] intValue];
+
+ // Get the current revision
+ if ([prefs objectForKey:@"lastUsedVersion"]) recordedVersionNumber = [[prefs objectForKey:@"lastUsedVersion"] intValue];
+ if ([prefs objectForKey:@"LastUsedVersion"]) recordedVersionNumber = [[prefs objectForKey:@"LastUsedVersion"] intValue];
+
+ // Skip processing if the current version matches or is less than recorded version
+ if (currentVersionNumber <= recordedVersionNumber) return;
+
+ // If no recorded version, update to current revision and skip processing
+ if (!recordedVersionNumber) {
+ [prefs setObject:[NSNumber numberWithInt:currentVersionNumber] forKey:@"LastUsedVersion"];
+ return;
+ }
+
+ // For versions prior to r336 (0.9.4), where column widths have been saved, walk through them and remove
+ // any table widths set to 15 or less (fix for mangled columns caused by Issue #140)
+ if (recordedVersionNumber < 336 && [prefs objectForKey:@"tableColumnWidths"] != nil) {
+ NSEnumerator *databaseEnumerator, *tableEnumerator, *columnEnumerator;
+ NSString *databaseKey, *tableKey, *columnKey;
+ NSMutableDictionary *newDatabase, *newTable;
+ float columnWidth;
+ NSMutableDictionary *newTableColumnWidths = [[NSMutableDictionary alloc] init];
+
+ databaseEnumerator = [[prefs objectForKey:@"tableColumnWidths"] keyEnumerator];
+ while (databaseKey = [databaseEnumerator nextObject]) {
+ newDatabase = [[NSMutableDictionary alloc] init];
+ tableEnumerator = [[[prefs objectForKey:@"tableColumnWidths"] objectForKey:databaseKey] keyEnumerator];
+ while (tableKey = [tableEnumerator nextObject]) {
+ newTable = [[NSMutableDictionary alloc] init];
+ columnEnumerator = [[[[prefs objectForKey:@"tableColumnWidths"] objectForKey:databaseKey] objectForKey:tableKey] keyEnumerator];
+ while (columnKey = [columnEnumerator nextObject]) {
+ columnWidth = [[[[[prefs objectForKey:@"tableColumnWidths"] objectForKey:databaseKey] objectForKey:tableKey] objectForKey:columnKey] floatValue];
+ if (columnWidth >= 15) {
+ [newTable setObject:[NSNumber numberWithFloat:columnWidth] forKey:[NSString stringWithString:columnKey]];
+ }
+ }
+ if ([newTable count]) {
+ [newDatabase setObject:[NSDictionary dictionaryWithDictionary:newTable] forKey:[NSString stringWithString:tableKey]];
+ }
+ [newTable release];
+ }
+ if ([newDatabase count]) {
+ [newTableColumnWidths setObject:[NSDictionary dictionaryWithDictionary:newDatabase] forKey:[NSString stringWithString:databaseKey]];
+ }
+ [newDatabase release];
+ }
+ [prefs setObject:[NSDictionary dictionaryWithDictionary:newTableColumnWidths] forKey:@"tableColumnWidths"];
+ [newTableColumnWidths release];
+ }
+
+ // For versions prior to r561 (0.9.5), migrate old pref keys where they exist to the new pref keys
+ if (recordedVersionNumber < 561) {
+ NSEnumerator *keyEnumerator;
+ NSString *oldKey, *newKey;
+ NSDictionary *keysToUpgrade = [NSDictionary dictionaryWithObjectsAndKeys:
+ @"encoding", @"DefaultEncoding",
+ @"useMonospacedFonts", @"UseMonospacedFonts",
+ @"reloadAfterAdding", @"ReloadAfterAddingRow",
+ @"reloadAfterEditing", @"ReloadAfterEditingRow",
+ @"reloadAfterRemoving", @"ReloadAfterRemovingRow",
+ @"dontShowBlob", @"LoadBlobsAsNeeded",
+ @"fetchRowCount", @"FetchCorrectRowCount",
+ @"limitRows", @"LimitResults",
+ @"limitRowsValue", @"LimitResultsValue",
+ @"nullValue", @"NullValue",
+ @"showError", @"ShowNoAffectedRowsError",
+ @"connectionTimeout", @"ConnectionTimeoutValue",
+ @"keepAliveInterval", @"KeepAliveInterval",
+ @"lastFavoriteIndex", @"LastFavoriteIndex",
+ nil];
+
+ keyEnumerator = [keysToUpgrade keyEnumerator];
+ while (newKey = [keyEnumerator nextObject]) {
+ oldKey = [keysToUpgrade objectForKey:newKey];
+ if ([prefs objectForKey:oldKey]) {
+ [prefs setObject:[prefs objectForKey:oldKey] forKey:newKey];
+ [prefs removeObjectForKey:oldKey];
+ }
+ }
+
+ // Remove outdated keys
+ [prefs removeObjectForKey:@"lastUsedVersion"];
+ [prefs removeObjectForKey:@"version"];
+ }
+
+ // For versions prior to r567 (0.9.5), add a timestamp-based identifier to favorites and keychain entries
+ if (recordedVersionNumber < 567 && [prefs objectForKey:@"favorites"]) {
+ int i;
+ NSMutableArray *favoritesArray = [NSMutableArray arrayWithArray:[prefs objectForKey:@"favorites"]];
+ NSMutableDictionary *favorite;
+ NSString *password, *keychainName, *keychainAccount;
+ KeyChain *upgradeKeychain = [[KeyChain alloc] init];
+
+ // Cycle through the favorites, generating a timestamp-derived ID for each and renaming associated keychain items.
+ for (i = 0; i < [favoritesArray count]; i++) {
+ favorite = [NSMutableDictionary dictionaryWithDictionary:[favoritesArray objectAtIndex:i]];
+ if ([favorite objectForKey:@"id"]) continue;
+ [favorite setObject:[NSNumber numberWithInt:[[NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]] hash]] forKey:@"id"];
+ keychainName = [NSString stringWithFormat:@"Sequel Pro : %@", [favorite objectForKey:@"name"]];
+ keychainAccount = [NSString stringWithFormat:@"%@@%@/%@",
+ [favorite objectForKey:@"user"], [favorite objectForKey:@"host"], [favorite objectForKey:@"database"]];
+ password = [upgradeKeychain getPasswordForName:keychainName account:keychainAccount];
+ [upgradeKeychain deletePasswordForName:keychainName account:keychainAccount];
+ if (password && [password length]) {
+ keychainName = [NSString stringWithFormat:@"Sequel Pro : %@ (%i)", [favorite objectForKey:@"name"], [[favorite objectForKey:@"id"] intValue]];
+ [upgradeKeychain addPassword:password forName:keychainName account:keychainAccount];
+ }
+ [favoritesArray replaceObjectAtIndex:i withObject:[NSDictionary dictionaryWithDictionary:favorite]];
+ }
+ [prefs setObject:[NSArray arrayWithArray:favoritesArray] forKey:@"favorites"];
+ [upgradeKeychain release];
+ password = nil;
+ }
+
+ // Update the prefs revision
+ [prefs setObject:[NSNumber numberWithInt:currentVersionNumber] forKey:@"LastUsedVersion"];
+}
+
+#pragma mark -
+#pragma mark IBAction methods
+
+// -------------------------------------------------------------------------------
+// addFavorite:
+// -------------------------------------------------------------------------------
+- (IBAction)addFavorite:(id)sender
+{
+ NSNumber *favoriteid = [NSNumber numberWithInt:[[NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]] hash]];
+
+ // Create default favorite
+ NSMutableDictionary *favorite = [NSMutableDictionary dictionaryWithObjects:[NSArray arrayWithObjects:@"New Favorite", @"", @"", @"", @"", @"", favoriteid, nil]
+ forKeys:[NSArray arrayWithObjects:@"name", @"host", @"socket", @"user", @"port", @"database", @"id", nil]];
+
+ [favoritesController addObject:favorite];
+
+ [favoritesTableView reloadData];
+ [self updateDefaultFavoritePopup];
+}
+
+// -------------------------------------------------------------------------------
+// removeFavorite:
+// -------------------------------------------------------------------------------
+- (IBAction)removeFavorite:(id)sender
+{
+ if ([favoritesTableView numberOfSelectedRows] == 1) {
+
+ // Get selected favorite's details
+ NSString *name = [favoritesController valueForKeyPath:@"selection.name"];
+ NSString *user = [favoritesController valueForKeyPath:@"selection.user"];
+ NSString *host = [favoritesController valueForKeyPath:@"selection.host"];
+ NSString *database = [favoritesController valueForKeyPath:@"selection.database"];
+ int favoriteid = [[favoritesController valueForKeyPath:@"selection.id"] intValue];
+
+ // Remove passwords from the Keychain
+ [keychain deletePasswordForName:[NSString stringWithFormat:@"Sequel Pro : %@ (%i)", name, favoriteid]
+ account:[NSString stringWithFormat:@"%@@%@/%@", user, host, database]];
+ [keychain deletePasswordForName:[NSString stringWithFormat:@"Sequel Pro SSHTunnel : %@ (%i)", name, favoriteid]
+ account:[NSString stringWithFormat:@"%@@%@/%@", user, host, database]];
+
+ // Reset last used favorite
+ if ([favoritesTableView selectedRow] == [prefs integerForKey:@"LastFavoriteIndex"]) {
+ [prefs setInteger:0 forKey:@"LastFavoriteIndex"];
+ }
+
+ // Reset default favorite
+ if ([favoritesTableView selectedRow] == [prefs integerForKey:@"DefaultFavorite"]) {
+ [prefs setInteger:[prefs integerForKey:@"LastFavoriteIndex"] forKey:@"DefaultFavorite"];
+ }
+
+ [favoritesController removeObjectAtArrangedObjectIndex:[favoritesTableView selectedRow]];
+
+ [favoritesTableView reloadData];
+ [self updateDefaultFavoritePopup];
+ }
+}
+
+// -------------------------------------------------------------------------------
+// duplicateFavorite:
+// -------------------------------------------------------------------------------
+- (IBAction)duplicateFavorite:(id)sender
+{
+ if ([favoritesTableView numberOfSelectedRows] == 1) {
+ NSString *keychainName, *keychainAccount, *password;
+ NSMutableDictionary *favorite = [NSMutableDictionary dictionaryWithDictionary:[[favoritesController arrangedObjects] objectAtIndex:[favoritesTableView selectedRow]]];
+ NSNumber *favoriteid = [NSNumber numberWithInt:[[NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]] hash]];
+
+ // Select the keychain password for duplication
+ keychainName = [NSString stringWithFormat:@"Sequel Pro : %@ (%i)", [favorite objectForKey:@"name"], [[favorite objectForKey:@"id"] intValue]];
+ keychainAccount = [NSString stringWithFormat:@"%@@%@/%@",
+ [favorite objectForKey:@"user"], [favorite objectForKey:@"host"], [favorite objectForKey:@"database"]];
+ password = [keychain getPasswordForName:keychainName account:keychainAccount];
+
+ // Update the unique ID
+ [favorite setObject:favoriteid forKey:@"id"];
+
+ // Alter the name for clarity
+ [favorite setObject:[NSString stringWithFormat:@"%@ Copy", [favorite objectForKey:@"name"]] forKey:@"name"];
+
+ // Create a new keychain item if appropriate
+ if (password && [password length]) {
+ keychainName = [NSString stringWithFormat:@"Sequel Pro : %@ (%i)", [favorite objectForKey:@"name"], [[favorite objectForKey:@"id"] intValue]];
+ [keychain addPassword:password forName:keychainName account:keychainAccount];
+ }
+ password = nil;
+
+ [favoritesController addObject:favorite];
+
+ [favoritesTableView reloadData];
+ [self updateDefaultFavoritePopup];
+ }
+}
+
+// -------------------------------------------------------------------------------
+// saveFavorite:
+// -------------------------------------------------------------------------------
+- (IBAction)saveFavorite:(id)sender
+{
+
+}
+
+// -------------------------------------------------------------------------------
+// updateDefaultFavorite:
+// -------------------------------------------------------------------------------
+- (IBAction)updateDefaultFavorite:(id)sender
+{
+ if ([defaultFavoritePopup indexOfSelectedItem] == 0) {
+ [prefs setBool:YES forKey:@"SelectLastFavoriteUsed"];
+ } else {
+ [prefs setBool:NO forKey:@"SelectLastFavoriteUsed"];
+
+ // Minus 2 from index to account for the "Last Used" and separator items
+ [prefs setInteger:[defaultFavoritePopup indexOfSelectedItem]-2 forKey:@"DefaultFavorite"];
+ }
+}
+
+#pragma mark -
+#pragma mark Toolbar item IBAction methods
+
+// -------------------------------------------------------------------------------
+// displayGeneralPreferences:
+// -------------------------------------------------------------------------------
+- (IBAction)displayGeneralPreferences:(id)sender
+{
+ [toolbar setSelectedItemIdentifier:PREFERENCE_TOOLBAR_GENERAL];
+ [self _resizeWindowForContentView:generalView];
+}
+
+// -------------------------------------------------------------------------------
+// displayTablePreferences:
+// -------------------------------------------------------------------------------
+- (IBAction)displayTablePreferences:(id)sender
+{
+ [toolbar setSelectedItemIdentifier:PREFERENCE_TOOLBAR_TABLES];
+ [self _resizeWindowForContentView:tablesView];
+}
+
+// -------------------------------------------------------------------------------
+// displayFavoritePreferences:
+// -------------------------------------------------------------------------------
+- (IBAction)displayFavoritePreferences:(id)sender
+{
+ [toolbar setSelectedItemIdentifier:PREFERENCE_TOOLBAR_FAVORITES];
+ [self _resizeWindowForContentView:favoritesView];
+
+ // Set the default favorite popup back to preference
+ if (sender == [defaultFavoritePopup lastItem]) {
+ if (![prefs boolForKey:@"SelectLastFavoriteUsed"]) {
+ [defaultFavoritePopup selectItemAtIndex:[prefs integerForKey:@"DefaultFavorite"]+2];
+ } else {
+ [defaultFavoritePopup selectItemAtIndex:0];
+ }
+ }
+}
+
+// -------------------------------------------------------------------------------
+// displayNotificationPreferences:
+// -------------------------------------------------------------------------------
+- (IBAction)displayNotificationPreferences:(id)sender
+{
+ [toolbar setSelectedItemIdentifier:PREFERENCE_TOOLBAR_NOTIFICATIONS];
+ [self _resizeWindowForContentView:notificationsView];
+}
+
+// -------------------------------------------------------------------------------
+// displayAutoUpdatePreferences:
+// -------------------------------------------------------------------------------
+- (IBAction)displayAutoUpdatePreferences:(id)sender
+{
+ [toolbar setSelectedItemIdentifier:PREFERENCE_TOOLBAR_AUTOUPDATE];
+ [self _resizeWindowForContentView:autoUpdateView];
+}
+
+// -------------------------------------------------------------------------------
+// displayNetworkPreferences:
+// -------------------------------------------------------------------------------
+- (IBAction)displayNetworkPreferences:(id)sender
+{
+ [toolbar setSelectedItemIdentifier:PREFERENCE_TOOLBAR_NETWORK];
+ [self _resizeWindowForContentView:networkView];
+}
+
+#pragma mark -
+#pragma mark TableView datasource methods
+
+// -------------------------------------------------------------------------------
+// numberOfRowsInTableView:
+// -------------------------------------------------------------------------------
+- (int)numberOfRowsInTableView:(NSTableView *)aTableView
+{
+ return [[favoritesController arrangedObjects] count];
+}
+
+// -------------------------------------------------------------------------------
+// tableView:objectValueForTableColumn:row:
+// -------------------------------------------------------------------------------
+- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex
+{
+ return [[[favoritesController arrangedObjects] objectAtIndex:rowIndex] objectForKey:[aTableColumn identifier]];
+}
+
+#pragma mark -
+#pragma mark TableView drag & drop datasource methods
+
+// -------------------------------------------------------------------------------
+// tableView:writeRows:toPasteboard:
+// -------------------------------------------------------------------------------
+- (BOOL)tableView:(NSTableView *)tv writeRows:(NSArray *)rows toPasteboard:(NSPasteboard *)pboard
+{
+ int originalRow;
+ NSArray *pboardTypes;
+
+ if ([rows count] == 1) {
+ pboardTypes = [NSArray arrayWithObject:FAVORITES_PB_DRAG_TYPE];
+ originalRow = [[rows objectAtIndex:0] intValue];
+
+ [pboard declareTypes:pboardTypes owner:nil];
+ [pboard setString:[[NSNumber numberWithInt:originalRow] stringValue] forType:FAVORITES_PB_DRAG_TYPE];
+
+ return YES;
+ }
+ else {
+ return NO;
+ }
+}
+
+// -------------------------------------------------------------------------------
+// tableView:validateDrop:proposedRow:proposedDropOperation:
+// -------------------------------------------------------------------------------
+- (NSDragOperation)tableView:(NSTableView *)tv validateDrop:(id <NSDraggingInfo>)info proposedRow:(int)row proposedDropOperation:(NSTableViewDropOperation)operation
+{
+ int originalRow;
+ NSArray *pboardTypes = [[info draggingPasteboard] types];
+
+ if (([pboardTypes count] > 1) && (row != -1)) {
+ if (([pboardTypes containsObject:FAVORITES_PB_DRAG_TYPE]) && (operation == NSTableViewDropAbove)) {
+ originalRow = [[[info draggingPasteboard] stringForType:FAVORITES_PB_DRAG_TYPE] intValue];
+
+ if ((row != originalRow) && (row != (originalRow + 1))) {
+ return NSDragOperationMove;
+ }
+ }
+ }
+
+ return NSDragOperationNone;
+}
+
+// -------------------------------------------------------------------------------
+// tableView:acceptDrop:row:dropOperation:
+// -------------------------------------------------------------------------------
+- (BOOL)tableView:(NSTableView *)tv acceptDrop:(id <NSDraggingInfo>)info row:(int)row dropOperation:(NSTableViewDropOperation)operation
+{
+ int originalRow;
+ int destinationRow;
+ NSMutableDictionary *draggedRow;
+
+ originalRow = [[[info draggingPasteboard] stringForType:FAVORITES_PB_DRAG_TYPE] intValue];
+ destinationRow = row;
+
+ if (destinationRow > originalRow) {
+ destinationRow--;
+ }
+
+ draggedRow = [NSMutableDictionary dictionaryWithDictionary:[[favoritesController arrangedObjects] objectAtIndex:originalRow]];
+
+ [favoritesController removeObjectAtArrangedObjectIndex:originalRow];
+ [favoritesController insertObject:draggedRow atArrangedObjectIndex:destinationRow];
+
+ [favoritesTableView reloadData];
+ [favoritesTableView selectRowIndexes:[NSIndexSet indexSetWithIndex:destinationRow] byExtendingSelection:NO];
+
+ // Update default favorite to take on new value
+ if ([prefs integerForKey:@"LastFavoriteIndex"] == originalRow) {
+ [prefs setInteger:destinationRow forKey:@"LastFavoriteIndex"];
+ }
+
+ // Update default favorite to take on new value
+ if ([prefs integerForKey:@"DefaultFavorite"] == originalRow) {
+ [prefs setInteger:destinationRow forKey:@"DefaultFavorite"];
+ }
+ [self updateDefaultFavoritePopup];
+
+ return YES;
+}
+
+
+#pragma mark -
+#pragma mark TableView delegate methods
+
+// -------------------------------------------------------------------------------
+// tableView:willDisplayCell:forTableColumn:row:
+// -------------------------------------------------------------------------------
+- (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(int)index
+{
+ if ([cell isKindOfClass:[SPFavoriteTextFieldCell class]]) {
+ [cell setFavoriteName:[[[favoritesController arrangedObjects] objectAtIndex:index] objectForKey:@"name"]];
+ [cell setFavoriteHost:[[[favoritesController arrangedObjects] objectAtIndex:index] objectForKey:@"host"]];
+ }
+}
+
+// -------------------------------------------------------------------------------
+// tableViewSelectionDidChange:
+// -------------------------------------------------------------------------------
+- (void)tableViewSelectionDidChange:(NSNotification *)notification
+{
+ if ([[favoritesTableView selectedRowIndexes] count] > 0) {
+ [favoritesController setSelectionIndexes:[favoritesTableView selectedRowIndexes]];
+ }
+
+ // If no selection is present, blank the field.
+ if ([[favoritesTableView selectedRowIndexes] count] == 0) {
+ [passwordField setStringValue:@""];
+ return;
+ }
+
+ // Otherwise retrieve and set the password.
+ NSString *keychainName = [NSString stringWithFormat:@"Sequel Pro : %@ (%i)", [favoritesController valueForKeyPath:@"selection.name"], [[favoritesController valueForKeyPath:@"selection.id"] intValue]];
+ NSString *keychainAccount = [NSString stringWithFormat:@"%@@%@/%@",
+ [favoritesController valueForKeyPath:@"selection.user"],
+ [favoritesController valueForKeyPath:@"selection.host"],
+ [favoritesController valueForKeyPath:@"selection.database"]];
+
+ [passwordField setStringValue:[keychain getPasswordForName:keychainName account:keychainAccount]];
+}
+
+#pragma mark -
+#pragma mark Toolbar delegate methods
+
+// -------------------------------------------------------------------------------
+// toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:
+// -------------------------------------------------------------------------------
+- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
+{
+ if ([itemIdentifier isEqualToString:PREFERENCE_TOOLBAR_GENERAL]) {
+ return generalItem;
+ }
+ else if ([itemIdentifier isEqualToString:PREFERENCE_TOOLBAR_TABLES]) {
+ return tablesItem;
+ }
+ else if ([itemIdentifier isEqualToString:PREFERENCE_TOOLBAR_FAVORITES]) {
+ return favoritesItem;
+ }
+ else if ([itemIdentifier isEqualToString:PREFERENCE_TOOLBAR_NOTIFICATIONS]) {
+ return notificationsItem;
+ }
+ else if ([itemIdentifier isEqualToString:PREFERENCE_TOOLBAR_AUTOUPDATE]) {
+ return autoUpdateItem;
+ }
+ else if ([itemIdentifier isEqualToString:PREFERENCE_TOOLBAR_NETWORK]) {
+ return networkItem;
+ }
+
+ return [[[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier] autorelease];
+}
+
+// -------------------------------------------------------------------------------
+// toolbarAllowedItemIdentifiers:
+// -------------------------------------------------------------------------------
+- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar
+{
+ return [NSArray arrayWithObjects:PREFERENCE_TOOLBAR_GENERAL, PREFERENCE_TOOLBAR_TABLES, PREFERENCE_TOOLBAR_FAVORITES, PREFERENCE_TOOLBAR_NOTIFICATIONS, PREFERENCE_TOOLBAR_AUTOUPDATE, PREFERENCE_TOOLBAR_NETWORK, nil];
+}
+
+// -------------------------------------------------------------------------------
+// toolbarDefaultItemIdentifiers:
+// -------------------------------------------------------------------------------
+- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar
+{
+ return [NSArray arrayWithObjects:PREFERENCE_TOOLBAR_GENERAL, PREFERENCE_TOOLBAR_TABLES, PREFERENCE_TOOLBAR_FAVORITES, PREFERENCE_TOOLBAR_NOTIFICATIONS, PREFERENCE_TOOLBAR_AUTOUPDATE, PREFERENCE_TOOLBAR_NETWORK, nil];
+}
+
+// -------------------------------------------------------------------------------
+// toolbarSelectableItemIdentifiers:
+// -------------------------------------------------------------------------------
+- (NSArray *)toolbarSelectableItemIdentifiers:(NSToolbar *)toolbar
+{
+ return [NSArray arrayWithObjects:PREFERENCE_TOOLBAR_GENERAL, PREFERENCE_TOOLBAR_TABLES, PREFERENCE_TOOLBAR_FAVORITES, PREFERENCE_TOOLBAR_NOTIFICATIONS, PREFERENCE_TOOLBAR_AUTOUPDATE, PREFERENCE_TOOLBAR_NETWORK, nil];
+}
+
+#pragma mark -
+#pragma mark SplitView delegate methods
+
+// -------------------------------------------------------------------------------
+// splitView:constrainMaxCoordinate:ofSubviewAt:
+// -------------------------------------------------------------------------------
+- (float)splitView:(NSSplitView *)sender constrainMaxCoordinate:(float)proposedMax ofSubviewAt:(int)offset
+{
+ return (proposedMax - 220);
+}
+
+// -------------------------------------------------------------------------------
+// splitView:constrainMinCoordinate:ofSubviewAt:
+// -------------------------------------------------------------------------------
+- (float)splitView:(NSSplitView *)sender constrainMinCoordinate:(float)proposedMin ofSubviewAt:(int)offset
+{
+ return (proposedMin + 100);
+}
+
+
+#pragma mark -
+#pragma mark TextField delegate methods
+
+// -------------------------------------------------------------------------------
+// control:textShouldEndEditing:
+// Trap editing end notifications and use them to update the keychain password
+// appropriately when name, host, user, password or database changes.
+// -------------------------------------------------------------------------------
+- (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor
+{
+ NSString *oldKeychainName, *newKeychainName;
+ NSString *oldKeychainAccount, *newKeychainAccount;
+ NSString *oldPassword;
+
+ // Only proceed for name, host, user or database changes
+ if (control != nameField && control != hostField && control != userField && control != passwordField && control != databaseField)
+ return YES;
+
+ // Set the current keychain name and account strings
+ oldKeychainName = [NSString stringWithFormat:@"Sequel Pro : %@ (%i)", [favoritesController valueForKeyPath:@"selection.name"], [[favoritesController valueForKeyPath:@"selection.id"] intValue]];
+ oldKeychainAccount = [NSString stringWithFormat:@"%@@%@/%@",
+ [favoritesController valueForKeyPath:@"selection.user"],
+ [favoritesController valueForKeyPath:@"selection.host"],
+ [favoritesController valueForKeyPath:@"selection.database"]];
+
+ // Retrieve the old password
+ oldPassword = [keychain getPasswordForName:oldKeychainName account:oldKeychainAccount];
+
+ // If no details have changed, skip processing
+ if ([nameField stringValue] == [favoritesController valueForKeyPath:@"selection.name"]
+ && [hostField stringValue] == [favoritesController valueForKeyPath:@"selection.host"]
+ && [userField stringValue] == [favoritesController valueForKeyPath:@"selection.user"]
+ && [databaseField stringValue] == [favoritesController valueForKeyPath:@"selection.database"]
+ && [passwordField stringValue] == oldPassword) {
+ oldPassword = nil;
+ return YES;
+ }
+ oldPassword = nil;
+
+ // Set up the new keychain name and account strings
+ newKeychainName = [NSString stringWithFormat:@"Sequel Pro : %@ (%i)", [nameField stringValue], [[favoritesController valueForKeyPath:@"selection.id"] intValue]];
+ newKeychainAccount = [NSString stringWithFormat:@"%@@%@/%@",
+ [userField stringValue],
+ [hostField stringValue],
+ [databaseField stringValue]];
+
+ // Delete the old keychain item
+ [keychain deletePasswordForName:oldKeychainName account:oldKeychainAccount];
+
+ // Add the new keychain item if the password field has a value
+ if ([[passwordField stringValue] length])
+ [keychain addPassword:[passwordField stringValue] forName:newKeychainName account:newKeychainAccount];
+
+ // Proceed with editing
+ return YES;
+}
+
+#pragma mark -
+#pragma mark Window delegate methods
+
+// -------------------------------------------------------------------------------
+// windowWillClose:
+// Trap window close notifications and use them to ensure changes are saved.
+// -------------------------------------------------------------------------------
+- (void)windowWillClose:(NSNotification *)notification
+{
+ // Mark the currently selected field in the window as having finished editing, to trigger saves.
+ if ([preferencesWindow firstResponder])
+ [preferencesWindow endEditingFor:[preferencesWindow firstResponder]];
+}
+
+#pragma mark -
+#pragma mark Other
+
+// -------------------------------------------------------------------------------
+// updateDefaultFavoritePopup:
+//
+// Build the default favorite popup button
+// -------------------------------------------------------------------------------
+- (void)updateDefaultFavoritePopup;
+{
+ [defaultFavoritePopup removeAllItems];
+
+ // Use the last used favorite
+ [defaultFavoritePopup addItemWithTitle:@"Last Used"];
+ [[defaultFavoritePopup menu] addItem:[NSMenuItem separatorItem]];
+
+ // Load in current favorites
+ [defaultFavoritePopup addItemsWithTitles:[[favoritesController arrangedObjects] valueForKeyPath:@"name"]];
+
+ // Add item to switch to edit favorites pane
+ [[defaultFavoritePopup menu] addItem:[NSMenuItem separatorItem]];
+ [defaultFavoritePopup addItemWithTitle:@"Edit Favorites…"];
+ [[[defaultFavoritePopup menu] itemWithTitle:@"Edit Favorites…"] setAction:@selector(displayFavoritePreferences:)];
+ [[[defaultFavoritePopup menu] itemWithTitle:@"Edit Favorites…"] setTarget:self];
+
+ // Select the default favorite from prefs
+ if (![prefs boolForKey:@"SelectLastFavoriteUsed"]) {
+ [defaultFavoritePopup selectItemAtIndex:[prefs integerForKey:@"DefaultFavorite"] + 2];
+ } else {
+ [defaultFavoritePopup selectItemAtIndex:0];
+ }
+}
+
+// -------------------------------------------------------------------------------
+// selectFavorite:
+//
+// Selects the specified favorite(s) in the favorites list
+// -------------------------------------------------------------------------------
+- (void)selectFavorites:(NSArray *)favorites
+{
+ [favoritesController setSelectedObjects:favorites];
+}
+
+// -------------------------------------------------------------------------------
+// dealloc
+// -------------------------------------------------------------------------------
+- (void)dealloc
+{
+ [keychain release], keychain = nil;
+
+ [super dealloc];
+}
+
+@end
+
+
+
+#pragma mark -
+
+@implementation SPPreferenceController (PrivateAPI)
+
+// -------------------------------------------------------------------------------
+// _setupToolbar
+//
+// Constructs the preferences' window toolbar.
+// -------------------------------------------------------------------------------
+- (void)_setupToolbar
+{
+ toolbar = [[[NSToolbar alloc] initWithIdentifier:@"Preference Toolbar"] autorelease];
+
+ // General preferences
+ generalItem = [[NSToolbarItem alloc] initWithItemIdentifier:PREFERENCE_TOOLBAR_GENERAL];
+
+ [generalItem setLabel:NSLocalizedString(@"General", @"")];
+ [generalItem setImage:[NSImage imageNamed:@"toolbar-preferences-general"]];
+ [generalItem setTarget:self];
+ [generalItem setAction:@selector(displayGeneralPreferences:)];
+
+ // Table preferences
+ tablesItem = [[NSToolbarItem alloc] initWithItemIdentifier:PREFERENCE_TOOLBAR_TABLES];
+
+ [tablesItem setLabel:NSLocalizedString(@"Tables", @"")];
+ [tablesItem setImage:[NSImage imageNamed:@"toolbar-preferences-tables"]];
+ [tablesItem setTarget:self];
+ [tablesItem setAction:@selector(displayTablePreferences:)];
+
+ // Favorite preferences
+ favoritesItem = [[NSToolbarItem alloc] initWithItemIdentifier:PREFERENCE_TOOLBAR_FAVORITES];
+
+ [favoritesItem setLabel:NSLocalizedString(@"Favorites", @"")];
+ [favoritesItem setImage:[NSImage imageNamed:@"toolbar-preferences-favorites"]];
+ [favoritesItem setTarget:self];
+ [favoritesItem setAction:@selector(displayFavoritePreferences:)];
+
+ // Notification preferences
+ notificationsItem = [[NSToolbarItem alloc] initWithItemIdentifier:PREFERENCE_TOOLBAR_NOTIFICATIONS];
+
+ [notificationsItem setLabel:NSLocalizedString(@"Notifications", @"")];
+ [notificationsItem setImage:[NSImage imageNamed:@"toolbar-preferences-notifications"]];
+ [notificationsItem setTarget:self];
+ [notificationsItem setAction:@selector(displayNotificationPreferences:)];
+
+ // AutoUpdate preferences
+ autoUpdateItem = [[NSToolbarItem alloc] initWithItemIdentifier:PREFERENCE_TOOLBAR_AUTOUPDATE];
+
+ [autoUpdateItem setLabel:NSLocalizedString(@"Auto Update", @"")];
+ [autoUpdateItem setImage:[NSImage imageNamed:@"toolbar-preferences-autoupdate"]];
+ [autoUpdateItem setTarget:self];
+ [autoUpdateItem setAction:@selector(displayAutoUpdatePreferences:)];
+
+ // Network preferences
+ networkItem = [[NSToolbarItem alloc] initWithItemIdentifier:PREFERENCE_TOOLBAR_NETWORK];
+
+ [networkItem setLabel:NSLocalizedString(@"Network", @"")];
+ [networkItem setImage:[NSImage imageNamed:@"toolbar-preferences-network"]];
+ [networkItem setTarget:self];
+ [networkItem setAction:@selector(displayNetworkPreferences:)];
+
+ [toolbar setDelegate:self];
+ [toolbar setSelectedItemIdentifier:PREFERENCE_TOOLBAR_GENERAL];
+ [toolbar setAllowsUserCustomization:NO];
+
+ [preferencesWindow setToolbar:toolbar];
+ [preferencesWindow setShowsToolbarButton:NO];
+
+ [self displayGeneralPreferences:nil];
+}
+
+// -------------------------------------------------------------------------------
+// _resizeWindowForContentView:
+//
+// Resizes the window to the size of the supplied view.
+// -------------------------------------------------------------------------------
+- (void)_resizeWindowForContentView:(NSView *)view
+{
+ // remove all current views
+ NSEnumerator *en = [[[preferencesWindow contentView] subviews] objectEnumerator];
+ NSView *subview;
+
+ while (subview = [en nextObject])
+ {
+ [subview removeFromSuperview];
+ }
+
+ // resize window
+ [preferencesWindow resizeForContentView:view titleBarVisible:YES];
+
+ // add view
+ [[preferencesWindow contentView] addSubview:view];
+ [view setFrameOrigin:NSMakePoint(0, 0)];
+}
+
+@end
diff --git a/Source/SPQueryConsole.h b/Source/SPQueryConsole.h
index e1072904..4a2326c6 100644
--- a/Source/SPQueryConsole.h
+++ b/Source/SPQueryConsole.h
@@ -24,15 +24,33 @@
@interface SPQueryConsole : NSWindowController
{
- IBOutlet NSTextView *consoleTextView;
+ IBOutlet NSView *saveLogView;
+ IBOutlet NSTableView *consoleTableView;
+ IBOutlet NSSearchField *consoleSearchField;
+ IBOutlet NSProgressIndicator *progressIndicator;
+ IBOutlet NSButton *includeTimeStampsButton, *saveConsoleButton, *clearConsoleButton;
+
+ NSFont *consoleFont;
+ NSMutableArray *messagesFullSet, *messagesFilteredSet, *messagesVisibleSet;
+ BOOL showSelectStatementsAreDisabled;
+ BOOL filterIsActive;
+ NSMutableString *activeFilterString;
+
+ float uncollapsedDateColumnWidth;
}
++ (SPQueryConsole *)sharedQueryConsole;
+
+- (IBAction)copy:(id)sender;
- (IBAction)clearConsole:(id)sender;
- (IBAction)saveConsoleAs:(id)sender;
+- (IBAction)toggleShowTimeStamps:(id)sender;
+- (IBAction)toggleShowSelectShowStatements:(id)sender;
- (void)showMessageInConsole:(NSString *)message;
- (void)showErrorInConsole:(NSString *)error;
-- (NSTextView *)consoleTextView;
-
+- (int)consoleMessageCount;
+- (NSFont *)consoleFont;
+- (void)setConsoleFont:(NSFont *)theFont;
@end
diff --git a/Source/SPQueryConsole.m b/Source/SPQueryConsole.m
index 1fe62d65..ac96e51f 100644
--- a/Source/SPQueryConsole.m
+++ b/Source/SPQueryConsole.m
@@ -21,127 +21,501 @@
// More info at <http://code.google.com/p/sequel-pro/>
#import "SPQueryConsole.h"
+#import "SPConsoleMessage.h"
+
+#define MESSAGE_TRUNCATE_CHARACTER_LENGTH 256
+#define MESSAGE_TIME_STAMP_FORMAT @"%H:%M:%S"
#define DEFAULT_CONSOLE_LOG_FILENAME @"untitled"
#define DEFAULT_CONSOLE_LOG_FILE_EXTENSION @"log"
#define CONSOLE_WINDOW_AUTO_SAVE_NAME @"QueryConsole"
+// Table view column identifiers
+#define TABLEVIEW_MESSAGE_COLUMN_IDENTIFIER @"message"
+#define TABLEVIEW_DATE_COLUMN_IDENTIFIER @"messageDate"
+
@interface SPQueryConsole (PrivateAPI)
-- (void)_appendMessageToConsole:(NSString *)message withColor:(NSColor *)color;
+- (NSString *)_getConsoleStringWithTimeStamps:(BOOL)timeStamps;
+
+- (void)_updateFilterState;
+- (void)_addMessageToConsole:(NSString *)message isError:(BOOL)error;
+- (BOOL)_messageMatchesCurrentFilters:(NSString *)message;
@end
+static SPQueryConsole *sharedQueryConsole = nil;
+
@implementation SPQueryConsole
-// -------------------------------------------------------------------------------
-// awakeFromNib
-//
-// Set the window's auto save name.
-// -------------------------------------------------------------------------------
+/*
+ * Returns the shared query console.
+ */
++ (SPQueryConsole *)sharedQueryConsole
+{
+ @synchronized(self) {
+ if (sharedQueryConsole == nil) {
+ [[self alloc] init];
+ }
+ }
+
+ return sharedQueryConsole;
+}
+
++ (id)allocWithZone:(NSZone *)zone
+{
+ @synchronized(self) {
+ if (sharedQueryConsole == nil) {
+ sharedQueryConsole = [super allocWithZone:zone];
+
+ return sharedQueryConsole;
+ }
+ }
+
+ return nil; // On subsequent allocation attempts return nil
+}
+
+- (id)init
+{
+ if ((self = [super initWithWindowNibName:@"Console"])) {
+ messagesFullSet = [[NSMutableArray alloc] init];
+ messagesFilteredSet = [[NSMutableArray alloc] init];
+ consoleFont = [[NSFont systemFontOfSize:[NSFont smallSystemFontSize]] retain];
+
+ showSelectStatementsAreDisabled = NO;
+ filterIsActive = NO;
+ activeFilterString = [[NSMutableString alloc] init];
+
+ // Weak reference to active messages set - starts off as full set
+ messagesVisibleSet = messagesFullSet;
+
+ uncollapsedDateColumnWidth = [[consoleTableView tableColumnWithIdentifier:TABLEVIEW_DATE_COLUMN_IDENTIFIER] width];
+ }
+
+ 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 { }
+
+/**
+ * Set the window's auto save name.
+ */
- (void)awakeFromNib
{
[self setWindowFrameAutosaveName:CONSOLE_WINDOW_AUTO_SAVE_NAME];
}
-// -------------------------------------------------------------------------------
-// clearConsole:
-//
-// Clears the console by setting its displayed text to an empty string.
-// -------------------------------------------------------------------------------
+/**
+ * Copy implementation for console table view.
+ */
+- (void)copy:(id)sender
+{
+ NSResponder *firstResponder = [[self window] firstResponder];
+
+ if ((firstResponder == consoleTableView) && ([consoleTableView numberOfSelectedRows] > 0)) {
+
+ NSString *string = @"";
+ NSIndexSet *rows = [consoleTableView selectedRowIndexes];
+
+ int i = [rows firstIndex];
+
+ while (i != NSNotFound)
+ {
+ if (i < [messagesVisibleSet count]) {
+ SPConsoleMessage *message = [messagesVisibleSet objectAtIndex:i];
+
+ NSString *consoleMessage = [message message];
+
+ // If the timestamp column is not hidden we need to include them in the copy
+ if ([[consoleTableView tableColumnWithIdentifier:TABLEVIEW_DATE_COLUMN_IDENTIFIER] width] > 0) {
+
+ NSString *dateString = [[message messageDate] descriptionWithCalendarFormat:MESSAGE_TIME_STAMP_FORMAT timeZone:nil locale:nil];
+
+ consoleMessage = [NSString stringWithFormat:@"/* MySQL %@ */ %@", dateString, consoleMessage];
+ }
+
+ string = [string stringByAppendingFormat:@"%@\n", consoleMessage];
+ }
+
+ i = [rows indexGreaterThanIndex:i];
+ }
+
+ NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
+
+ // Copy the string to the pasteboard
+ [pasteBoard declareTypes:[NSArray arrayWithObjects:NSStringPboardType, nil] owner:nil];
+ [pasteBoard setString:string forType:NSStringPboardType];
+ }
+}
+
+/**
+ * Clears the console by removing all of its messages.
+ */
- (IBAction)clearConsole:(id)sender
{
- [consoleTextView setString:@""];
+ [messagesFullSet removeAllObjects];
+ [messagesFilteredSet removeAllObjects];
+
+ [consoleTableView reloadData];
}
-// -------------------------------------------------------------------------------
-// saveConsoleAs:
-//
-// Presents the user with a save panel to the save the current console to a log file.
-// -------------------------------------------------------------------------------
+/**
+ * Presents the user with a save panel to the save the current console to a log file.
+ */
- (IBAction)saveConsoleAs:(id)sender
{
NSSavePanel *panel = [NSSavePanel savePanel];
-
+
[panel setRequiredFileType:DEFAULT_CONSOLE_LOG_FILE_EXTENSION];
-
+
[panel setExtensionHidden:NO];
[panel setAllowsOtherFileTypes:YES];
[panel setCanSelectHiddenExtension:YES];
-
+
+ [panel setAccessoryView:saveLogView];
+
[panel beginSheetForDirectory:nil file:DEFAULT_CONSOLE_LOG_FILENAME modalForWindow:[self window] modalDelegate:self didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:) contextInfo:NULL];
+ [panel beginSheetForDirectory:nil
+ file:DEFAULT_CONSOLE_LOG_FILENAME
+ modalForWindow:[self window]
+ modalDelegate:self
+ didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:)
+ contextInfo:NULL];
}
-// -------------------------------------------------------------------------------
-// showMessageInConsole:
-//
-// Shows the supplied message in the console.
-// -------------------------------------------------------------------------------
+/**
+ * Toggles the display of the message time stamp column in the table view.
+ */
+- (IBAction)toggleShowTimeStamps:(id)sender
+{
+ if ([sender intValue]) {
+ [[consoleTableView tableColumnWithIdentifier:TABLEVIEW_DATE_COLUMN_IDENTIFIER] setMinWidth:50.0];
+ [[consoleTableView tableColumnWithIdentifier:TABLEVIEW_DATE_COLUMN_IDENTIFIER] setWidth:uncollapsedDateColumnWidth];
+ } else {
+ uncollapsedDateColumnWidth = [[consoleTableView tableColumnWithIdentifier:TABLEVIEW_DATE_COLUMN_IDENTIFIER] width];
+ [[consoleTableView tableColumnWithIdentifier:TABLEVIEW_DATE_COLUMN_IDENTIFIER] setMinWidth:0.0];
+ [[consoleTableView tableColumnWithIdentifier:TABLEVIEW_DATE_COLUMN_IDENTIFIER] setWidth: 0.0];
+ }
+}
+
+/**
+ * Toggles the hiding of messages containing SELECT and SHOW statements
+ */
+- (IBAction)toggleShowSelectShowStatements:(id)sender
+{
+ // Store the state of the toggle for later quick reference
+ showSelectStatementsAreDisabled = ![sender intValue];
+
+ [self _updateFilterState];
+}
+
+/**
+ * Shows the supplied message in the console.
+ */
- (void)showMessageInConsole:(NSString *)message
{
- [self _appendMessageToConsole:message withColor:[NSColor blackColor]];
+ [self _addMessageToConsole:message isError:NO];
}
-// -------------------------------------------------------------------------------
-// showErrorInConsole:
-//
-// Shows the supplied error in the console.
-// -------------------------------------------------------------------------------
+/**
+ * Shows the supplied error in the console.
+ */
- (void)showErrorInConsole:(NSString *)error
{
- [self _appendMessageToConsole:error withColor:[NSColor redColor]];
+ [self _addMessageToConsole:error isError:YES];
}
-// -------------------------------------------------------------------------------
-// consoleTextView
-//
-// Return a reference to the console's text view.
-// -------------------------------------------------------------------------------
-- (NSTextView *)consoleTextView
+/**
+ * Returns the number of messages currently in the console.
+ */
+- (int)consoleMessageCount
{
- return consoleTextView;
+ return [messagesFullSet count];
}
-// -------------------------------------------------------------------------------
-// savePanelDidEnd:returnCode:contextInfo:
-//
-// Called when the NSSavePanel sheet ends.
-// -------------------------------------------------------------------------------
+/**
+ * Called when the NSSavePanel sheet ends. Writes the console's current content to the selected file if required.
+ */
- (void)savePanelDidEnd:(NSSavePanel *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo
{
if (returnCode == NSOKButton) {
- [[[consoleTextView textStorage] string] writeToFile:[sheet filename] atomically:YES encoding:NSUTF8StringEncoding error:NULL];
+ [[self _getConsoleStringWithTimeStamps:[includeTimeStampsButton intValue]] writeToFile:[sheet filename] atomically:YES encoding:NSUTF8StringEncoding error:NULL];
+ }
+}
+
+#pragma mark -
+#pragma mark Tableview delegate methods
+
+/**
+ * Table view delegate method. Returns the number of rows in the table veiw.
+ */
+- (int)numberOfRowsInTableView:(NSTableView *)tableView
+{
+ return [messagesVisibleSet count];
+}
+
+/**
+ * Table view delegate method. Returns the specific object for the request column and row.
+ */
+- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row
+{
+ NSString *returnValue = nil;
+
+ id object = [[messagesVisibleSet objectAtIndex:row] valueForKey:[tableColumn identifier]];
+
+ if ([[tableColumn identifier] isEqualToString:TABLEVIEW_DATE_COLUMN_IDENTIFIER]) {
+
+ NSString *dateString = [(NSDate *)object descriptionWithCalendarFormat:MESSAGE_TIME_STAMP_FORMAT timeZone:nil locale:nil];
+
+ returnValue = [NSString stringWithFormat:@"/* MySQL %@ */", dateString];
+ }
+ else {
+ if ([(NSString *)object length] > MESSAGE_TRUNCATE_CHARACTER_LENGTH) {
+ object = [NSString stringWithFormat:@"%@...", [object substringToIndex:MESSAGE_TRUNCATE_CHARACTER_LENGTH]];
+ }
+
+ returnValue = object;
+ }
+
+ NSMutableDictionary *stringAtributes = nil;
+
+ if (consoleFont) {
+ stringAtributes = [NSMutableDictionary dictionaryWithObject:consoleFont forKey:NSFontAttributeName];
+ }
+
+ // If this is an error message give it a red colour
+ if ([(SPConsoleMessage *)[messagesVisibleSet objectAtIndex:row] isError]) {
+ if (stringAtributes) {
+ [stringAtributes setObject:[NSColor redColor] forKey:NSForegroundColorAttributeName];
+ }
+ else {
+ stringAtributes = [NSMutableDictionary dictionaryWithObject:[NSColor redColor] forKey:NSForegroundColorAttributeName];
+ }
}
+
+ return [[[NSAttributedString alloc] initWithString:returnValue attributes:stringAtributes] autorelease];
+}
+
+#pragma mark -
+#pragma mark Other
+
+/**
+ * Called whenver the test within the search field changes.
+ */
+- (void)controlTextDidChange:(NSNotification *)notification
+{
+ id object = [notification object];
+
+ if ([object isEqualTo:consoleSearchField]) {
+
+ // Store the state of the text filter and the current filter string for later quick reference
+ [activeFilterString setString:[[object stringValue] lowercaseString]];
+ filterIsActive = [activeFilterString length]?YES:NO;
+
+ [self _updateFilterState];
+ }
+}
+
+/**
+ * Menu item validation for console table view contextual menu.
+ */
+- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
+{
+ if ([menuItem action] == @selector(copy:)) {
+ return ([consoleTableView numberOfSelectedRows] > 0);
+ }
+
+ if ([menuItem action] == @selector(clearConsole:)) {
+ return ([self consoleMessageCount] > 0);
+ }
+
+ return [[self window] validateMenuItem:menuItem];
+}
+
+- (NSFont *)consoleFont
+{
+ return consoleFont;
+}
+
+- (void)setConsoleFont:(NSFont *)theFont
+{
+ if (consoleFont) [consoleFont release];
+ consoleFont = [theFont copy];
+}
+
+/**
+ * Standard dealloc.
+ */
+- (void)dealloc
+{
+ messagesVisibleSet = nil;
+
+ [messagesFullSet release], messagesFullSet = nil;
+ [messagesFilteredSet release], messagesFilteredSet = nil;
+ [activeFilterString release], activeFilterString = nil;
+ [consoleFont release], consoleFont = nil;
+
+ [super dealloc];
}
@end
@implementation SPQueryConsole (PrivateAPI)
-// -------------------------------------------------------------------------------
-// _appendMessageToConsole:withColor:
-//
-// Appeds the supplied string to the query console, coloring the text using the
-// supplied color.
-// -------------------------------------------------------------------------------
-- (void)_appendMessageToConsole:(NSString *)message withColor:(NSColor *)color
-{
- int begin, end;
-
- // Set the selected range of the text view to be the very last character
- [consoleTextView setSelectedRange:NSMakeRange([[consoleTextView string] length], 0)];
- begin = [[consoleTextView string] length];
-
- // Apped the message to the current text storage using the text view's current typing attributes
- [[consoleTextView textStorage] appendAttributedString:[[NSAttributedString alloc] initWithString:message attributes:[consoleTextView typingAttributes]]];
- end = [[consoleTextView string] length];
-
- // Color the text we just added
- [consoleTextView setTextColor:color range:NSMakeRange(begin, (end - begin))];
-
- // Scroll to the text we just added
- [consoleTextView scrollRangeToVisible:[consoleTextView selectedRange]];
+/**
+ * Creates and returns a string made entirely of all of the console's messages and includes the message
+ * time stamps if specified.
+ */
+- (NSString *)_getConsoleStringWithTimeStamps:(BOOL)timeStamps
+{
+ NSMutableString *consoleString = [[[NSMutableString alloc] init] autorelease];
+ int i;
+
+ for (i = 0; i < [messagesVisibleSet count]; i++) {
+ SPConsoleMessage *message = [messagesVisibleSet objectAtIndex:i];
+ if (timeStamps) {
+ NSString *dateString = [[message messageDate] descriptionWithCalendarFormat:MESSAGE_TIME_STAMP_FORMAT timeZone:nil locale:nil];
+
+ [consoleString appendString:[NSString stringWithFormat:@"/* MySQL %@ */ ", dateString]];
+ }
+
+ [consoleString appendString:[NSString stringWithFormat:@"%@\n", [message message]]];
+ }
+
+ return consoleString;
+}
+
+
+/**
+ * Updates the filtered result set based on any filter string and whether or not
+ * all SELECT nd SHOW statements should be shown within the console.
+ */
+- (void)_updateFilterState
+{
+ int i;
+
+ // Display start progress spinner
+ [progressIndicator setHidden:NO];
+ [progressIndicator startAnimation:self];
+
+ // Don't allow clearing the console while filtering its content
+ [saveConsoleButton setEnabled:NO];
+ [clearConsoleButton setEnabled:NO];
+
+ [messagesFilteredSet removeAllObjects];
+
+ // If filtering is disabled and all show/selects are shown, empty the filtered
+ // result set and set the full set to visible.
+ if (!filterIsActive && !showSelectStatementsAreDisabled) {
+ messagesVisibleSet = messagesFullSet;
+
+ [consoleTableView reloadData];
+ [consoleTableView scrollRowToVisible:([messagesVisibleSet count] - 1)];
+
+ [saveConsoleButton setEnabled:YES];
+ [clearConsoleButton setEnabled:YES];
+
+ [saveConsoleButton setTitle:@"Save As..."];
+
+ // Hide progress spinner
+ [progressIndicator setHidden:YES];
+ [progressIndicator stopAnimation:self];
+ return;
+ }
+
+ // Cache frequently used selector, avoiding dynamic binding overhead
+ IMP messageMatchesFilters = [self methodForSelector:@selector(_messageMatchesCurrentFilters:)];
+
+ // Loop through all the messages in the full set to determine which should be
+ // added to the filtered set.
+ for (i = 0; i < [messagesFullSet count]; i++) {
+ SPConsoleMessage *message = [messagesFullSet objectAtIndex:i];
+
+ // Add a reference to the message to the filtered set if filters are active and the
+ // current message matches them
+ if ((messageMatchesFilters)(self, @selector(_messageMatchesCurrentFilters:), [message message])) {
+ [messagesFilteredSet addObject:message];
+ }
+ }
+
+ // Ensure that the filtered set is marked as the currently visible set.
+ messagesVisibleSet = messagesFilteredSet;
+
+ [consoleTableView reloadData];
+ [consoleTableView scrollRowToVisible:([messagesVisibleSet count] - 1)];
+
+ if ([messagesVisibleSet count] > 0) {
+ [saveConsoleButton setEnabled:YES];
+ [clearConsoleButton setEnabled:YES];
+ }
+
+ [saveConsoleButton setTitle:@"Save View As..."];
+
+ // Hide progress spinner
+ [progressIndicator setHidden:YES];
+ [progressIndicator stopAnimation:self];
+}
+
+/**
+ * Adds the supplied message to the query console.
+ */
+- (void)_addMessageToConsole:(NSString *)message isError:(BOOL)error
+{
+ SPConsoleMessage *consoleMessage = [SPConsoleMessage consoleMessageWithMessage:[[message stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] stringByAppendingString:@";"] date:[NSDate date]];
+
+ [consoleMessage setIsError:error];
+
+ [messagesFullSet addObject:consoleMessage];
+
+ // If filtering is active, determine whether to add a reference to the filtered set
+ if ((showSelectStatementsAreDisabled || filterIsActive)
+ && [self _messageMatchesCurrentFilters:[consoleMessage message]])
+ {
+ [messagesFilteredSet addObject:[messagesFullSet lastObject]];
+ }
+
+ // Reload the table and scroll to the new message
+ [consoleTableView reloadData];
+ [consoleTableView scrollRowToVisible:([messagesVisibleSet count] - 1)];
+}
+
+/**
+ * Checks whether the supplied message text matches the current filter text, if any,
+ * and whether it should be hidden if the SELECT/SHOW toggle is off.
+ */
+- (BOOL)_messageMatchesCurrentFilters:(NSString *)message
+{
+ BOOL messageMatchesCurrentFilters = YES;
+
+ // Check whether to hide the message based on the current filter text, if any
+ if (filterIsActive
+ && [message rangeOfString:activeFilterString options:NSCaseInsensitiveSearch].location == NSNotFound)
+ {
+ messageMatchesCurrentFilters = NO;
+ }
+
+ // If hiding SELECTs and SHOWs is toggled to on, check whether the message is a SELECT or SHOW
+ if (messageMatchesCurrentFilters
+ && showSelectStatementsAreDisabled
+ && ([message hasPrefix:@"SELECT"] || [message hasPrefix:@"SHOW"]))
+ {
+ messageMatchesCurrentFilters = NO;
+ }
+
+ return messageMatchesCurrentFilters;
}
@end
diff --git a/Source/SPSQLParser.h b/Source/SPSQLParser.h
index 14ac6f9d..be068c16 100644
--- a/Source/SPSQLParser.h
+++ b/Source/SPSQLParser.h
@@ -23,6 +23,12 @@
#import <Cocoa/Cocoa.h>
+/*
+ * Define the length of the character cache to use when parsing instead of accessing
+ * via characterAtIndex:. There is a balance here between updating the cache very
+ * often and access penalties; 1500 appears a reasonable compromise.
+ */
+#define CHARACTER_CACHE_LENGTH 1500
/*
* This class provides a string class intended for SQL parsing. It extends NSMutableString,
@@ -53,6 +59,9 @@
@interface SPSQLParser : NSMutableString
{
id string;
+ unichar *stringCharCache;
+ long charCacheStart;
+ long charCacheEnd;
}
@@ -210,6 +219,15 @@ typedef enum _SPCommentTypes {
- (long) endIndexOfStringQuotedByCharacter:(unichar)quoteCharacter startingAtIndex:(long)index;
- (long) endIndexOfCommentOfType:(SPCommentType)commentType startingAtIndex:(long)index;
+/*
+ * Cacheing methods to enable a faster alternative to characterAtIndex: when walking strings, and overrides to update.
+ */
+- (unichar) charAtIndex:(long)index;
+- (void) clearCharCache;
+- (void) deleteCharactersInRange:(NSRange)aRange;
+- (void) insertString:(NSString *)aString atIndex:(int)anIndex;
+
+
/* Required and primitive methods to allow subclassing class cluster */
#pragma mark -
diff --git a/Source/SPSQLParser.m b/Source/SPSQLParser.m
index 9828f529..ad749fc7 100644
--- a/Source/SPSQLParser.m
+++ b/Source/SPSQLParser.m
@@ -60,11 +60,11 @@
case '-':
if (stringLength < currentStringIndex + 2) break;
if ([string characterAtIndex:currentStringIndex+1] != '-') break;
- if (![[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:[string characterAtIndex:currentStringIndex+2]]) break;
+ if (![[NSCharacterSet whitespaceCharacterSet] characterIsMember:[string characterAtIndex:currentStringIndex+2]]) break;
commentEndIndex = [self endIndexOfCommentOfType:SPDoubleDashComment startingAtIndex:currentStringIndex];
// Remove the comment
- [string deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)];
+ [self deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)];
stringLength -= commentEndIndex - currentStringIndex + 1;
currentStringIndex--;
break;
@@ -73,7 +73,7 @@
commentEndIndex = [self endIndexOfCommentOfType:SPHashComment startingAtIndex:currentStringIndex];
// Remove the comment
- [string deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)];
+ [self deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)];
stringLength -= commentEndIndex - currentStringIndex + 1;
currentStringIndex--;
break;
@@ -85,7 +85,7 @@
commentEndIndex = [self endIndexOfCommentOfType:SPCStyleComment startingAtIndex:currentStringIndex];
// Remove the comment
- [string deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)];
+ [self deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)];
stringLength -= commentEndIndex - currentStringIndex + 1;
currentStringIndex--;
break;
@@ -158,7 +158,7 @@
if (stringIndex == NSNotFound) return NO;
// If it has been found, trim the string appropriately and return YES
- [string deleteCharactersInRange:NSMakeRange(0, stringIndex + (inclusive?1:0))];
+ [self deleteCharactersInRange:NSMakeRange(0, stringIndex + (inclusive?1:0))];
return YES;
}
@@ -213,7 +213,7 @@
// Select the appropriate string range, truncate the current string, and return the selected string
resultString = [NSString stringWithString:[string substringWithRange:NSMakeRange(0, stringIndex + (inclusiveReturn?1:0))]];
- [string deleteCharactersInRange:NSMakeRange(0, stringIndex + (inclusiveTrim?1:0))];
+ [self deleteCharactersInRange:NSMakeRange(0, stringIndex + (inclusiveTrim?1:0))];
return resultString;
}
@@ -255,7 +255,7 @@
- (NSString *) stringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter inclusively:(BOOL)inclusive skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings
{
long fromCharacterIndex, toCharacterIndex;
-
+
// Look for the first occurrence of the from: character
fromCharacterIndex = [self firstOccurrenceOfCharacter:fromCharacter afterIndex:-1 skippingBrackets:skipBrackets ignoringQuotedStrings:ignoreQuotedStrings];
if (fromCharacterIndex == NSNotFound) return nil;
@@ -318,7 +318,7 @@
// Select the correct part of the string, truncate the current string, and return the selected string.
resultString = [string substringWithRange:NSMakeRange(fromCharacterIndex + (inclusiveReturn?0:1), toCharacterIndex + (inclusiveReturn?1:-1) - fromCharacterIndex)];
- [string deleteCharactersInRange:NSMakeRange(fromCharacterIndex + (inclusiveTrim?0:1), toCharacterIndex + (inclusiveTrim?1:-1) - fromCharacterIndex)];
+ [self deleteCharactersInRange:NSMakeRange(fromCharacterIndex + (inclusiveTrim?0:1), toCharacterIndex + (inclusiveTrim?1:-1) - fromCharacterIndex)];
return resultString;
}
@@ -358,16 +358,14 @@
NSMutableArray *resultsArray = [NSMutableArray array];
long stringIndex = -1, nextIndex = 0;
- // Walk through the string finding the character to split by, and add non-zero length strings.
+ // Walk through the string finding the character to split by, and add all strings to the array.
while (1) {
nextIndex = [self firstOccurrenceOfCharacter:character afterIndex:stringIndex skippingBrackets:skipBrackets ignoringQuotedStrings:ignoreQuotedStrings];
if (nextIndex == NSNotFound) {
break;
}
- if (nextIndex - stringIndex - 1 > 0) {
- [resultsArray addObject:[string substringWithRange:NSMakeRange(stringIndex+1, nextIndex - stringIndex - 1)]];
- }
+ [resultsArray addObject:[string substringWithRange:NSMakeRange(stringIndex+1, nextIndex - stringIndex - 1)]];
stringIndex = nextIndex;
}
@@ -408,12 +406,16 @@
long stringLength = [string length];
int bracketingLevel = 0;
+ // Cache frequently used selectors, avoiding dynamic binding overhead
+ IMP charAtIndex = [self methodForSelector:@selector(charAtIndex:)];
+ IMP endIndex = [self methodForSelector:@selector(endIndexOfStringQuotedByCharacter:startingAtIndex:)];
+
// Sanity check inputs
if (startIndex < -1) startIndex = -1;
// Walk along the string, processing characters
for (currentStringIndex = startIndex + 1; currentStringIndex < stringLength; currentStringIndex++) {
- currentCharacter = [string characterAtIndex:currentStringIndex];
+ currentCharacter = (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex);
// Check for the ending character, and if it has been found and quoting/brackets is valid, return.
if (currentCharacter == character) {
@@ -430,7 +432,7 @@
case '"':
case '`':
if (!ignoreQuotedStrings) break;
- quotedStringEndIndex = [self endIndexOfStringQuotedByCharacter:currentCharacter startingAtIndex:currentStringIndex+1];
+ quotedStringEndIndex = (long)(*endIndex)(self, @selector(endIndexOfStringQuotedByCharacter:startingAtIndex:), currentCharacter, currentStringIndex+1);
if (quotedStringEndIndex == NSNotFound) {
return NSNotFound;
}
@@ -449,8 +451,8 @@
// For comments starting "--[\s]", ensure the start syntax is valid before proceeding.
case '-':
if (stringLength < currentStringIndex + 2) break;
- if ([string characterAtIndex:currentStringIndex+1] != '-') break;
- if (![[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:[string characterAtIndex:currentStringIndex+2]]) break;
+ if ((unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex+1) != '-') break;
+ if (![[NSCharacterSet whitespaceCharacterSet] characterIsMember:(unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex+2)]) break;
currentStringIndex = [self endIndexOfCommentOfType:SPDoubleDashComment startingAtIndex:currentStringIndex];
break;
@@ -461,7 +463,7 @@
// For comments starting "/*", ensure the start syntax is valid before proceeding.
case '/':
if (stringLength < currentStringIndex + 1) break;
- if ([string characterAtIndex:currentStringIndex+1] != '*') break;
+ if ((unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex+1) != '*') break;
currentStringIndex = [self endIndexOfCommentOfType:SPCStyleComment startingAtIndex:currentStringIndex];
break;
}
@@ -480,17 +482,20 @@
BOOL characterIsEscaped;
unichar currentCharacter;
+ // Cache the charAtIndex selector, avoiding dynamic binding overhead
+ IMP charAtIndex = [self methodForSelector:@selector(charAtIndex:)];
+
stringLength = [string length];
// Walk the string looking for the string end
for ( currentStringIndex = index; currentStringIndex < stringLength; currentStringIndex++) {
- currentCharacter = [string characterAtIndex:currentStringIndex];
+ currentCharacter = (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex);
// If the string end is a backtick and one has been encountered, treat it as end of string
if (quoteCharacter == '`' && currentCharacter == '`') {
// ...as long as the next character isn't also a backtick, in which case it's being quoted. Skip both.
- if ((currentStringIndex + 1) < stringLength && [string characterAtIndex:currentStringIndex+1] == '`') {
+ if ((currentStringIndex + 1) < stringLength && (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex+1) == '`') {
currentStringIndex++;
continue;
}
@@ -504,7 +509,7 @@
characterIsEscaped = NO;
i = 1;
quotedStringLength = currentStringIndex - 1;
- while ((quotedStringLength - i) > 0 && [string characterAtIndex:currentStringIndex - i] == '\\') {
+ while ((quotedStringLength - i) > 0 && (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex - i) == '\\') {
characterIsEscaped = !characterIsEscaped;
i++;
}
@@ -512,7 +517,7 @@
// If an even number have been found, it may be the end of the string - as long as the subsequent character
// isn't also the same character, in which case it's another form of escaping.
if (!characterIsEscaped) {
- if ((currentStringIndex + 1) < stringLength && [string characterAtIndex:currentStringIndex+1] == quoteCharacter) {
+ if ((currentStringIndex + 1) < stringLength && (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex+1) == quoteCharacter) {
currentStringIndex++;
continue;
}
@@ -534,6 +539,9 @@
long stringLength = [string length];
unichar currentCharacter;
+ // Cache the charAtIndex selector, avoiding dynamic binding overhead
+ IMP charAtIndex = [self methodForSelector:@selector(charAtIndex:)];
+
switch (commentType) {
// For comments of type "--[\s]", start the comment processing two characters in to match the start syntax,
@@ -545,7 +553,7 @@
case SPHashComment:
index++;
for ( ; index < stringLength; index++ ) {
- currentCharacter = [string characterAtIndex:index];
+ currentCharacter = (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), index);
if (currentCharacter == '\r' || currentCharacter == '\n') {
return index-1;
}
@@ -557,8 +565,8 @@
case SPCStyleComment:
index = index+2;
for ( ; index < stringLength; index++ ) {
- if ([string characterAtIndex:index] == '*') {
- if ((stringLength > index + 1) && [string characterAtIndex:index+1] == '/') {
+ if ((unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), index) == '*') {
+ if ((stringLength > index + 1) && (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), index+1) == '/') {
return (index+1);
}
}
@@ -569,6 +577,52 @@
return (stringLength-1);
}
+/*
+ * Provide a method to retrieve a character from the local cache.
+ * Does no bounds checking on the underlying string, and so is kept
+ * separate for characterAtIndex:.
+ */
+- (unichar) charAtIndex:(long)index
+{
+
+ // If the current cache doesn't include the current character, update it.
+ if (index > charCacheEnd || index < charCacheStart) {
+ if (charCacheEnd > -1) {
+ free(stringCharCache);
+ }
+ unsigned int remainingStringLength = [string length] - index;
+ unsigned int newcachelength = (CHARACTER_CACHE_LENGTH < remainingStringLength)?CHARACTER_CACHE_LENGTH:remainingStringLength;
+ stringCharCache = (unichar *)calloc(newcachelength, sizeof(unichar));
+ [string getCharacters:stringCharCache range:NSMakeRange(index, newcachelength)];
+ charCacheEnd = index + newcachelength - 1;
+ charCacheStart = index;
+ }
+ return stringCharCache[index - charCacheStart];
+}
+
+/*
+ * Provide a method to clear the cache, and use it when updating the string.
+ */
+- (void) clearCharCache
+{
+ if (charCacheEnd > -1) {
+ free(stringCharCache);
+ }
+ charCacheEnd = -1;
+ charCacheStart = 0;
+}
+- (void) deleteCharactersInRange:(NSRange)aRange
+{
+ [super deleteCharactersInRange:aRange];
+ [self clearCharCache];
+}
+- (void) insertString:(NSString *)aString atIndex:(int)anIndex
+{
+ [super insertString:aString atIndex:anIndex];
+ [self clearCharCache];
+}
+
+
/* Required and primitive methods to allow subclassing class cluster */
#pragma mark -
@@ -576,45 +630,53 @@
if (self = [super init]) {
string = [[NSMutableString string] retain];
}
+ charCacheEnd = -1;
return self;
}
- (id) initWithBytes:(const void *)bytes length:(unsigned int)length encoding:(NSStringEncoding)encoding {
if (self = [super init]) {
string = [[NSMutableString alloc] initWithBytes:bytes length:length encoding:encoding];
}
+ charCacheEnd = -1;
return self;
}
- (id) initWithBytesNoCopy:(void *)bytes length:(unsigned int)length encoding:(NSStringEncoding)encoding freeWhenDone:(BOOL)flag {
if (self = [super init]) {
string = [[NSMutableString alloc] initWithBytesNoCopy:bytes length:length encoding:encoding freeWhenDone:flag];
}
+ charCacheEnd = -1;
return self;
}
- (id) initWithCapacity:(unsigned int)capacity {
if (self = [super init]) {
string = [[NSMutableString stringWithCapacity:capacity] retain];
}
+ charCacheEnd = -1;
return self;
}
- (id) initWithCharactersNoCopy:(unichar *)characters length:(unsigned int)length freeWhenDone:(BOOL)flag {
if (self = [super init]) {
string = [[NSMutableString alloc] initWithCharactersNoCopy:characters length:length freeWhenDone:flag];
}
+ charCacheEnd = -1;
return self;
}
- (id) initWithContentsOfFile:(id)path {
+ charCacheEnd = -1;
return [self initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL];
}
- (id) initWithContentsOfFile:(NSString *)path encoding:(NSStringEncoding)encoding error:(NSError **)error {
if (self = [super init]) {
string = [[NSMutableString alloc] initWithContentsOfFile:path encoding:encoding error:error];
}
+ charCacheEnd = -1;
return self;
}
- (id) initWithCString:(const char *)nullTerminatedCString encoding:(NSStringEncoding)encoding {
if (self = [super init]) {
string = [[NSMutableString alloc] initWithCString:nullTerminatedCString encoding:encoding];
}
+ charCacheEnd = -1;
return self;
}
- (id) initWithFormat:(NSString *)format, ... {
@@ -622,12 +684,14 @@
va_start(argList, format);
id str = [self initWithFormat:format arguments:argList];
va_end(argList);
+ charCacheEnd = -1;
return str;
}
- (id) initWithFormat:(NSString *)format arguments:(va_list)argList {
if (self = [super init]) {
string = [[NSMutableString alloc] initWithFormat:format arguments:argList];
}
+ charCacheEnd = -1;
return self;
}
- (unsigned int) length {
@@ -641,15 +705,19 @@
}
- (unsigned int) replaceOccurrencesOfString:(NSString *)target withString:(NSString *)replacement options:(unsigned)options range:(NSRange)searchRange {
return [string replaceOccurrencesOfString:target withString:replacement options:options range:searchRange];
+ [self clearCharCache];
}
- (void) setString:(NSString *)aString {
[string setString:aString];
+ [self clearCharCache];
}
- (void) replaceCharactersInRange:(NSRange)range withString:(NSString *)aString {
[string replaceCharactersInRange:range withString:aString];
+ [self clearCharCache];
}
- (void) dealloc {
[string release];
+ if (charCacheEnd != -1) free(stringCharCache);
[super dealloc];
}
@end \ No newline at end of file
diff --git a/Source/SPStringAdditions.h b/Source/SPStringAdditions.h
index 4acd748c..1d706666 100644
--- a/Source/SPStringAdditions.h
+++ b/Source/SPStringAdditions.h
@@ -25,6 +25,10 @@
@interface NSString (SPStringAdditions)
+ (NSString *)stringForByteSize:(int)byteSize;
++ (NSString *)stringForTimeInterval:(float)timeInterval;
+
+- (NSString *)backtickQuotedString;
+- (NSArray *)lineRangesForRange:(NSRange)aRange;
#if MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_5
- (NSArray *)componentsSeparatedByCharactersInSet:(NSCharacterSet *)set;
diff --git a/Source/SPStringAdditions.m b/Source/SPStringAdditions.m
index 2916611d..e0bf65cf 100644
--- a/Source/SPStringAdditions.m
+++ b/Source/SPStringAdditions.m
@@ -24,11 +24,9 @@
@implementation NSString (SPStringAdditions)
-// -------------------------------------------------------------------------------
-// stringForByteSize:
-//
-// Returns a human readable version string of the supplied byte size.
-// -------------------------------------------------------------------------------
+/*
+ * Returns a human readable version string of the supplied byte size.
+ */
+ (NSString *)stringForByteSize:(int)byteSize
{
float size = byteSize;
@@ -66,37 +64,148 @@
return [numberFormatter stringFromNumber:[NSNumber numberWithFloat:size]];
}
+
+// -------------------------------------------------------------------------------
+// stringForTimeInterval:
+//
+// Returns a human readable version string of the supplied time interval.
+// -------------------------------------------------------------------------------
++ (NSString *)stringForTimeInterval:(float)timeInterval
+{
+ NSNumberFormatter *numberFormatter = [[[NSNumberFormatter alloc] init] autorelease];
+
+ [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
+
+ if (timeInterval < 1) {
+ timeInterval = (timeInterval * 1000);
+ [numberFormatter setFormat:@"#,##0 ms"];
+
+ return [numberFormatter stringFromNumber:[NSNumber numberWithFloat:timeInterval]];
+ }
+
+ if (timeInterval < 10) {
+ [numberFormatter setFormat:@"#,##0.00 s"];
+
+ return [numberFormatter stringFromNumber:[NSNumber numberWithFloat:timeInterval]];
+ }
+
+ if (timeInterval < 100) {
+ [numberFormatter setFormat:@"#,##0.0 s"];
+
+ return [numberFormatter stringFromNumber:[NSNumber numberWithFloat:timeInterval]];
+ }
+
+ if (timeInterval < 300) {
+ [numberFormatter setFormat:@"#,##0 s"];
+
+ return [numberFormatter stringFromNumber:[NSNumber numberWithFloat:timeInterval]];
+ }
+
+ if (timeInterval < 3600) {
+ timeInterval = (timeInterval / 60);
+ [numberFormatter setFormat:@"#,##0 min"];
+
+ return [numberFormatter stringFromNumber:[NSNumber numberWithFloat:timeInterval]];
+ }
+
+ timeInterval = (timeInterval / 3600);
+ [numberFormatter setFormat:@"#,##0 hours"];
+
+ return [numberFormatter stringFromNumber:[NSNumber numberWithFloat:timeInterval]];
+}
+
+
+// -------------------------------------------------------------------------------
+// backtickQuotedString
+//
+// Returns the string quoted with backticks as required for MySQL identifiers
+// eg.: tablename => `tablename`
+// my`table => `my``table`
+// -------------------------------------------------------------------------------
+- (NSString *)backtickQuotedString
+{
+ // mutableCopy automatically retains the returned string, so don't forget to release it later...
+ NSMutableString *workingCopy = [self mutableCopy];
+
+ // First double all backticks in the string to escape them
+ // I don't want to use "stringByReplacingOccurrencesOfString:withString:" because it's only available in 10.5
+ [workingCopy replaceOccurrencesOfString: @"`"
+ withString: @"``"
+ options: NSLiteralSearch
+ range: NSMakeRange(0, [workingCopy length]) ];
+
+ // Add the quotes around the string
+ NSString *quotedString = [NSString stringWithFormat: @"`%@`", workingCopy];
+
+ [workingCopy release];
+
+ return quotedString;
+}
+
+// -------------------------------------------------------------------------------
+// lineRangesForRange
+//
+// Returns an array of serialised NSRanges, each representing a line within the string
+// which is at least partially covered by the NSRange supplied.
+// Each line includes the line termination character(s) for the line. As per
+// lineRangeForRange, lines are split by CR, LF, CRLF, U+2028 (Unicode line separator),
+// or U+2029 (Unicode paragraph separator).
+// -------------------------------------------------------------------------------
+- (NSArray *)lineRangesForRange:(NSRange)aRange
+{
+ NSMutableArray *lineRangesArray = [NSMutableArray array];
+ NSRange currentLineRange;
+
+ // Check that the range supplied is valid - if not return an empty array.
+ if (aRange.location == NSNotFound || aRange.location + aRange.length > [self length])
+ return lineRangesArray;
+
+ // Get the range of the first string covered by the specified range, and add it to the array
+ currentLineRange = [self lineRangeForRange:NSMakeRange(aRange.location, 0)];
+ [lineRangesArray addObject:NSStringFromRange(currentLineRange)];
+
+ // Loop through until the line end matches or surpasses the end of the specified range
+ while (currentLineRange.location + currentLineRange.length < aRange.location + aRange.length) {
+ currentLineRange = [self lineRangeForRange:NSMakeRange(currentLineRange.location + currentLineRange.length, 0)];
+ [lineRangesArray addObject:NSStringFromRange(currentLineRange)];
+ }
+
+ // Return the constructed array of ranges
+ return lineRangesArray;
+}
+
+
#if MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_5
- // -------------------------------------------------------------------------------
- // componentsSeparatedByCharactersInSet:
- // Credit - Greg Hulands <ghulands@mac.com>
- // Needed for 10.4+ compatibility
- // -------------------------------------------------------------------------------
- - (NSArray *)componentsSeparatedByCharactersInSet:(NSCharacterSet *)set // 10.5 adds this to NSString, but we are 10.4+
- {
- NSMutableArray *result = [NSMutableArray array];
- NSScanner *scanner = [NSScanner scannerWithString:self];
- NSString *chunk = nil;
-
- [scanner setCharactersToBeSkipped:nil];
- BOOL sepFound = [scanner scanCharactersFromSet:set intoString:(NSString **)nil]; // skip any preceding separators
-
- if (sepFound) { // if initial separator, start with empty component
- [result addObject:@""];
- }
-
- while ([scanner scanUpToCharactersFromSet:set intoString:&chunk]) {
- [result addObject:chunk];
- sepFound = [scanner scanCharactersFromSet: set intoString: (NSString **) nil];
- }
-
- if (sepFound) { // if final separator, end with empty component
- [result addObject: @""];
- }
-
- result = [result copy];
- return [result autorelease];
+/*
+ * componentsSeparatedByCharactersInSet:
+ * Credit - Greg Hulands <ghulands@mac.com>
+ * Needed for 10.4+ compatibility
+ */
+- (NSArray *)componentsSeparatedByCharactersInSet:(NSCharacterSet *)set // 10.5 adds this to NSString, but we are 10.4+
+{
+ NSMutableArray *result = [NSMutableArray array];
+ NSScanner *scanner = [NSScanner scannerWithString:self];
+ NSString *chunk = nil;
+
+ [scanner setCharactersToBeSkipped:nil];
+ BOOL sepFound = [scanner scanCharactersFromSet:set intoString:(NSString **)nil]; // skip any preceding separators
+
+ if (sepFound) { // if initial separator, start with empty component
+ [result addObject:@""];
+ }
+
+ while ([scanner scanUpToCharactersFromSet:set intoString:&chunk]) {
+ [result addObject:chunk];
+ sepFound = [scanner scanCharactersFromSet: set intoString: (NSString **) nil];
}
+
+ if (sepFound) { // if final separator, end with empty component
+ [result addObject: @""];
+ }
+
+ result = [result copy];
+ return [result autorelease];
+}
#endif
@end
diff --git a/Source/SPTableData.h b/Source/SPTableData.h
index 0a3a8883..099b1e22 100644
--- a/Source/SPTableData.h
+++ b/Source/SPTableData.h
@@ -42,6 +42,7 @@
- (NSDictionary *) columnWithName:(NSString *)colName;
- (NSArray *) columnNames;
- (NSDictionary *) columnAtIndex:(int)index;
+- (BOOL) columnIsBlobOrText:(NSString *)colName;
- (NSString *) statusValueForKey:(NSString *)aKey;
- (NSDictionary *) statusValues;
- (void) resetAllData;
diff --git a/Source/SPTableData.m b/Source/SPTableData.m
index 1ff4917c..b791563b 100644
--- a/Source/SPTableData.m
+++ b/Source/SPTableData.m
@@ -28,6 +28,7 @@
#import "SPSQLParser.h"
#import "TableDocument.h"
#import "TablesList.h"
+#import "SPStringAdditions.h"
@implementation SPTableData
@@ -130,6 +131,24 @@
return [columns objectAtIndex:index];
}
+/*
+ * Checks if this column is type text or blob.
+ * Used to determine if we have to show a popup when we edit a value from this column.
+ */
+
+- (BOOL) columnIsBlobOrText:(NSString *)colName
+{
+ if ([columns count] == 0) {
+ if ([tableListInstance tableType] == SP_TABLETYPE_VIEW) {
+ [self updateInformationForCurrentView];
+ } else {
+ [self updateInformationForCurrentTable];
+ }
+ }
+
+ return (BOOL) ([[[self columnWithName:colName] objectForKey:@"typegrouping"] isEqualToString:@"textdata" ] || [[[self columnWithName:colName] objectForKey:@"typegrouping"] isEqualToString:@"blobdata"]);
+}
+
/*
* Retrieve the table status value for a supplied key, using or refreshing the cache as appropriate.
@@ -241,11 +260,13 @@
if ([tableName isEqualToString:@""] || !tableName) return nil;
// Retrieve the CREATE TABLE syntax for the table
- CMMCPResult *theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW CREATE TABLE `%@`", tableName]];
+ CMMCPResult *theResult = [mySQLConnection queryString: [NSString stringWithFormat: @"SHOW CREATE TABLE %@",
+ [tableName backtickQuotedString]
+ ]];
// Check for any errors, but only display them if a connection still exists
if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) {
- if (![mySQLConnection isConnected]) {
+ if ([mySQLConnection isConnected]) {
NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while retrieving table information:\n\n%@", [mySQLConnection getLastErrorMessage]], @"OK", nil, nil);
}
return nil;
@@ -284,13 +305,34 @@
// If the first character is a backtick, this is a field definition.
if ([fieldsParser characterAtIndex:0] =='`') {
-
- // Capture the area between the two backticks as the name
- [tableColumn setObject:[fieldsParser trimAndReturnStringFromCharacter:'`' toCharacter:'`' trimmingInclusively:YES returningInclusively:NO ignoringQuotedStrings:NO] forKey:@"name"];
+
+ // Capture the area between the two backticks as the name
+ NSString *fieldName = [fieldsParser trimAndReturnStringFromCharacter: '`'
+ toCharacter: '`'
+ trimmingInclusively: YES
+ returningInclusively: NO
+ ignoringQuotedStrings: NO];
+ //if the next character is again a backtick, we stumbled across an escaped backtick. we have to continue parsing.
+ while ([fieldsParser characterAtIndex:0] =='`') {
+ fieldName = [fieldName stringByAppendingFormat: @"`%@",
+ [fieldsParser trimAndReturnStringFromCharacter: '`'
+ toCharacter: '`'
+ trimmingInclusively: YES
+ returningInclusively: NO
+ ignoringQuotedStrings: NO]
+ ];
+ }
+
+ [tableColumn setObject:fieldName forKey:@"name"];
// Split the remaining field definition string by spaces and process
[tableColumn addEntriesFromDictionary:[self parseFieldDefinitionStringParts:[fieldsParser splitStringByCharacter:' ' skippingBrackets:YES]]];
-
+
+ //if column is not null, but doesn't have a default value, set empty string
+ if([[tableColumn objectForKey:@"null"] intValue] == 0 && [[tableColumn objectForKey:@"autoincrement"] intValue] == 0 && ![tableColumn objectForKey:@"default"]) {
+ [tableColumn setObject:@"" forKey:@"default"];
+ }
+
// Store the column.
[tableColumns addObject:[NSDictionary dictionaryWithDictionary:tableColumn]];
@@ -393,11 +435,11 @@
if ([viewName isEqualToString:@""] || !viewName) return nil;
// Retrieve the SHOW COLUMNS syntax for the table
- CMMCPResult *theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM `%@`", viewName]];
+ CMMCPResult *theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM %@", [viewName backtickQuotedString]]];
// Check for any errors, but only display them if a connection still exists
if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) {
- if (![mySQLConnection isConnected]) {
+ if ([mySQLConnection isConnected]) {
NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while retrieving view information:\n\n%@", [mySQLConnection getLastErrorMessage]], @"OK", nil, nil);
}
return nil;
@@ -431,7 +473,7 @@
// Select the column default if available
if ([resultRow objectForKey:@"Default"]) {
if ([[resultRow objectForKey:@"Default"] isNSNull]) {
- [tableColumn setValue:[NSString stringWithString:[[NSUserDefaults standardUserDefaults] objectForKey:@"nullValue"]] forKey:@"default"];
+ [tableColumn setValue:[NSString stringWithString:[[NSUserDefaults standardUserDefaults] objectForKey:@"NullValue"]] forKey:@"default"];
} else {
[tableColumn setValue:[NSString stringWithString:[resultRow objectForKey:@"Default"]] forKey:@"default"];
}
@@ -477,9 +519,11 @@
// Run the status query and retrieve as a dictionary.
CMMCPResult *tableStatusResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW TABLE STATUS LIKE '%@'", [tableListInstance tableName]]];
- // Check for any errors
+ // Check for any errors, only displaying them if the connection hasn't been terminated
if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) {
- NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while retrieving table status:\n\n%@", [mySQLConnection getLastErrorMessage]], @"OK", nil, nil);
+ if ([mySQLConnection isConnected]) {
+ NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while retrieving table status:\n\n%@", [mySQLConnection getLastErrorMessage]], @"OK", nil, nil);
+ }
return FALSE;
}
@@ -508,12 +552,18 @@
NSMutableDictionary *fieldDetails = [[NSMutableDictionary alloc] init];
NSMutableArray *detailParts;
NSString *detailString;
- int i, partsArrayLength;
+ int i, definitionPartsIndex = 0, partsArrayLength;
if (![definitionParts count]) return [NSDictionary dictionary];
+ // Skip blank items within the definition parts
+ while (definitionPartsIndex < [definitionParts count]
+ && ![[[definitionParts objectAtIndex:definitionPartsIndex] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length])
+ definitionPartsIndex++;
+
// The first item is always the data type.
- [fieldParser setString:[definitionParts objectAtIndex:0]];
+ [fieldParser setString:[definitionParts objectAtIndex:definitionPartsIndex]];
+ definitionPartsIndex++;
// If no field length definition is present, store only the type
if ([fieldParser firstOccurrenceOfCharacter:'(' ignoringQuotedStrings:YES] == NSNotFound) {
@@ -554,6 +604,7 @@
}
[detailParser release];
}
+ [fieldParser release];
// Also capture a general column type "group" to allow behavioural switches
detailString = [[NSString alloc] initWithString:[fieldDetails objectForKey:@"type"]];
@@ -583,6 +634,7 @@
} else {
[fieldDetails setObject:@"blobdata" forKey:@"typegrouping"];
}
+ [detailString release];
// Set up some column defaults for all columns
[fieldDetails setValue:[NSNumber numberWithBool:YES] forKey:@"null"];
@@ -593,8 +645,8 @@
// Walk through the remaining column definition parts storing recognised details
partsArrayLength = [definitionParts count];
- for (i = 1; i < partsArrayLength; i++) {
- detailString = [[NSString alloc] initWithString:[[definitionParts objectAtIndex:i] uppercaseString]];
+ for ( ; definitionPartsIndex < partsArrayLength; definitionPartsIndex++) {
+ detailString = [[NSString alloc] initWithString:[[definitionParts objectAtIndex:definitionPartsIndex] uppercaseString]];
// Whether numeric fields are unsigned
if ([detailString isEqualToString:@"UNSIGNED"]) {
@@ -609,30 +661,30 @@
[fieldDetails setValue:[NSNumber numberWithBool:YES] forKey:@"binary"];
// Whether text types have a different encoding to the table
- } else if ([detailString isEqualToString:@"CHARSET"] && (i + 1 < partsArrayLength)) {
- if (![[[definitionParts objectAtIndex:i+1] uppercaseString] isEqualToString:@"DEFAULT"]) {
- [fieldDetails setValue:[definitionParts objectAtIndex:i+1] forKey:@"encoding"];
+ } else if ([detailString isEqualToString:@"CHARSET"] && (definitionPartsIndex + 1 < partsArrayLength)) {
+ if (![[[definitionParts objectAtIndex:definitionPartsIndex+1] uppercaseString] isEqualToString:@"DEFAULT"]) {
+ [fieldDetails setValue:[definitionParts objectAtIndex:definitionPartsIndex+1] forKey:@"encoding"];
}
- i++;
- } else if ([detailString isEqualToString:@"CHARACTER"] && (i + 2 < partsArrayLength)
- && [[[definitionParts objectAtIndex:i+1] uppercaseString] isEqualToString:@"SET"]) {
- if (![[[definitionParts objectAtIndex:i+2] uppercaseString] isEqualToString:@"DEFAULT"]) {;
- [fieldDetails setValue:[definitionParts objectAtIndex:i+2] forKey:@"encoding"];
+ definitionPartsIndex++;
+ } else if ([detailString isEqualToString:@"CHARACTER"] && (definitionPartsIndex + 2 < partsArrayLength)
+ && [[[definitionParts objectAtIndex:definitionPartsIndex+1] uppercaseString] isEqualToString:@"SET"]) {
+ if (![[[definitionParts objectAtIndex:definitionPartsIndex+2] uppercaseString] isEqualToString:@"DEFAULT"]) {;
+ [fieldDetails setValue:[definitionParts objectAtIndex:definitionPartsIndex+2] forKey:@"encoding"];
}
- i = i + 2;
+ definitionPartsIndex += 2;
// Whether text types have a different collation to the table
- } else if ([detailString isEqualToString:@"COLLATE"] && (i + 1 < partsArrayLength)) {
- if (![[[definitionParts objectAtIndex:i+1] uppercaseString] isEqualToString:@"DEFAULT"]) {
- [fieldDetails setValue:[definitionParts objectAtIndex:i+1] forKey:@"collation"];
+ } else if ([detailString isEqualToString:@"COLLATE"] && (definitionPartsIndex + 1 < partsArrayLength)) {
+ if (![[[definitionParts objectAtIndex:definitionPartsIndex+1] uppercaseString] isEqualToString:@"DEFAULT"]) {
+ [fieldDetails setValue:[definitionParts objectAtIndex:definitionPartsIndex+1] forKey:@"collation"];
}
- i++;
+ definitionPartsIndex++;
// Whether fields are NOT NULL
- } else if ([detailString isEqualToString:@"NOT"] && (i + 1 < partsArrayLength)
- && [[[definitionParts objectAtIndex:i+1] uppercaseString] isEqualToString:@"NULL"]) {
+ } else if ([detailString isEqualToString:@"NOT"] && (definitionPartsIndex + 1 < partsArrayLength)
+ && [[[definitionParts objectAtIndex:definitionPartsIndex+1] uppercaseString] isEqualToString:@"NULL"]) {
[fieldDetails setValue:[NSNumber numberWithBool:NO] forKey:@"null"];
- i++;
+ definitionPartsIndex++;
// Whether fields are NULL
} else if ([detailString isEqualToString:@"NULL"]) {
@@ -643,11 +695,11 @@
[fieldDetails setValue:[NSNumber numberWithBool:YES] forKey:@"autoincrement"];
// Field defaults
- } else if ([detailString isEqualToString:@"DEFAULT"] && (i + 1 < partsArrayLength)) {
- detailParser = [[SPSQLParser alloc] initWithString:[definitionParts objectAtIndex:i+1]];
+ } else if ([detailString isEqualToString:@"DEFAULT"] && (definitionPartsIndex + 1 < partsArrayLength)) {
+ detailParser = [[SPSQLParser alloc] initWithString:[definitionParts objectAtIndex:definitionPartsIndex+1]];
[fieldDetails setValue:[detailParser unquotedString] forKey:@"default"];
[detailParser release];
- i++;
+ definitionPartsIndex++;
}
// TODO: Currently unhandled: [UNIQUE | PRIMARY] KEY | COMMENT 'foo' | COLUMN_FORMAT bar | STORAGE q | REFERENCES...
diff --git a/Source/SPTableInfo.m b/Source/SPTableInfo.m
index cb494ea1..c87a337d 100644
--- a/Source/SPTableInfo.m
+++ b/Source/SPTableInfo.m
@@ -155,7 +155,7 @@
- (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:@"TablePropertyIcon"]];
+ [(ImageAndTextCell*)aCell setImage:[NSImage imageNamed:@"table-property"]];
[(ImageAndTextCell*)aCell setIndentationLevel:1];
} else {
[(ImageAndTextCell*)aCell setImage:nil];
diff --git a/Source/SPTextViewAdditions.h b/Source/SPTextViewAdditions.h
new file mode 100644
index 00000000..95075165
--- /dev/null
+++ b/Source/SPTextViewAdditions.h
@@ -0,0 +1,39 @@
+//
+// SPTextViewAdditions.h
+// sequel-pro
+//
+// Created by Hans-Jörg Bibiko on April 05, 2009
+//
+// 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>
+
+@interface NSTextView (SPTextViewAdditions)
+
+- (IBAction)selectCurrentWord:(id)sender;
+- (IBAction)selectCurrentLine:(id)sender;
+- (IBAction)doSelectionUpperCase:(id)sender;
+- (IBAction)doSelectionLowerCase:(id)sender;
+- (IBAction)doSelectionTitleCase:(id)sender;
+- (IBAction)doDecomposedStringWithCanonicalMapping:(id)sender;
+- (IBAction)doDecomposedStringWithCompatibilityMapping:(id)sender;
+- (IBAction)doPrecomposedStringWithCanonicalMapping:(id)sender;
+- (IBAction)doPrecomposedStringWithCompatibilityMapping:(id)sender;
+- (IBAction)doTranspose:(id)sender;
+- (IBAction)doRemoveDiacritics:(id)sender;
+
+@end \ No newline at end of file
diff --git a/Source/SPTextViewAdditions.m b/Source/SPTextViewAdditions.m
new file mode 100644
index 00000000..885e51df
--- /dev/null
+++ b/Source/SPTextViewAdditions.m
@@ -0,0 +1,287 @@
+//
+// SPTextViewAdditions.m
+// sequel-pro
+//
+// Created by Hans-Jörg Bibiko on April 05, 2009
+//
+// 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 "SPStringAdditions.h"
+
+@implementation NSTextView (SPTextViewAdditions)
+
+/*
+ * Returns the range of the current word.
+ * finds: [| := caret] |word wo|rd word|
+ * If | is in between whitespaces nothing will be selected.
+ */
+- (NSRange)getRangeForCurrentWord
+{
+
+ NSRange curRange = [self selectedRange];
+ unsigned long curLocation = curRange.location;
+
+ [self moveWordLeft:self];
+ [self moveWordRightAndModifySelection:self];
+
+ unsigned long newStartRange = [self selectedRange].location;
+ unsigned long newEndRange = newStartRange + [self selectedRange].length;
+
+ // if current location does not intersect with found range
+ // then caret is at the begin of a word -> change strategy
+ if(curLocation < newStartRange || curLocation > newEndRange)
+ {
+ [self setSelectedRange:curRange];
+ [self moveWordRightAndModifySelection:self];
+ }
+
+ if([[[self string] substringWithRange:[self selectedRange]] rangeOfCharacterFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].location != NSNotFound)
+ [self setSelectedRange:curRange];
+
+ NSRange wordRange = [self selectedRange];
+
+ [self setSelectedRange:curRange];
+
+ return(wordRange);
+
+}
+
+/*
+ * Select current word.
+ * finds: [| := caret] |word wo|rd word|
+ * If | is in between whitespaces nothing will be selected.
+ */
+- (IBAction)selectCurrentWord:(id)sender
+{
+ [self setSelectedRange:[self getRangeForCurrentWord]];
+}
+
+/*
+ * Select current line.
+ */
+- (IBAction)selectCurrentLine:(id)sender
+{
+ [self doCommandBySelector:@selector(moveToBeginningOfLine:)];
+ [self doCommandBySelector:@selector(moveToEndOfLineAndModifySelection:)];
+}
+
+/*
+ * Change selection or current word to upper case and preserves the selection.
+ */
+- (IBAction)doSelectionUpperCase:(id)sender
+{
+ NSRange curRange = [self selectedRange];
+ NSRange selRange = (curRange.length) ? curRange : [self getRangeForCurrentWord];
+ [self setSelectedRange:selRange];
+ [self insertText:[[[self string] substringWithRange:selRange] uppercaseString]];
+ [self setSelectedRange:curRange];
+}
+
+/*
+ * Change selection or current word to lower case and preserves the selection.
+ */
+- (IBAction)doSelectionLowerCase:(id)sender
+{
+ NSRange curRange = [self selectedRange];
+ NSRange selRange = (curRange.length) ? curRange : [self getRangeForCurrentWord];
+ [self setSelectedRange:selRange];
+ [self insertText:[[[self string] substringWithRange:selRange] lowercaseString]];
+ [self setSelectedRange:curRange];
+}
+
+/*
+ * Change selection or current word to title case and preserves the selection.
+ */
+- (IBAction)doSelectionTitleCase:(id)sender
+{
+ NSRange curRange = [self selectedRange];
+ NSRange selRange = (curRange.length) ? curRange : [self getRangeForCurrentWord];
+ [self setSelectedRange:selRange];
+ [self insertText:[[[self string] substringWithRange:selRange] capitalizedString]];
+ [self setSelectedRange:curRange];
+}
+
+/*
+ * Change selection or current word according to Unicode's NFD and preserves the selection.
+ */
+- (IBAction)doDecomposedStringWithCanonicalMapping:(id)sender
+{
+ NSRange curRange = [self selectedRange];
+ NSRange selRange = (curRange.length) ? curRange : [self getRangeForCurrentWord];
+ [self setSelectedRange:selRange];
+ NSString* convString = [[[self string] substringWithRange:selRange] decomposedStringWithCanonicalMapping];
+ [self insertText:convString];
+
+ // correct range for combining characters
+ if(curRange.length)
+ [self setSelectedRange:NSMakeRange(selRange.location, [convString length])];
+ else
+ // if no selection place the caret at the end of the current word
+ {
+ NSRange newRange = [self getRangeForCurrentWord];
+ [self setSelectedRange:NSMakeRange(newRange.location + newRange.length, 0)];
+ }
+}
+
+/*
+ * Change selection or current word according to Unicode's NFKD and preserves the selection.
+ */
+- (IBAction)doDecomposedStringWithCompatibilityMapping:(id)sender
+{
+ NSRange curRange = [self selectedRange];
+ NSRange selRange = (curRange.length) ? curRange : [self getRangeForCurrentWord];
+ [self setSelectedRange:selRange];
+ NSString* convString = [[[self string] substringWithRange:selRange] decomposedStringWithCompatibilityMapping];
+ [self insertText:convString];
+
+ // correct range for combining characters
+ if(curRange.length)
+ [self setSelectedRange:NSMakeRange(selRange.location, [convString length])];
+ else
+ // if no selection place the caret at the end of the current word
+ {
+ NSRange newRange = [self getRangeForCurrentWord];
+ [self setSelectedRange:NSMakeRange(newRange.location + newRange.length, 0)];
+ }
+}
+
+/*
+ * Change selection or current word according to Unicode's NFC and preserves the selection.
+ */
+- (IBAction)doPrecomposedStringWithCanonicalMapping:(id)sender
+{
+ NSRange curRange = [self selectedRange];
+ NSRange selRange = (curRange.length) ? curRange : [self getRangeForCurrentWord];
+ [self setSelectedRange:selRange];
+ NSString* convString = [[[self string] substringWithRange:selRange] precomposedStringWithCanonicalMapping];
+ [self insertText:convString];
+
+ // correct range for combining characters
+ if(curRange.length)
+ [self setSelectedRange:NSMakeRange(selRange.location, [convString length])];
+ else
+ // if no selection place the caret at the end of the current word
+ {
+ NSRange newRange = [self getRangeForCurrentWord];
+ [self setSelectedRange:NSMakeRange(newRange.location + newRange.length, 0)];
+ }
+}
+
+- (IBAction)doRemoveDiacritics:(id)sender
+{
+
+ NSRange curRange = [self selectedRange];
+ NSRange selRange = (curRange.length) ? curRange : [self getRangeForCurrentWord];
+ [self setSelectedRange:selRange];
+ NSString* convString = [[[self string] substringWithRange:selRange] decomposedStringWithCanonicalMapping];
+ NSArray* chars;
+ chars = [convString componentsSeparatedByCharactersInSet:[NSCharacterSet nonBaseCharacterSet]];
+ NSString* cleanString = [chars componentsJoinedByString:@""];
+ [self insertText:cleanString];
+ if(curRange.length)
+ [self setSelectedRange:NSMakeRange(selRange.location, [cleanString length])];
+ else
+ // if no selection place the caret at the end of the current word
+ {
+ NSRange newRange = [self getRangeForCurrentWord];
+ [self setSelectedRange:NSMakeRange(newRange.location + newRange.length, 0)];
+ }
+
+}
+
+/*
+ * Change selection or current word according to Unicode's NFKC to title case and preserves the selection.
+ */
+- (IBAction)doPrecomposedStringWithCompatibilityMapping:(id)sender
+{
+ NSRange curRange = [self selectedRange];
+ NSRange selRange = (curRange.length) ? curRange : [self getRangeForCurrentWord];
+ [self setSelectedRange:selRange];
+ NSString* convString = [[[self string] substringWithRange:selRange] precomposedStringWithCompatibilityMapping];
+ [self insertText:convString];
+
+ // correct range for combining characters
+ if(curRange.length)
+ [self setSelectedRange:NSMakeRange(selRange.location, [convString length])];
+ else
+ // if no selection place the caret at the end of the current word
+ {
+ NSRange newRange = [self getRangeForCurrentWord];
+ [self setSelectedRange:NSMakeRange(newRange.location + newRange.length, 0)];
+ }
+}
+
+
+/*
+ * Transpose adjacent characters, or if a selection is given reverse the selected characters.
+ * If the caret is at the absolute end of the text field it transpose the two last charaters.
+ * If the caret is at the absolute beginnng of the text field do nothing.
+ * TODO: not yet combining-diacritics-safe
+ */
+- (IBAction)doTranspose:(id)sender
+{
+ NSRange curRange = [self selectedRange];
+ NSRange workingRange = curRange;
+
+ if(!curRange.length)
+ @try // caret is in between two chars
+ {
+ if(curRange.location+1 > [[self string] length])
+ {
+ // caret is at the end of a text field
+ // transpose last two characters
+ [self moveLeftAndModifySelection:self];
+ [self moveLeftAndModifySelection:self];
+ workingRange = [self selectedRange];
+ }
+ else if(curRange.location == 0)
+ {
+ // caret is at the beginning of the text field
+ // do nothing
+ workingRange.length = 0;
+ }
+ else
+ {
+ // caret is in between two characters
+ // reverse adjacent characters
+ NSRange twoCharRange = NSMakeRange(curRange.location-1, 2);
+ [self setSelectedRange:twoCharRange];
+ workingRange = twoCharRange;
+ }
+ }
+ @catch(id ae)
+ { workingRange.length = 0; }
+
+
+
+ // reverse string : TODO not yet combining diacritics safe!
+ if(workingRange.length > 1)
+ {
+ NSMutableString *reversedStr;
+ unsigned long len = workingRange.length;
+ reversedStr = [NSMutableString stringWithCapacity:len];
+ while (len > 0)
+ [reversedStr appendString:
+ [NSString stringWithFormat:@"%C", [[self string] characterAtIndex:--len+workingRange.location]]];
+
+ [self insertText:reversedStr];
+ [self setSelectedRange:curRange];
+ }
+}
+
+@end
+
diff --git a/Source/SPWindowAdditions.h b/Source/SPWindowAdditions.h
new file mode 100644
index 00000000..af247427
--- /dev/null
+++ b/Source/SPWindowAdditions.h
@@ -0,0 +1,30 @@
+//
+// SPWindowAdditions.h
+// sequel-pro
+//
+// Created by Stuart Connolly (stuconnolly.com) on Dec 10, 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>
+
+@interface NSWindow (SPWindowAdditions)
+
+- (float)toolbarHeight;
+- (void)resizeForContentView:(NSView *)view titleBarVisible:(BOOL)visible;
+
+@end
diff --git a/Source/SPWindowAdditions.m b/Source/SPWindowAdditions.m
new file mode 100644
index 00000000..d5992a86
--- /dev/null
+++ b/Source/SPWindowAdditions.m
@@ -0,0 +1,75 @@
+//
+// SPWindowAdditions.m
+// sequel-pro
+//
+// Created by Stuart Connolly (stuconnolly.com) on Dec 10, 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 "SPWindowAdditions.h"
+
+@implementation NSWindow (SPWindowAdditions)
+
+// -------------------------------------------------------------------------------
+// toolbarHeight
+//
+// Returns the height of the currently visible toolbar.
+// -------------------------------------------------------------------------------
+- (float)toolbarHeight
+{
+ NSRect windowFrame;
+ float toolbarHeight = 0.0;
+
+ if (([self toolbar]) && ([[self toolbar] isVisible])) {
+ windowFrame = [NSWindow contentRectForFrameRect:[self frame] styleMask:[self styleMask]];
+
+ toolbarHeight = NSHeight(windowFrame) - NSHeight([[self contentView] frame]);
+ }
+
+ return toolbarHeight;
+}
+
+// -------------------------------------------------------------------------------
+// resizeForContentView:titleBarVisible
+//
+// Resizes this window to the size of the supplied view.
+// -------------------------------------------------------------------------------
+- (void)resizeForContentView:(NSView *)view titleBarVisible:(BOOL)visible
+{
+ NSSize viewSize = [view frame].size;
+ NSRect frame = [self frame];
+
+ if ((viewSize.height) < [self contentMinSize].height) {
+ viewSize.height = [self contentMinSize].height;
+ }
+
+ float newHeight = (viewSize.height + [self toolbarHeight]);
+
+ // If the title bar is visible add 22 pixels to new height of window.
+ if (visible) {
+ newHeight += 22;
+ }
+
+ frame.origin.y += frame.size.height - newHeight;
+
+ frame.size.height = newHeight;
+ frame.size.width = viewSize.width;
+
+ [self setFrame:frame display:YES animate:YES];
+}
+
+@end
diff --git a/Source/TableContent.h b/Source/TableContent.h
index f18659a0..cc242aba 100644
--- a/Source/TableContent.h
+++ b/Source/TableContent.h
@@ -25,16 +25,14 @@
#import <Cocoa/Cocoa.h>
#import <MCPKit_bundled/MCPKit_bundled.h>
-#import "CMCopyTable.h"
-#import "CMMCPConnection.h"
-#import "CMMCPResult.h"
+
+@class CMMCPConnection, CMMCPResult, CMCopyTable;
@interface TableContent : NSObject
{
IBOutlet id tableDocumentInstance;
IBOutlet id tablesListInstance;
IBOutlet id tableDataInstance;
- IBOutlet id queryConsoleInstance;
IBOutlet id tableWindow;
IBOutlet CMCopyTable *tableContentView;
@@ -66,7 +64,7 @@
NSString *compareType, *sortField;
BOOL isEditingRow, isEditingNewRow, isSavingRow, isDesc, setLimit;
NSUserDefaults *prefs;
- int numRows, currentlyEditingRow;
+ int numRows, currentlyEditingRow, maxNumRowsOfCurrentTable;
bool areShowingAllRows;
}
diff --git a/Source/TableContent.m b/Source/TableContent.m
index a25a2cf4..d52a2dda 100644
--- a/Source/TableContent.m
+++ b/Source/TableContent.m
@@ -27,9 +27,14 @@
#import "TableDocument.h"
#import "TablesList.h"
#import "CMImageView.h"
+#import "CMCopyTable.h"
+#import "CMMCPConnection.h"
+#import "CMMCPResult.h"
#import "SPDataCellFormatter.h"
#import "SPTableData.h"
#import "SPQueryConsole.h"
+#import "SPStringAdditions.h"
+#import "SPArrayAdditions.h"
@implementation TableContent
@@ -45,7 +50,7 @@
sortField = nil;
areShowingAllRows = false;
currentlyEditingRow = -1;
-
+
return self;
}
@@ -83,6 +88,7 @@
// Remove existing columns from the table
theColumns = [tableContentView tableColumns];
+
while ([theColumns count]) {
[tableContentView removeTableColumn:[theColumns objectAtIndex:0]];
}
@@ -90,7 +96,6 @@
// If no table has been supplied, reset the view to a blank table and disabled elements
if ( [aTable isEqualToString:@""] || !aTable )
{
-
// Empty the stored data arrays
[fullResult removeAllObjects];
[filteredResult removeAllObjects];
@@ -127,10 +132,14 @@
// Post a notification that a query will be performed
[[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self];
- // Retrieve the field names and types for this table from the data cache. This is used when requesting all data as part
+ // Retrieve the field names and types for this table from the data cache. This is used when requesting all data as part
// of the fieldListForQuery method, and also to decide whether or not to preserve the current filter/sort settings.
theColumns = [tableDataInstance columns];
columnNames = [tableDataInstance columnNames];
+
+ // Retrieve the total number of rows of the current table
+ // to adjustify "Limit From:"
+ maxNumRowsOfCurrentTable = [[[tableDataInstance statusValues] objectForKey:@"Rows"] intValue];
// Retrieve the number of rows in the table and initially mark all as being visible.
numRows = [self getNumberOfRows];
@@ -171,7 +180,7 @@
}
// Set the data cell font according to the preferences
- if ( [prefs boolForKey:@"useMonospacedFonts"] ) {
+ if ( [prefs boolForKey:@"UseMonospacedFonts"] ) {
[dataCell setFont:[NSFont fontWithName:@"Monaco" size:10]];
} else {
[dataCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
@@ -203,7 +212,10 @@
// Otherwise, clear sorting
} else {
- sortField = nil;
+ if (sortField) {
+ [sortField release];
+ sortField = nil;
+ }
isDesc = NO;
}
@@ -232,6 +244,7 @@
[fieldField selectItemWithTitle:preservedFilterField];
[self setCompareTypes:self];
}
+
if (preserveCurrentView && preservedFilterField != nil
&& [fieldField itemWithTitle:preservedFilterField]
&& [compareField itemWithTitle:preservedFilterComparison]) {
@@ -241,7 +254,7 @@
}
// Enable or disable the limit fields according to preference setting
- if ( [prefs boolForKey:@"limitRows"] ) {
+ if ( [prefs boolForKey:@"LimitResults"] ) {
// Attempt to preserve the limit value if it's still valid
if (!preserveCurrentView || [limitRowsField intValue] < 1 || [limitRowsField intValue] >= numRows) {
@@ -251,8 +264,8 @@
[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)
+ [prefs integerForKey:@"LimitResultsValue"]]];
+ if ([prefs integerForKey:@"LimitResultsValue"] < numRows)
areShowingAllRows = NO;
} else {
[limitRowsField setEnabled:NO];
@@ -262,26 +275,26 @@
[limitRowsText setStringValue:NSLocalizedString(@"No limit", @"text showing that the result isn't limited")];
}
- // Enable the table buttons
+ // set the state of the table buttons
[addButton setEnabled:YES];
- [copyButton setEnabled:YES];
- [removeButton setEnabled:YES];
+ [copyButton setEnabled:NO];
+ [removeButton setEnabled:NO];
// 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];
+ query = [NSString stringWithFormat:@"SELECT %@ FROM %@", [self fieldListForQuery], [selectedTable backtickQuotedString]];
if ( sortField ) {
- query = [NSString stringWithFormat:@"%@ ORDER BY `%@`", query, sortField];
+ query = [NSString stringWithFormat:@"%@ ORDER BY %@", query, [sortField backtickQuotedString]];
if ( isDesc )
query = [query stringByAppendingString:@" DESC"];
}
- if ( [prefs boolForKey:@"limitRows"] ) {
+ if ( [prefs boolForKey:@"LimitResults"] ) {
if ( [limitRowsField intValue] <= 0 ) {
[limitRowsField setStringValue:@"1"];
}
query = [query stringByAppendingString:
[NSString stringWithFormat:@" LIMIT %d,%d",
- [limitRowsField intValue]-1, [prefs integerForKey:@"limitRowsValue"]]];
+ [limitRowsField intValue]-1, [prefs integerForKey:@"LimitResultsValue"]]];
}
queryResult = [mySQLConnection queryString:query];
@@ -291,17 +304,25 @@
}
[fullResult setArray:[self fetchResultAsArray:queryResult]];
+
+ // This to fix an issue where by areShowingAllRows is set to NO above during the restore of the filter options
+ // leading the code to believe that the result set is filtered. If the filtered result set count is the same as the
+ // maximum rows in the table then filtering is currently not in use and we set areShowingAllRows back to YES.
+ if ([filteredResult count] == maxNumRowsOfCurrentTable) {
+ areShowingAllRows = YES;
+ }
// 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 {
+ }
+ else {
[filteredResult setArray:fullResult];
[countText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%d rows in table", @"text showing how many rows are in the result"), [fullResult count]]];
}
- // Reload the table data.
+ // Reload the table data
[tableContentView reloadData];
// Post the notification that the query is finished
@@ -309,13 +330,12 @@
}
/*
- * Reloads the current table data, performing a new SQL query. Now attempts to preserve sort order, filters, and viewport.
+ * Reloads the current table data, performing a new SQL query. Now attempts to preserve sort order, filters, and viewport.
*/
- (IBAction)reloadTable:(id)sender
{
-
// Check whether a save of the current row is required.
- if ( ![self saveRowOnDeselect] ) return;
+ if (![self saveRowOnDeselect]) return;
// Store the current viewport location
NSRect viewRect = [tableContentView visibleRect];
@@ -323,13 +343,13 @@
// Clear the table data column cache
[tableDataInstance resetColumnData];
+ // Load the table's data
[self loadTable:selectedTable];
// Restore the viewport
[tableContentView scrollRectToVisible:viewRect];
}
-
/*
* Reload the table values without reconfiguring the tableView (with filter and limit if set)
*/
@@ -342,12 +362,12 @@
[[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self];
//enable or disable limit fields
- if ( [prefs boolForKey:@"limitRows"] ) {
+ if ( [prefs boolForKey:@"LimitResults"] ) {
[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"]]];
+ [prefs integerForKey:@"LimitResultsValue"]]];
} else {
[limitRowsField setEnabled:NO];
[limitRowsButton setEnabled:NO];
@@ -357,20 +377,20 @@
}
// queryString = [@"SELECT * FROM " stringByAppendingString:selectedTable];
- queryString = [NSString stringWithFormat:@"SELECT %@ FROM `%@`", [self fieldListForQuery], selectedTable];
+ queryString = [NSString stringWithFormat:@"SELECT %@ FROM %@", [self fieldListForQuery], [selectedTable backtickQuotedString]];
if ( sortField ) {
- queryString = [NSString stringWithFormat:@"%@ ORDER BY `%@`", queryString, sortField];
- // queryString = [queryString stringByAppendingString:[NSString stringWithFormat:@" ORDER BY `%@`", sortField]];
+ queryString = [NSString stringWithFormat:@"%@ ORDER BY %@", queryString, [sortField backtickQuotedString]];
+ // queryString = [queryString stringByAppendingString:[NSString stringWithFormat:@" ORDER BY %@", [sortField backtickQuotedString]]];
if ( isDesc )
queryString = [queryString stringByAppendingString:@" DESC"];
}
- if ( [prefs boolForKey:@"limitRows"] ) {
+ if ( [prefs boolForKey:@"LimitResults"] ) {
if ( [limitRowsField intValue] <= 0 ) {
[limitRowsField setStringValue:@"1"];
}
queryString = [queryString stringByAppendingString:
[NSString stringWithFormat:@" LIMIT %d,%d",
- [limitRowsField intValue]-1, [prefs integerForKey:@"limitRowsValue"]]];
+ [limitRowsField intValue]-1, [prefs integerForKey:@"LimitResultsValue"]]];
[limitRowsField selectText:self];
}
queryResult = [mySQLConnection queryString:queryString];
@@ -413,10 +433,17 @@
if ( [limitRowsField intValue] <= 0 ) {
[limitRowsField setStringValue:@"1"];
}
+
+ // If limitRowsField > number of total found rows show the last limitRowsValue rows
+ if ( [prefs boolForKey:@"LimitResults"] && [limitRowsField intValue] >= maxNumRowsOfCurrentTable ) {
+ int newLimit = maxNumRowsOfCurrentTable - [prefs integerForKey:@"LimitResultsValue"];
+ [limitRowsField setStringValue:[[NSNumber numberWithInt:(newLimit<1)?1:newLimit] stringValue]];
+ }
+
// 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)) {
+ if (([argument length] == 0) && (![[[compareField selectedItem] title] hasSuffix:@"NULL"]) && (![prefs boolForKey:@"LimitResults"] || [limitRowsField intValue] == 1)) {
[argument release];
[self showAll:sender];
return;
@@ -429,7 +456,7 @@
BOOL ignoreArgument = NO;
// Start building the query string
- queryString = [NSString stringWithFormat:@"SELECT %@ FROM `%@`", [self fieldListForQuery], selectedTable];
+ queryString = [NSString stringWithFormat:@"SELECT %@ FROM %@", [self fieldListForQuery], [selectedTable backtickQuotedString]];
// Add filter if appropriate
if (([argument length] > 0) || [[[compareField selectedItem] title] hasSuffix:@"NULL"]) {
@@ -454,7 +481,7 @@
case 4:
compareOperator = @"IN";
doQuote = NO;
- [argument setString:[[@"('" stringByAppendingString:argument] stringByAppendingString:@"')"]];
+ [argument setString:[[@"(" stringByAppendingString:argument] stringByAppendingString:@")"]];
break;
case 5:
compareOperator = @"IS NULL";
@@ -551,11 +578,11 @@
}
}
[argument setString:[mySQLConnection prepareString:argument]];
- queryString = [NSString stringWithFormat:@"%@ WHERE `%@` %@ \"%@\"",
- queryString, [fieldField titleOfSelectedItem], compareOperator, argument];
+ queryString = [NSString stringWithFormat:@"%@ WHERE %@ %@ \"%@\"",
+ queryString, [[fieldField titleOfSelectedItem] backtickQuotedString], compareOperator, argument];
} else {
- queryString = [NSString stringWithFormat:@"%@ WHERE `%@` %@ %@",
- queryString, [fieldField titleOfSelectedItem],
+ queryString = [NSString stringWithFormat:@"%@ WHERE %@ %@ %@",
+ queryString, [[fieldField titleOfSelectedItem] backtickQuotedString],
compareOperator, (ignoreArgument) ? @"" : argument];
}
}
@@ -563,20 +590,33 @@
// Add sorting details if appropriate
if ( sortField ) {
- queryString = [NSString stringWithFormat:@"%@ ORDER BY `%@`", queryString, sortField];
+ queryString = [NSString stringWithFormat:@"%@ ORDER BY %@", queryString, [sortField backtickQuotedString]];
if ( isDesc )
queryString = [queryString stringByAppendingString:@" DESC"];
}
+ // retain the query before LIMIT
+ // to redo the query if nothing found for LIMIT > 1
+ NSString* tempQueryString;
// LIMIT if appropriate
- if ( [prefs boolForKey:@"limitRows"] ) {
+ if ( [prefs boolForKey:@"LimitResults"] ) {
+ tempQueryString = [NSString stringWithString:queryString];
queryString = [NSString stringWithFormat:@"%@ LIMIT %d,%d", queryString,
- [limitRowsField intValue]-1, [prefs integerForKey:@"limitRowsValue"]];
+ [limitRowsField intValue]-1, [prefs integerForKey:@"LimitResultsValue"]];
}
-
+
theResult = [mySQLConnection queryString:queryString];
[filteredResult setArray:[self fetchResultAsArray:theResult]];
+ // try it again if theResult is empty and limitRowsField > 1 by setting LIMIT to 0, limitRowsValue
+ if([prefs boolForKey:@"LimitResults"] && [limitRowsField intValue] > 1 && [filteredResult count] == 0) {
+ queryString = [NSString stringWithFormat:@"%@ LIMIT %d,%d", tempQueryString,
+ 0, [prefs integerForKey:@"LimitResultsValue"]];
+ theResult = [mySQLConnection queryString:queryString];
+ [limitRowsField setStringValue:@"1"];
+ [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
@@ -632,8 +672,8 @@
columns = [[NSArray alloc] initWithArray:[tableDataInstance columns]];
for ( i = 0 ; i < [columns count] ; i++ ) {
column = [columns objectAtIndex:i];
- if ([column objectForKey:@"default"] == nil) {
- [newRow setObject:[prefs stringForKey:@"nullValue"] forKey:[column objectForKey:@"name"]];
+ if ([column objectForKey:@"default"] == nil || [[column objectForKey:@"default"] isEqualToString:@"NULL"]) {
+ [newRow setObject:[prefs stringForKey:@"NullValue"] forKey:[column objectForKey:@"name"]];
} else {
[newRow setObject:[column objectForKey:@"default"] forKey:[column objectForKey:@"name"]];
}
@@ -657,7 +697,7 @@
{
NSMutableDictionary *tempRow;
CMMCPResult *queryResult;
- NSDictionary *row;
+ NSDictionary *row, *dbDataRow;
int i;
// Check whether a save of the current row is required.
@@ -673,15 +713,37 @@
//copy row
tempRow = [NSMutableDictionary dictionaryWithDictionary:[filteredResult objectAtIndex:[tableContentView selectedRow]]];
[filteredResult insertObject:tempRow atIndex:[tableContentView selectedRow]+1];
+
+ //if we don't show blobs, read data for this duplicate column from db
+ if ([prefs boolForKey:@"LoadBlobsAsNeeded"]) {
+ // Abort if there are no indices on this table - argumentForRow will display an error.
+ if (![[self argumentForRow:[tableContentView selectedRow]] length]){
+ return;
+ }
+ //if we have indexes, use argumentForRow
+ queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SELECT * FROM %@ WHERE %@", [selectedTable backtickQuotedString], [self argumentForRow:[tableContentView selectedRow]]]];
+ dbDataRow = [queryResult fetchRowAsDictionary];
+ }
+
+
//set autoincrement fields to NULL
- queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM `%@`", selectedTable]];
+ queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM %@", [selectedTable backtickQuotedString]]];
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"]];
+ [tempRow setObject:[prefs stringForKey:@"NullValue"] forKey:[row objectForKey:@"Field"]];
+ } else if ( [tableDataInstance columnIsBlobOrText:[row objectForKey:@"Field"]] && [prefs boolForKey:@"LoadBlobsAsNeeded"] && dbDataRow) {
+ NSString *valueString = nil;
+ //if what we read from DB is NULL (NSNull), we replace it with the string NULL
+ if([[dbDataRow objectForKey:[row objectForKey:@"Field"]] isKindOfClass:[NSNull class]])
+ valueString = [prefs objectForKey:@"NullValue"];
+ else
+ valueString = [dbDataRow objectForKey:[row objectForKey:@"Field"]];
+ [tempRow setObject:valueString forKey:[row objectForKey:@"Field"]];
}
}
+
//select row and go in edit mode
[tableContentView reloadData];
[tableContentView selectRow:[tableContentView selectedRow]+1 byExtendingSelection:NO];
@@ -705,11 +767,11 @@
/*
if ( ([tableContentView numberOfSelectedRows] == [self numberOfRowsInTableView:tableContentView]) &&
areShowingAllRows &&
- (![prefs boolForKey:@"limitRows"] || ([tableContentView numberOfSelectedRows] < [prefs integerForKey:@"limitRowsValue"])) ) {
+ (![prefs boolForKey:@"LimitResults"] || ([tableContentView numberOfSelectedRows] < [prefs integerForKey:@"LimitResultsValue"])) ) {
*/
if ( ([tableContentView numberOfSelectedRows] == [tableContentView numberOfRows]) &&
- (([prefs boolForKey:@"limitRows"] && [tableContentView numberOfSelectedRows] == [self fetchNumberOfRows]) ||
- (![prefs boolForKey:@"limitRows"] && [tableContentView numberOfSelectedRows] == [self getNumberOfRows])) ) {
+ (([prefs boolForKey:@"LimitResults"] && [tableContentView numberOfSelectedRows] == [self fetchNumberOfRows]) ||
+ (![prefs boolForKey:@"LimitResults"] && [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 ) {
@@ -989,7 +1051,7 @@
[tableContentView setVerticalMotionCanBeginDrag:NO];
prefs = [[NSUserDefaults standardUserDefaults] retain];
- if ( [prefs boolForKey:@"useMonospacedFonts"] ) {
+ 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]]];
@@ -1000,9 +1062,9 @@
}
[hexTextView setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]];
[limitRowsStepper setEnabled:NO];
- if ( [prefs boolForKey:@"limitRows"] ) {
+ if ( [prefs boolForKey:@"LimitResults"] ) {
[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"]]];
+ [prefs integerForKey:@"LimitResultsValue"]]];
} else {
[limitRowsText setStringValue:NSLocalizedString(@"No limit", @"text showing that the result isn't limited")];
[limitRowsField setStringValue:@""];
@@ -1016,7 +1078,7 @@
{
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 *numberTypes = [NSArray arrayWithObjects:@"=", @"≠", @">", @"<", @"≥", @"≤", @"IN", 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];
+ NSArray *dateTypes = [NSArray arrayWithObjects:NSLocalizedString(@"is", @"popup menuitem for field IS value"), NSLocalizedString(@"is not", @"popup menuitem for field IS NOT value"), NSLocalizedString(@"is after", @"popup menuitem for field AFTER DATE value"), NSLocalizedString(@"is before", @"popup menuitem for field BEFORE DATE value"), NSLocalizedString(@"is after or equal to", @"popup menuitem for field AFTER OR EQUAL TO value"), NSLocalizedString(@"is before or equal to", @"popup menuitem for field BEFORE OR EQUAL TO value"), nil];
NSString *fieldTypeGrouping = [NSString stringWithString:[[tableDataInstance columnWithName:[[fieldField selectedItem] title]] objectForKey:@"typegrouping"]];
int i;
@@ -1083,12 +1145,14 @@
*/
{
if ( [limitRowsStepper intValue] > 0 ) {
- [limitRowsField setIntValue:[limitRowsField intValue]+[prefs integerForKey:@"limitRowsValue"]];
+ int newStep = [limitRowsField intValue]+[prefs integerForKey:@"LimitResultsValue"];
+ // if newStep > the total number of rows in the current table retain the old value
+ [limitRowsField setIntValue:(newStep>maxNumRowsOfCurrentTable)?[limitRowsField intValue]:newStep];
} else {
- if ( ([limitRowsField intValue]-[prefs integerForKey:@"limitRowsValue"]) < 1 ) {
+ if ( ([limitRowsField intValue]-[prefs integerForKey:@"LimitResultsValue"]) < 1 ) {
[limitRowsField setIntValue:1];
} else {
- [limitRowsField setIntValue:[limitRowsField intValue]-[prefs integerForKey:@"limitRowsValue"]];
+ [limitRowsField setIntValue:[limitRowsField intValue]-[prefs integerForKey:@"LimitResultsValue"]];
}
}
[limitRowsStepper setIntValue:0];
@@ -1100,23 +1164,15 @@
- (NSArray *)fetchResultAsArray:(CMMCPResult *)theResult
{
NSArray *columns;
- NSString *columnTypeGrouping;
- NSMutableArray *columnsBlobStatus, *tempResult = [NSMutableArray array];
+ NSMutableArray *tempResult = [NSMutableArray array];
+
NSDictionary *tempRow;
NSMutableDictionary *modifiedRow = [NSMutableDictionary dictionary];
NSEnumerator *enumerator;
id key;
int i, j;
- BOOL columnIsBlobOrText;
-
+
columns = [tableDataInstance columns];
- columnsBlobStatus = [[NSMutableArray alloc] init];
- for ( i = 0 ; i < [columns count]; i++ ) {
- columnTypeGrouping = [[columns objectAtIndex:i] objectForKey:@"typegrouping"];
- columnIsBlobOrText = ([columnTypeGrouping isEqualToString:@"textdata"] || [columnTypeGrouping isEqualToString:@"blobdata"]);
- [columnsBlobStatus addObject:[NSNumber numberWithBool:columnIsBlobOrText]];
- }
-
if ([theResult numOfRows]) [theResult dataSeek:0];
for ( i = 0 ; i < [theResult numOfRows] ; i++ ) {
tempRow = [theResult fetchRowAsDictionary];
@@ -1124,17 +1180,17 @@
while ( key = [enumerator nextObject] ) {
if ( [[tempRow objectForKey:key] isMemberOfClass:[NSNull class]] ) {
- [modifiedRow setObject:[prefs stringForKey:@"nullValue"] forKey:key];
+ [modifiedRow setObject:[prefs stringForKey:@"NullValue"] forKey:key];
} else {
[modifiedRow setObject:[tempRow objectForKey:key] forKey:key];
}
}
// Add values for hidden blob and text fields if appropriate
- if ( [prefs boolForKey:@"dontShowBlob"] ) {
+ if ( [prefs boolForKey:@"LoadBlobsAsNeeded"] ) {
for ( j = 0 ; j < [columns count] ; j++ ) {
- if ( [[columnsBlobStatus objectAtIndex:j] boolValue] ) {
- [modifiedRow setObject:NSLocalizedString(@"- blob or text -", @"value shown for hidden blob and text fields") forKey:[[columns objectAtIndex:j] objectForKey:@"name"]];
+ if ( [tableDataInstance columnIsBlobOrText:[[columns objectAtIndex:j] objectForKey:@"name"] ] ) {
+ [modifiedRow setObject:NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields") forKey:[[columns objectAtIndex:j] objectForKey:@"name"]];
}
}
}
@@ -1142,8 +1198,6 @@
[tempResult addObject:[NSMutableDictionary dictionaryWithDictionary:modifiedRow]];
}
- [columnsBlobStatus release];
-
return tempResult;
}
@@ -1156,7 +1210,6 @@
- (BOOL)addRowToDB
{
NSArray *theColumns, *columnNames;
- NSMutableArray *fieldValues = [[NSMutableArray alloc] init];
NSMutableString *queryString;
NSString *query;
CMMCPResult *queryResult;
@@ -1166,7 +1219,6 @@
int i;
if ( !isEditingRow || currentlyEditingRow == -1) {
- [fieldValues release];
return YES;
}
@@ -1183,12 +1235,12 @@
theColumns = [tableDataInstance columns];
columnNames = [tableDataInstance columnNames];
+ NSMutableArray *fieldValues = [[NSMutableArray alloc] init];
// Get the field values
for ( i = 0 ; i < [columnNames count] ; i++ ) {
rowObject = [[filteredResult objectAtIndex:currentlyEditingRow] objectForKey:[columnNames 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"]]
+ 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
@@ -1219,18 +1271,18 @@
// Use INSERT syntax when creating new rows
if ( isEditingNewRow ) {
- queryString = [NSString stringWithFormat:@"INSERT INTO `%@` (`%@`) VALUES (%@)",
- selectedTable, [columnNames componentsJoinedByString:@"`,`"], [fieldValues componentsJoinedByString:@","]];
+ queryString = [NSString stringWithFormat:@"INSERT INTO %@ (%@) VALUES (%@)",
+ [selectedTable backtickQuotedString], [columnNames componentsJoinedAndBacktickQuoted], [fieldValues componentsJoinedByString:@","]];
// Use UPDATE syntax otherwise
} else {
- queryString = [NSMutableString stringWithFormat:@"UPDATE `%@` SET ", selectedTable];
+ queryString = [NSMutableString stringWithFormat:@"UPDATE %@ SET ", [selectedTable backtickQuotedString]];
for ( i = 0 ; i < [columnNames count] ; i++ ) {
if ( i > 0 ) {
[queryString appendString:@", "];
}
- [queryString appendString:[NSString stringWithFormat:@"`%@`=%@",
- [columnNames objectAtIndex:i], [fieldValues objectAtIndex:i]]];
+ [queryString appendString:[NSString stringWithFormat:@"%@=%@",
+ [[columnNames objectAtIndex:i] backtickQuotedString], [fieldValues objectAtIndex:i]]];
}
[queryString appendString:[NSString stringWithFormat:@" WHERE %@", [self argumentForRow:-2]]];
}
@@ -1239,7 +1291,7 @@
// If no rows have been changed, show error if appropriate.
if ( ![mySQLConnection affectedRows] ) {
- if ( [prefs boolForKey:@"showError"] ) {
+ if ( [prefs boolForKey:@"ShowNoAffectedRowsError"] ) {
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 {
@@ -1249,7 +1301,7 @@
isEditingRow = NO;
isEditingNewRow = NO;
currentlyEditingRow = -1;
- [queryConsoleInstance 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]];
+ [[SPQueryConsole sharedQueryConsole] 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;
// On success...
@@ -1258,7 +1310,7 @@
// New row created successfully
if ( isEditingNewRow ) {
- if ( [prefs boolForKey:@"reloadAfterAdding"] ) {
+ if ( [prefs boolForKey:@"ReloadAfterAddingRow"] ) {
[self reloadTableValues:self];
[tableContentView deselectAll:self];
[tableWindow endEditingFor:nil];
@@ -1277,26 +1329,26 @@
// Existing row edited successfully
} else {
- if ( [prefs boolForKey:@"reloadAfterEditing"] ) {
+ if ( [prefs boolForKey:@"ReloadAfterEditingRow"] ) {
[self reloadTableValues:self];
[tableContentView deselectAll:self];
[tableWindow endEditingFor:nil];
// TODO: this probably needs looking at... it's reloading it all itself?
} else {
- query = [NSString stringWithFormat:@"SELECT %@ FROM `%@`", [self fieldListForQuery], selectedTable];
+ query = [NSString stringWithFormat:@"SELECT %@ FROM %@", [self fieldListForQuery], [selectedTable backtickQuotedString]];
if ( sortField ) {
- query = [NSString stringWithFormat:@"%@ ORDER BY `%@`", query, sortField];
+ query = [NSString stringWithFormat:@"%@ ORDER BY %@", query, [sortField backtickQuotedString]];
if ( isDesc )
query = [query stringByAppendingString:@" DESC"];
}
- if ( [prefs boolForKey:@"limitRows"] ) {
+ if ( [prefs boolForKey:@"LimitResults"] ) {
if ( [limitRowsField intValue] <= 0 ) {
[limitRowsField setStringValue:@"1"];
}
query = [query stringByAppendingString:
[NSString stringWithFormat:@" LIMIT %d,%d",
- [limitRowsField intValue]-1, [prefs integerForKey:@"limitRowsValue"]]];
+ [limitRowsField intValue]-1, [prefs integerForKey:@"LimitResultsValue"]]];
}
queryResult = [mySQLConnection queryString:query];
[fullResult setArray:[self fetchResultAsArray:queryResult]];
@@ -1369,7 +1421,7 @@
if ( !keys ) {
setLimit = NO;
keys = [[NSMutableArray alloc] init];
- theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM `%@`", selectedTable]];
+ theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM %@", [selectedTable backtickQuotedString]]];
if ([theResult numOfRows]) [theResult dataSeek:0];
for ( i = 0 ; i < [theResult numOfRows] ; i++ ) {
theRow = [theResult fetchRowAsDictionary];
@@ -1382,11 +1434,11 @@
// If there is no primary key, all the fields are used in the argument.
if ( ![keys count] ) {
[keys setArray:columnNames];
- setLimit = YES;
-
+ setLimit = YES;
+
// When the option to not show blob or text options is set, we have a problem - we don't have
// the right values to use in the WHERE statement. Throw an error if this is the case.
- if ( [prefs boolForKey:@"dontShowBlob"] && [self tableContainsBlobOrTextColumns] ) {
+ if ( [prefs boolForKey:@"LoadBlobsAsNeeded"] && [self tableContainsBlobOrTextColumns] ) {
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];
@@ -1419,8 +1471,8 @@
[value setString:[tempValue description]];
}
- if ( [value isEqualToString:[prefs stringForKey:@"nullValue"]] ) {
- [argument appendString:[NSString stringWithFormat:@"`%@` IS NULL", [keys objectAtIndex:i]]];
+ if ( [value isEqualToString:[prefs stringForKey:@"NullValue"]] ) {
+ [argument appendString:[NSString stringWithFormat:@"%@ IS NULL", [[keys objectAtIndex:i] backtickQuotedString]]];
} else {
// Escape special characters (in WHERE statement!)
@@ -1442,9 +1494,9 @@
columnType = [[tableDataInstance columnWithName:[keys objectAtIndex:i]] objectForKey:@"typegrouping"];
if ( [columnType isEqualToString:@"integer"] || [columnType isEqualToString:@"float"] || [columnType isEqualToString:@"bit"] ) {
- [argument appendString:[NSString stringWithFormat:@"`%@` = %@", [keys objectAtIndex:i], value]];
+ [argument appendString:[NSString stringWithFormat:@"%@ = %@", [[keys objectAtIndex:i] backtickQuotedString], value]];
} else {
- [argument appendString:[NSString stringWithFormat:@"`%@` LIKE %@", [keys objectAtIndex:i], value]];
+ [argument appendString:[NSString stringWithFormat:@"%@ LIKE %@", [[keys objectAtIndex:i] backtickQuotedString], value]];
}
}
}
@@ -1462,11 +1514,9 @@
{
int i;
NSArray *tableColumns = [tableDataInstance columns];
- NSString *columnTypeGrouping;
for ( i = 0 ; i < [tableColumns count]; i++ ) {
- columnTypeGrouping = [[tableColumns objectAtIndex:i] objectForKey:@"typegrouping"];
- if ([columnTypeGrouping isEqualToString:@"textdata"] || [columnTypeGrouping isEqualToString:@"blobdata"]) {
+ if ( [tableDataInstance columnIsBlobOrText:[[tableColumns objectAtIndex:i] objectForKey:@"name"]] ) {
return YES;
}
}
@@ -1484,21 +1534,19 @@
NSMutableArray *fields = [NSMutableArray array];
NSArray *columns = [tableDataInstance columns];
NSArray *columnNames = [tableDataInstance columnNames];
- NSString *columnTypeGrouping;
- if ( [prefs boolForKey:@"dontShowBlob"] ) {
+ if ( [prefs boolForKey:@"LoadBlobsAsNeeded"] ) {
for ( i = 0 ; i < [columnNames count] ; i++ ) {
- columnTypeGrouping = [[columns objectAtIndex:i] objectForKey:@"typegrouping"];
- if (![columnTypeGrouping isEqualToString:@"textdata"] && ![columnTypeGrouping isEqualToString:@"blobdata"]) {
+ if (![tableDataInstance columnIsBlobOrText:[[columns objectAtIndex:i] objectForKey:@"name"]] ) {
[fields addObject:[columnNames objectAtIndex:i]];
}
}
// Always select at least one field - the first if there are no non-blob fields.
if ( [fields count] == 0 ) {
- return [NSString stringWithFormat:@"`%@`", [columnNames objectAtIndex:0]];
+ return [[columnNames objectAtIndex:0] backtickQuotedString];
} else {
- return [NSString stringWithFormat:@"`%@`", [fields componentsJoinedByString:@"`,`"]];
+ return [fields componentsJoinedAndBacktickQuoted];
}
} else {
return @"*";
@@ -1515,7 +1563,7 @@
NSNumber *index;
NSMutableArray *tempArray = [NSMutableArray array];
NSMutableArray *tempResult = [NSMutableArray array];
- NSString *queryString;
+ NSString *queryString, *wherePart;
CMMCPResult *queryResult;
int i, errors;
@@ -1543,9 +1591,9 @@
/*
if ( ([tableContentView numberOfSelectedRows] == [self numberOfRowsInTableView:tableContentView]) &&
areShowingAllRows &&
- ([tableContentView numberOfSelectedRows] < [prefs integerForKey:@"limitRowsValue"]) ) {
+ ([tableContentView numberOfSelectedRows] < [prefs integerForKey:@"LimitResultsValue"]) ) {
*/
- [mySQLConnection queryString:[NSString stringWithFormat:@"DELETE FROM `%@`", selectedTable]];
+ [mySQLConnection queryString:[NSString stringWithFormat:@"DELETE FROM %@", [selectedTable backtickQuotedString]]];
if ( [[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) {
[self reloadTable:self];
} else {
@@ -1559,18 +1607,24 @@
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];
+ wherePart = [NSString stringWithString:[self argumentForRow:[index intValue]]];
+ //argumentForRow might return empty query, in which case we shouldn't execute the partial query
+ if([wherePart length] > 0) {
+ [mySQLConnection queryString:[NSString stringWithFormat:@"DELETE FROM %@ WHERE %@", [selectedTable backtickQuotedString], wherePart]];
+ 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++;
+ }
} else {
- //error in mysql-query
errors++;
}
+
}
if ( errors ) {
@@ -1578,7 +1632,7 @@
}
//do deleting (after enumerating)
- if ( [prefs boolForKey:@"reloadAfterRemoving"] ) {
+ if ( [prefs boolForKey:@"ReloadAfterRemovingRow"] ) {
[self reloadTableValues:self];
} else {
for ( i = 0 ; i < [filteredResult count] ; i++ ) {
@@ -1589,21 +1643,22 @@
numRows = [self getNumberOfRows];
if ( !areShowingAllRows ) {
// queryString = [@"SELECT * FROM " stringByAppendingString:selectedTable];
- queryString = [NSString stringWithFormat:@"SELECT %@ FROM `%@`", [self fieldListForQuery], selectedTable];
+ queryString = [NSString stringWithFormat:@"SELECT %@ FROM %@", [self fieldListForQuery], [selectedTable backtickQuotedString]];
if ( sortField ) {
- // queryString = [queryString stringByAppendingString:[NSString stringWithFormat:@" ORDER BY `%@`", sortField]];
- queryString = [NSString stringWithFormat:@"%@ ORDER BY `%@`", queryString, sortField];
+ // queryString = [queryString stringByAppendingString:[NSString stringWithFormat:@" ORDER BY %@", [sortField backtickQuotedString]]];
+ queryString = [NSString stringWithFormat:@"%@ ORDER BY %@", queryString, [sortField backtickQuotedString]];
if ( isDesc )
queryString = [queryString stringByAppendingString:@" DESC"];
}
- if ( [prefs boolForKey:@"limitRows"] ) {
+ if ( [prefs boolForKey:@"LimitResults"] ) {
if ( [limitRowsField intValue] <= 0 ) {
[limitRowsField setStringValue:@"1"];
}
queryString = [queryString stringByAppendingString:
[NSString stringWithFormat:@" LIMIT %d,%d",
- [limitRowsField intValue]-1, [prefs integerForKey:@"limitRowsValue"]]];
+ [limitRowsField intValue]-1, [prefs integerForKey:@"LimitResultsValue"]]];
}
+
queryResult = [mySQLConnection queryString:queryString];
// [fullResult setArray:[[self fetchResultAsArray:queryResult] retain]];
[fullResult setArray:[self fetchResultAsArray:queryResult]];
@@ -1627,7 +1682,7 @@
*/
- (int)getNumberOfRows
{
- if ([prefs boolForKey:@"limitRows"] && [prefs boolForKey:@"fetchRowCount"]) {
+ if ([prefs boolForKey:@"LimitResults"] && [prefs boolForKey:@"FetchCorrectRowCount"]) {
numRows = [self fetchNumberOfRows];
} else {
numRows = [fullResult count];
@@ -1641,7 +1696,7 @@
*/
- (int)fetchNumberOfRows
{
- return [[[[mySQLConnection queryString:[NSString stringWithFormat:@"SELECT COUNT(1) FROM `%@`", selectedTable]] fetchRowAsArray] objectAtIndex:0] intValue];
+ return [[[[mySQLConnection queryString:[NSString stringWithFormat:@"SELECT COUNT(1) FROM %@", [selectedTable backtickQuotedString]]] fetchRowAsArray] objectAtIndex:0] intValue];
}
//tableView datasource methods
@@ -1655,10 +1710,10 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn
row:(int)rowIndex
{
id theRow, theValue;
-
+
theRow = [filteredResult objectAtIndex:rowIndex];
theValue = [theRow objectForKey:[aTableColumn identifier]];
-
+
// Convert data objects to their string representation in the current encoding, falling back to ascii
if ( [theValue isKindOfClass:[NSData class]] ) {
NSString *dataRepresentation = [[NSString alloc] initWithData:theValue encoding:[mySQLConnection encoding]];
@@ -1668,8 +1723,59 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn
else theValue = [NSString stringWithString:dataRepresentation];
if (dataRepresentation) [dataRepresentation release];
}
-
- return theValue;
+ return theValue;
+}
+
+- (void)tableView: (CMCopyTable *)aTableView
+ willDisplayCell: (id)cell
+ forTableColumn: (NSTableColumn*)aTableColumn
+ row: (int)row
+/*
+ * This function changes the text color of
+ * text/blob fields which are not yet loaded to gray
+ */
+{
+ // Check if loading of text/blob fields is disabled
+ // If not, all text fields are loaded and we don't have to make them gray
+ if ([prefs boolForKey:@"LoadBlobsAsNeeded"])
+ {
+ // Make sure that the cell actually responds to setTextColor:
+ // In the future, we might use different cells for the table view
+ // that don't support this selector
+ if ([cell respondsToSelector:@selector(setTextColor:)])
+ {
+ NSArray *columns = [tableDataInstance columns];
+ NSArray *columnNames = [tableDataInstance columnNames];
+ NSString *columnTypeGrouping;
+ int indexOfColumn;
+
+ // We have to find the index of the current column
+ // Make sure we find it, otherwise return (We might decide in the future
+ // to add a column to the TableView that doesn't correspond to a column
+ // of the Mysql table...)
+ indexOfColumn = [columnNames indexOfObject:[aTableColumn identifier]];
+ if (indexOfColumn == NSNotFound) return;
+
+ // Test if the current column is a text or a blob field
+ columnTypeGrouping = [[columns objectAtIndex:indexOfColumn] objectForKey:@"typegrouping"];
+ if ([columnTypeGrouping isEqualToString:@"textdata"] || [columnTypeGrouping isEqualToString:@"blobdata"]) {
+
+ // now check if the field has been loaded already or not
+ if ([[cell stringValue] isEqualToString:NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields")])
+ {
+ // change the text color of the cell to gray
+ [cell setTextColor: [NSColor grayColor]];
+ }
+ else
+ {
+ // Change the text color back to black
+ // This is necessary because NSTableView reuses
+ // the NSCell to draw further rows in the column
+ [cell setTextColor: [NSColor blackColor]];
+ }
+ }
+ }
+ }
}
- (void)tableView:(NSTableView *)aTableView
@@ -1724,20 +1830,21 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn
isDesc = NO;
[tableContentView setIndicatorImage:nil inTableColumn:[tableContentView tableColumnWithIdentifier:sortField]];
}
- sortField = [tableColumn identifier];
+ if (sortField) [sortField release];
+ sortField = [[NSString alloc] initWithString:[tableColumn identifier]];
//make queryString and perform query
- queryString = [NSString stringWithFormat:@"SELECT %@ FROM `%@` ORDER BY `%@`", [self fieldListForQuery],
- selectedTable, sortField];
+ queryString = [NSString stringWithFormat:@"SELECT %@ FROM %@ ORDER BY %@", [self fieldListForQuery],
+ [selectedTable backtickQuotedString], [sortField backtickQuotedString]];
if ( isDesc )
queryString = [queryString stringByAppendingString:@" DESC"];
- if ( [prefs boolForKey:@"limitRows"] ) {
+ if ( [prefs boolForKey:@"LimitResults"] ) {
if ( [limitRowsField intValue] <= 0 ) {
[limitRowsField setStringValue:@"1"];
}
queryString = [queryString stringByAppendingString:
[NSString stringWithFormat:@" LIMIT %d,%d",
- [limitRowsField intValue]-1, [prefs integerForKey:@"limitRowsValue"]]];
+ [limitRowsField intValue]-1, [prefs integerForKey:@"LimitResultsValue"]]];
}
queryResult = [mySQLConnection queryString:queryString];
@@ -1780,9 +1887,14 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn
if ( isEditingRow && [tableContentView selectedRow] != currentlyEditingRow && ![self saveRowOnDeselect] ) return;
// Update the row selection count
+ // and update the status of the delete/duplicate buttons
if ( [tableContentView numberOfSelectedRows] > 0 ) {
+ [copyButton setEnabled:YES];
+ [removeButton setEnabled:YES];
[countText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%d of %d rows selected", @"Text showing how many rows are selected"), [tableContentView numberOfSelectedRows], [tableContentView numberOfRows]]];
} else {
+ [copyButton setEnabled:NO];
+ [removeButton setEnabled:NO];
[countText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%d rows", @"Text showing how many rows are in the result"), [tableContentView numberOfRows]]];
}
}
@@ -1824,13 +1936,15 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn
if ( [tableColumnWidths objectForKey:database] == nil ) {
[tableColumnWidths setObject:[NSMutableDictionary dictionary] forKey:database];
} else {
- [tableColumnWidths setObject:[[tableColumnWidths objectForKey:database] mutableCopy] forKey:database];
+ [tableColumnWidths setObject:[NSMutableDictionary dictionaryWithDictionary:[tableColumnWidths objectForKey:database]] forKey:database];
+
}
// get table object
if ( [[tableColumnWidths objectForKey:database] objectForKey:table] == nil ) {
[[tableColumnWidths objectForKey:database] setObject:[NSMutableDictionary dictionary] forKey:table];
} else {
- [[tableColumnWidths objectForKey:database] setObject:[[[tableColumnWidths objectForKey:database] objectForKey:table] mutableCopy] forKey:table];
+ [[tableColumnWidths objectForKey:database] setObject:[NSMutableDictionary dictionaryWithDictionary:[[tableColumnWidths objectForKey:database] objectForKey:table]] forKey:table];
+
}
// save column size
[[[tableColumnWidths objectForKey:database] objectForKey:table] setObject:[NSNumber numberWithFloat:[[[aNotification userInfo] objectForKey:@"NSTableColumn"] width]] forKey:[[[aNotification userInfo] objectForKey:@"NSTableColumn"] identifier]];
@@ -1844,21 +1958,23 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn
- (BOOL)tableView:(NSTableView *)aTableView shouldEditTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex
{
int code;
- NSString *columnTypeGrouping, *query, *stringValue = nil;
+ NSString *query, *stringValue = nil, *wherePart = nil;
+
NSEnumerator *enumerator;
NSDictionary *tempRow;
NSMutableDictionary *modifiedRow = [NSMutableDictionary dictionary];
id key, theValue;
CMMCPResult *tempResult;
- BOOL columnIsBlobOrText = NO;
// If not isEditingRow and the preference value for not showing blobs is set, check whether the row contains any blobs.
- if ( [prefs boolForKey:@"dontShowBlob"] && !isEditingRow ) {
+ if ( [prefs boolForKey:@"LoadBlobsAsNeeded"] && !isEditingRow ) {
// If the table does contain blob or text fields, load the values ready for editing.
if ( [self tableContainsBlobOrTextColumns] ) {
- query = [NSString stringWithFormat:@"SELECT * FROM `%@` WHERE %@",
- selectedTable, [self argumentForRow:[tableContentView selectedRow]]];
+ wherePart = [NSString stringWithString:[self argumentForRow:[tableContentView selectedRow]]];
+ if([wherePart length]==0)
+ return NO;
+ query = [NSString stringWithFormat:@"SELECT * FROM %@ WHERE %@", [selectedTable backtickQuotedString], wherePart];
tempResult = [mySQLConnection queryString:query];
if ( ![tempResult numOfRows] ) {
NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil,
@@ -1869,7 +1985,7 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn
enumerator = [tempRow keyEnumerator];
while ( key = [enumerator nextObject] ) {
if ( [[tempRow objectForKey:key] isMemberOfClass:[NSNull class]] ) {
- [modifiedRow setObject:[prefs stringForKey:@"nullValue"] forKey:key];
+ [modifiedRow setObject:[prefs stringForKey:@"NullValue"] forKey:key];
} else {
[modifiedRow setObject:[tempRow objectForKey:key] forKey:key];
}
@@ -1879,14 +1995,8 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn
}
}
- // If the selected column is a blob/text type, force sheet editing.
- columnTypeGrouping = [[tableDataInstance columnWithName:[aTableColumn identifier]] objectForKey:@"typegrouping"];
- if ([columnTypeGrouping isEqualToString:@"textdata"] || [columnTypeGrouping isEqualToString:@"blobdata"]) {
- columnIsBlobOrText = YES;
- }
-
// Open the sheet if the multipleLineEditingButton is enabled or the column was a blob or a text.
- if ( [multipleLineEditingButton state] == NSOnState || columnIsBlobOrText ) {
+ if ( [multipleLineEditingButton state] == NSOnState || [tableDataInstance columnIsBlobOrText:[aTableColumn identifier]] ) {
theValue = [[filteredResult objectAtIndex:rowIndex] objectForKey:[aTableColumn identifier]];
NSImage *image = nil;
editData = [theValue retain];
@@ -2070,16 +2180,14 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn
//last but not least
- (void)dealloc
-{
- // NSLog(@"TableContent dealloc");
-
+{
[editData release];
[fullResult release];
[filteredResult release];
[keys release];
[oldRow release];
[compareType release];
- [sortField release];
+ if (sortField) [sortField release];
[prefs release];
[super dealloc];
diff --git a/Source/TableDocument.h b/Source/TableDocument.h
index 7227f0dd..a0c099f9 100644
--- a/Source/TableDocument.h
+++ b/Source/TableDocument.h
@@ -25,8 +25,8 @@
#import <Cocoa/Cocoa.h>
#import <MCPKit_bundled/MCPKit_bundled.h>
-#import "CMMCPConnection.h"
-#import "CMMCPResult.h"
+
+@class CMMCPConnection, CMMCPResult;
/**
* The TableDocument class controls the primary database view window.
@@ -42,7 +42,7 @@
IBOutlet id tableDumpInstance;
IBOutlet id tableDataInstance;
IBOutlet id tableStatusInstance;
- IBOutlet id queryConsoleInstance;
+ IBOutlet id spExportControllerInstance;
IBOutlet id tableWindow;
IBOutlet id connectSheet;
@@ -53,6 +53,7 @@
IBOutlet id favoritesButton;
IBOutlet NSTableView *connectFavoritesTableView;
IBOutlet NSArrayController *favoritesController;
+ IBOutlet id nameField;
IBOutlet id hostField;
IBOutlet id socketField;
IBOutlet id userField;
@@ -61,7 +62,7 @@
IBOutlet id databaseField;
IBOutlet id connectProgressBar;
- IBOutlet id connectProgressStatusText;
+ IBOutlet NSTextField *connectProgressStatusText;
IBOutlet id databaseNameField;
IBOutlet id databaseEncodingButton;
IBOutlet id addDatabaseButton;
@@ -71,6 +72,8 @@
IBOutlet id sidebarGrabber;
+ IBOutlet NSTextView *customQueryTextView;
+
IBOutlet NSTableView *dbTablesTableView;
IBOutlet id syntaxView;
@@ -79,7 +82,6 @@
CMMCPConnection *mySQLConnection;
- NSMutableArray *favorites;
NSArray *variables;
NSString *selectedDatabase;
NSString *mySQLVersion;
@@ -88,22 +90,25 @@
NSMenu *selectEncodingMenu;
BOOL _supportsEncoding;
NSString *_encoding;
+ BOOL _encodingViaLatin1;
+ BOOL _shouldOpenConnectionAutomatically;
NSToolbar *mainToolbar;
NSToolbarItem *chooseDatabaseToolbarItem;
}
//start sheet
+- (void)setShouldAutomaticallyConnect:(BOOL)shouldAutomaticallyConnect;
- (IBAction)connectToDB:(id)sender;
- (IBAction)connect:(id)sender;
- (IBAction)cancelConnectSheet:(id)sender;
- (IBAction)closeSheet:(id)sender;
- (IBAction)chooseFavorite:(id)sender;
-- (IBAction)removeFavorite:(id)sender;
+- (IBAction)editFavorites:(id)sender;
- (id)selectedFavorite;
- (NSString *)selectedFavoritePassword;
- (void)connectSheetAddToFavorites:(id)sender;
-- (void)addToFavoritesHost:(NSString *)host socket:(NSString *)socket
+- (void)addToFavoritesName:(NSString *)name host:(NSString *)host socket:(NSString *)socket
user:(NSString *)user password:(NSString *)password
port:(NSString *)port database:(NSString *)database
useSSH:(BOOL)useSSH // no-longer in use
@@ -111,7 +116,6 @@
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;
@@ -130,6 +134,7 @@
- (void)setConnectionEncoding:(NSString *)mysqlEncoding reloadingViews:(BOOL)reloadViews;
- (NSString *)databaseEncoding;
- (NSString *)connectionEncoding;
+- (BOOL)connectionEncodingViaLatin1;
- (IBAction)chooseEncoding:(id)sender;
- (BOOL)supportsEncoding;
- (void)updateEncodingMenuWithSelectedEncoding:(NSString *)encoding;
@@ -154,6 +159,7 @@
- (void)closeConnection;
//getter methods
+- (NSString *)name;
- (NSString *)database;
- (NSString *)table;
- (NSString *)mySQLVersion;
@@ -175,6 +181,7 @@
- (IBAction)viewContent:(id)sender;
- (IBAction)viewQuery:(id)sender;
- (IBAction)viewStatus:(id)sender;
+- (IBAction)addConnectionToFavorites:(id)sender;
//toolbar methods
- (void)setupToolbar;
@@ -184,34 +191,10 @@
- (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;
-// Connection sheet delegate methods
-- (void) controlTextDidChange:(NSNotification *)aNotification;
-
-//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;
diff --git a/Source/TableDocument.m b/Source/TableDocument.m
index 6d936800..b6cab9e4 100644
--- a/Source/TableDocument.m
+++ b/Source/TableDocument.m
@@ -33,24 +33,35 @@
#import "TableStatus.h"
#import "ImageAndTextCell.h"
#import "SPGrowlController.h"
+#import "SPExportController.h"
#import "SPQueryConsole.h"
#import "SPSQLParser.h"
#import "SPTableData.h"
+#import "SPStringAdditions.h"
+#import "SPQueryConsole.h"
+#import "CMMCPConnection.h"
+#import "CMMCPResult.h"
+#import "MainController.h"
+#import "SPPreferenceController.h"
NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocumentFavoritesControllerSelectionIndexDidChange";
-NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFavoritesControllerFavoritesDidChange";
+
+@interface TableDocument (PrivateAPI)
+
+- (BOOL)_favoriteAlreadyExists:(NSString *)database host:(NSString *)host user:(NSString *)user;
+
+@end
@implementation TableDocument
- (id)init
{
- if (![super init])
- return nil;
-
- _encoding = [@"utf8" retain];
- chooseDatabaseButton = nil;
- chooseDatabaseToolbarItem = nil;
-
+ if ((self = [super init])) {
+ _encoding = [@"utf8" retain];
+ chooseDatabaseButton = nil;
+ chooseDatabaseToolbarItem = nil;
+ }
+
return self;
}
@@ -59,11 +70,9 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
// 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];
+ [connectFavoritesTableView setDoubleAction:@selector(connect:)];
// 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];
@@ -78,40 +87,42 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
[self chooseFavorite:self];
return;
}
-
- if (context == TableDocumentFavoritesControllerFavoritesDidChange) {
- [prefs setObject:[self favorites] forKey:@"favorites"];
- return;
- }
-
+
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
- (NSPrintOperation *)printOperationWithSettings:(NSDictionary *)ps error:(NSError **)e
-{
-
+{
NSPrintInfo *printInfo = [self printInfo];
+ [printInfo setHorizontalPagination:NSFitPagination];
+ [printInfo setVerticalPagination:NSAutoPagination];
NSPrintOperation *printOp = [NSPrintOperation printOperationWithView:[[tableTabView selectedTabViewItem] view] printInfo:printInfo];
return printOp;
}
-
- (CMMCPConnection *)sharedConnection
{
return mySQLConnection;
}
-
//start sheet
/**
+ * Set whether the connection sheet should automaticall start connecting
+ */
+- (void)setShouldAutomaticallyConnect:(BOOL)shouldAutomaticallyConnect
+{
+ _shouldOpenConnectionAutomatically = shouldAutomaticallyConnect;
+}
+
+/**
* 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
+ // load the details of the currently selected favorite into the text boxes in connect sheet
[self chooseFavorite:self];
// run the connect sheet (modal)
@@ -120,6 +131,13 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
modalDelegate:self
didEndSelector:@selector(connectSheetDidEnd:returnCode:contextInfo:)
contextInfo:nil];
+
+ // Connect automatically to the last used or default favourite
+ // connectSheet must open first.
+ if (_shouldOpenConnectionAutomatically) {
+ _shouldOpenConnectionAutomatically = false;
+ [self connect:self];
+ }
}
@@ -193,7 +211,7 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
//register as delegate
[mySQLConnection setDelegate:self];
// set encoding
- NSString *encodingName = [prefs objectForKey:@"encoding"];
+ NSString *encodingName = [prefs objectForKey:@"DefaultEncoding"];
if ( [encodingName isEqualToString:@"Autodetect"] ) {
[self setConnectionEncoding:[self databaseEncoding] reloadingViews:NO];
} else {
@@ -214,12 +232,12 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
[tableContentInstance setConnection:mySQLConnection];
[customQueryInstance setConnection:mySQLConnection];
[tableDumpInstance setConnection:mySQLConnection];
+ [spExportControllerInstance setConnection:mySQLConnection];
[tableStatusInstance setConnection:mySQLConnection];
[tableDataInstance 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]]];
+ [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@/%@", mySQLVersion, [self name], [databaseField stringValue]]];
// Connected Growl notification
[[SPGrowlController sharedGrowlController] notifyWithTitle:@"Connected"
@@ -230,12 +248,12 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
//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]]);
+ [NSString stringWithFormat:NSLocalizedString(@"Unable to connect to host %@, or the request timed out.\n\nBe sure that the address is correct and that you have the necessary privileges, or try increasing the connection timeout (currently %i seconds).\n\nMySQL said: %@", @"message of panel when connection to host failed"), [hostField stringValue], [[prefs objectForKey:@"ConnectionTimeoutValue"] intValue], [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]]);
+ [NSString stringWithFormat:NSLocalizedString(@"Connected to host, but unable to connect to database %@.\n\nBe sure that the database exists and that you have the necessary privileges.\n\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,
@@ -250,12 +268,12 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
[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
+/**
+ * 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
*/
+- (IBAction)closeSheet:(id)sender
{
[NSApp stopModalWithCode:0];
}
@@ -268,55 +286,28 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
if (![self selectedFavorite])
return;
+ [nameField setStringValue:[self valueForKeyPath:@"selectedFavorite.name"]];
[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]];
-
+ [prefs setInteger:[favoritesController selectionIndex] forKey:@"LastFavoriteIndex"];
}
/**
- * Return the favorites array.
+ * Opens the preferences window, or brings it to the front, and switch to the favorites tab.
+ * If a favorite is selected in the connection sheet, it is also select in the prefs window.
*/
-- (NSMutableArray *)favorites
+- (IBAction)editFavorites:(id)sender
{
- // 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];
- }
+ SPPreferenceController *prefsController = [[NSApp delegate] preferenceController];
- return favorites;
+ [prefsController showWindow:self];
+ [prefsController displayFavoritePreferences:self];
+ [prefsController selectFavorites:[favoritesController selectedObjects]];
}
/**
@@ -340,7 +331,7 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
if (![self selectedFavorite])
return nil;
- NSString *keychainName = [NSString stringWithFormat:@"Sequel Pro : %@", [self valueForKeyPath:@"selectedFavorite.name"]];
+ NSString *keychainName = [NSString stringWithFormat:@"Sequel Pro : %@ (%i)", [self valueForKeyPath:@"selectedFavorite.name"], [[self valueForKeyPath:@"selectedFavorite.id"] intValue]];
NSString *keychainAccount = [NSString stringWithFormat:@"%@@%@/%@",
[self valueForKeyPath:@"selectedFavorite.user"],
[self valueForKeyPath:@"selectedFavorite.host"],
@@ -351,23 +342,24 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
- (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:@""];
+ [self addToFavoritesName:[nameField stringValue] host:[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:@""])
+- (void)addToFavoritesName:(NSString *)name host:(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 = [name length]?name:[NSString stringWithFormat:@"%@@%@", user, host];
+ NSNumber *favoriteid = [NSNumber numberWithInt:[[NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]] hash]];
+ if (![name length] && ![database isEqualToString:@""])
favoriteName = [NSString stringWithFormat:@"%@ %@", database, favoriteName];
// test if host and socket are not nil
@@ -376,21 +368,18 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
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];
-
+ NSMutableDictionary *newFavorite = [NSMutableDictionary dictionaryWithObjects:[NSArray arrayWithObjects:favoriteName, host, socket, user, port, database, favoriteid, nil]
+ forKeys:[NSArray arrayWithObjects:@"name", @"host", @"socket", @"user", @"port", @"database", @"id", nil]];
if (![password isEqualToString:@""]) {
[keyChainInstance addPassword:password
- forName:[NSString stringWithFormat:@"Sequel Pro : %@", favoriteName]
+ forName:[NSString stringWithFormat:@"Sequel Pro : %@ (%i)", favoriteName, [favoriteid intValue]]
account:[NSString stringWithFormat:@"%@@%@/%@", user, host, database]];
}
- [self didChangeValueForKey:@"favorites"];
+ [favoritesController addObject:newFavorite];
[favoritesController setSelectedObjects:[NSArray arrayWithObject:newFavorite]];
+ [[[NSApp delegate] preferenceController] updateDefaultFavoritePopup];
}
/**
@@ -400,10 +389,9 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
* 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"]) {
+ [sheet orderOut:self];
[self connectToDB:nil];
return;
}
@@ -412,7 +400,7 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
if (returnCode != NSAlertDefaultReturn)
return;
- [mySQLConnection queryString:[NSString stringWithFormat:@"DROP DATABASE `%@`", [self database]]];
+ [mySQLConnection queryString:[NSString stringWithFormat:@"DROP DATABASE %@", [[self database] backtickQuotedString]]];
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]]);
@@ -424,7 +412,7 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
[self setDatabases:self];
[tablesListInstance setConnection:mySQLConnection];
[tableDumpInstance setConnection:mySQLConnection];
- [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@@%@/", mySQLVersion, [userField stringValue], [hostField stringValue]]];
+ [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@/", mySQLVersion, [self name]]];
}
}
@@ -496,7 +484,7 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
selectedDatabase = [[chooseDatabaseButton titleOfSelectedItem] retain];
[tablesListInstance setConnection:mySQLConnection];
[tableDumpInstance setConnection:mySQLConnection];
- [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@@%@/%@", mySQLVersion, [userField stringValue], [hostField stringValue], [self database]]];
+ [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@/%@", mySQLVersion, [self name], [self database]]];
}
/**
@@ -535,11 +523,11 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
return;
}
- NSString *createStatement = [NSString stringWithFormat:@"CREATE DATABASE `%@`", [databaseNameField stringValue]];
+ NSString *createStatement = [NSString stringWithFormat:@"CREATE DATABASE %@", [[databaseNameField stringValue] backtickQuotedString]];
// If there is an encoding selected other than the default we must specify it in CREATE DATABASE statement
if ([databaseEncodingButton indexOfSelectedItem] > 0) {
- createStatement = [NSString stringWithFormat:@"%@ DEFAULT CHARACTER SET `%@`", createStatement, [self mysqlEncodingFromDisplayEncoding:[databaseEncodingButton title]]];
+ createStatement = [NSString stringWithFormat:@"%@ DEFAULT CHARACTER SET %@", createStatement, [[self mysqlEncodingFromDisplayEncoding:[databaseEncodingButton title]] backtickQuotedString]];
}
// Create the database
@@ -565,7 +553,7 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
[self setDatabases:self];
[tablesListInstance setConnection:mySQLConnection];
[tableDumpInstance setConnection:mySQLConnection];
- [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@@%@/%@", mySQLVersion, [userField stringValue], [hostField stringValue], selectedDatabase]];
+ [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@/%@", mySQLVersion, [self name], selectedDatabase]];
}
/**
@@ -583,10 +571,19 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
{
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]]);
+ NSAlert *alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:NSLocalizedString(@"Delete database '%@'?", @"delete database message"), [self database]]
+ defaultButton:NSLocalizedString(@"Delete", @"delete button")
+ alternateButton:NSLocalizedString(@"Cancel", @"cancel button")
+ otherButton:nil
+ informativeTextWithFormat:[NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to delete the database '%@'. This operation cannot be undone.", @"delete database informative message"), [self database]]];
+
+ [alert setAlertStyle:NSCriticalAlertStyle];
+
+ [alert beginSheetModalForWindow:tableWindow modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:@"removedatabase"];
}
#pragma mark Console methods
@@ -596,7 +593,25 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
*/
- (void)toggleConsole:(id)sender
{
- [[queryConsoleInstance window] setIsVisible:![[queryConsoleInstance window] isVisible]];
+ BOOL isConsoleVisible = [[[SPQueryConsole sharedQueryConsole] window] isVisible];
+
+ // Show or hide the console
+ [[[SPQueryConsole sharedQueryConsole] window] setIsVisible:(!isConsoleVisible)];
+
+ // Get the menu item for showing and hiding the console. This is isn't the best way to get it as any
+ // changes to the menu structure will result in the wrong item being selected.
+ NSMenuItem *menuItem = [[[[NSApp mainMenu] itemAtIndex:3] submenu] itemAtIndex:5];
+
+ // Only update the menu item title if its the menu item and not the toolbar
+ [menuItem setTitle:(!isConsoleVisible) ? NSLocalizedString(@"Hide Console", @"Hide Console") : NSLocalizedString(@"Show Console", @"Show Console")];
+}
+
+/**
+ * Clears the console by removing all of its messages
+ */
+- (void)clearConsole:(id)sender
+{
+ [[SPQueryConsole sharedQueryConsole] clearConsole:sender];
}
#pragma mark Encoding Methods
@@ -606,11 +621,11 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
*/
- (void)setConnectionEncoding:(NSString *)mysqlEncoding reloadingViews:(BOOL)reloadViews
{
- BOOL uselatin1results = NO;
+ _encodingViaLatin1 = NO;
// Special-case UTF-8 over latin 1 to allow viewing/editing of mangled data.
if ([mysqlEncoding isEqualToString:@"utf8-"]) {
- uselatin1results = YES;
+ _encodingViaLatin1 = YES;
mysqlEncoding = @"utf8";
}
@@ -618,13 +633,14 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
[mySQLConnection queryString:[NSString stringWithFormat:@"SET NAMES '%@'", mysqlEncoding]];
if ( [[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) {
- if (uselatin1results)
+ if (_encodingViaLatin1)
[mySQLConnection queryString:@"SET CHARACTER_SET_RESULTS=latin1"];
[mySQLConnection setEncoding:[CMMCPConnection encodingForMySQLEncoding:[mysqlEncoding UTF8String]]];
[_encoding autorelease];
_encoding = [mysqlEncoding retain];
} else {
[mySQLConnection queryString:[NSString stringWithFormat:@"SET NAMES '%@'", [self databaseEncoding]]];
+ _encodingViaLatin1 = NO;
if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) {
NSLog(@"Error: could not set encoding to %@ nor fall back to database encoding on MySQL %@", mysqlEncoding, [self mySQLVersion]);
return;
@@ -632,7 +648,7 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
}
// update the selected menu item
- if (uselatin1results) {
+ if (_encodingViaLatin1) {
[self updateEncodingMenuWithSelectedEncoding:[self encodingNameFromMySQLEncoding:[NSString stringWithFormat:@"%@-", mysqlEncoding]]];
} else {
[self updateEncodingMenuWithSelectedEncoding:[self encodingNameFromMySQLEncoding:mysqlEncoding]];
@@ -656,6 +672,14 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
}
/**
+ * Returns whether the current encoding should display results via Latin1 transport for backwards compatibility
+ */
+- (BOOL)connectionEncodingViaLatin1
+{
+ return _encodingViaLatin1;
+}
+
+/**
* updates the currently selected item in the encoding menu
*
* @param NSString *encoding - the title of the menu item which will be selected
@@ -781,18 +805,22 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
return _supportsEncoding;
}
-
#pragma mark Table Methods
+/**
+ * Displays the CREATE TABLE syntax of the selected table to the user via a HUD panel.
+ */
- (IBAction)showCreateTableSyntax:(id)sender
{
//Create the query and get results
- NSString *query = [NSString stringWithFormat:@"SHOW CREATE TABLE `%@`", [self table]];
+ NSString *query = [NSString stringWithFormat:@"SHOW CREATE TABLE %@", [[self table] backtickQuotedString]];
CMMCPResult *theResult = [mySQLConnection queryString:query];
- // Check for errors
+ // Check for errors, only displaying if the connection hasn't been terminated
if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) {
- NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while creating table syntax.\n\n: %@",[mySQLConnection getLastErrorMessage]], @"OK", nil, nil);
+ if ([mySQLConnection isConnected]) {
+ NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while creating table syntax.\n\n: %@",[mySQLConnection getLastErrorMessage]], @"OK", nil, nil);
+ }
return;
}
@@ -805,15 +833,20 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
[createTableSyntaxWindow makeKeyAndOrderFront:self];
}
+/**
+ * Copies the CREATE TABLE syntax of the selected table to the pasteboard.
+ */
- (IBAction)copyCreateTableSyntax:(id)sender
{
// Create the query and get results
- NSString *query = [NSString stringWithFormat:@"SHOW CREATE TABLE `%@`", [self table]];
+ NSString *query = [NSString stringWithFormat:@"SHOW CREATE TABLE %@", [[self table] backtickQuotedString]];
CMMCPResult *theResult = [mySQLConnection queryString:query];
- // Check for errors
+ // Check for errors, only displaying if the connection hasn't been terminated
if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) {
- NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while creating table syntax.\n\n: %@",[mySQLConnection getLastErrorMessage]], @"OK", nil, nil);
+ if ([mySQLConnection isConnected]) {
+ NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while creating table syntax.\n\n: %@",[mySQLConnection getLastErrorMessage]], @"OK", nil, nil);
+ }
return;
}
@@ -833,131 +866,264 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
notificationName:@"Table Syntax Copied"];
}
+/**
+ * Performs a MySQL check table on the selected table and presents the result to the user via an alert sheet.
+ */
- (IBAction)checkTable:(id)sender
-{
- NSString *query;
- CMMCPResult *theResult;
- NSDictionary *theRow;
+{
+ CMMCPResult *theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"CHECK TABLE %@", [[self table] backtickQuotedString]]];
- //Create the query and get results
- query = [NSString stringWithFormat:@"CHECK TABLE `%@`", [self table]];
- theResult = [mySQLConnection queryString:query];
-
- // Check for errors
+ // Check for errors, only displaying if the connection hasn't been terminated
if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) {
- NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while checking table.\n\n: %@",[mySQLConnection getLastErrorMessage]], @"OK", nil, nil);
+ if ([mySQLConnection isConnected]) {
+
+ [[NSAlert alertWithMessageText:@"Unable to check table"
+ defaultButton:@"OK"
+ alternateButton:nil
+ otherButton:nil
+ informativeTextWithFormat:[NSString stringWithFormat:@"An error occurred while trying to check the table '%@'. Please try again.\n\n%@", [self table], [mySQLConnection getLastErrorMessage]]]
+ beginSheetModalForWindow:tableWindow
+ modalDelegate:self
+ didEndSelector:NULL
+ contextInfo:NULL];
+ }
+
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);
+ NSDictionary *result = [[theResult fetch2DResultAsType:MCPTypeDictionary] lastObject];
+
+ NSString *message = @"";
+
+ message = ([[result objectForKey:@"Msg_type"] isEqualToString:@"status"]) ? @"Check table successfully passed." : @"Check table failed.";
+
+ message = [NSString stringWithFormat:@"%@\n\nMySQL said: %@", message, [result objectForKey:@"Msg_text"]];
+
+ [[NSAlert alertWithMessageText:[NSString stringWithFormat:@"Check table '%@'", [self table]]
+ defaultButton:@"OK"
+ alternateButton:nil
+ otherButton:nil
+ informativeTextWithFormat:message]
+ beginSheetModalForWindow:tableWindow
+ modalDelegate:self
+ didEndSelector:NULL
+ contextInfo:NULL];
}
+/**
+ * Analyzes the selected table and presents the result to the user via an alert sheet.
+ */
- (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];
+ CMMCPResult *theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"ANALYZE TABLE %@", [[self table] backtickQuotedString]]];
- // Check for errors
+ // Check for errors, only displaying if the connection hasn't been terminated
if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) {
- NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while analyzing table.\n\n: %@",[mySQLConnection getLastErrorMessage]], @"OK", nil, nil);
+ if ([mySQLConnection isConnected]) {
+
+ [[NSAlert alertWithMessageText:@"Unable to analyze table"
+ defaultButton:@"OK"
+ alternateButton:nil
+ otherButton:nil
+ informativeTextWithFormat:[NSString stringWithFormat:@"An error occurred while trying to analyze the table '%@'. Please try again.\n\n%@", [self table], [mySQLConnection getLastErrorMessage]]]
+ beginSheetModalForWindow:tableWindow
+ modalDelegate:self
+ didEndSelector:NULL
+ contextInfo:NULL];
+ }
+
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);
+ NSDictionary *result = [[theResult fetch2DResultAsType:MCPTypeDictionary] lastObject];
+
+ NSString *message = @"";
+
+ message = ([[result objectForKey:@"Msg_type"] isEqualToString:@"status"]) ? @"Successfully analyzed table" : @"Analyze table failed.";
+
+ message = [NSString stringWithFormat:@"%@\n\nMySQL said: %@", message, [result objectForKey:@"Msg_text"]];
+
+ [[NSAlert alertWithMessageText:[NSString stringWithFormat:@"Analyze table '%@'", [self table]]
+ defaultButton:@"OK"
+ alternateButton:nil
+ otherButton:nil
+ informativeTextWithFormat:message]
+ beginSheetModalForWindow:tableWindow
+ modalDelegate:self
+ didEndSelector:NULL
+ contextInfo:NULL];
}
+/**
+ * Optimizes the selected table and presents the result to the user via an alert sheet.
+ */
- (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];
+ CMMCPResult *theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"OPTIMIZE TABLE %@", [[self table] backtickQuotedString]]];
- // Check for errors
- if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) {
- NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while optimizing table.\n\n: %@",[mySQLConnection getLastErrorMessage]], @"OK", nil, nil);
+ // Check for errors, only displaying if the connection hasn't been terminated
+ if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) {
+ if ([mySQLConnection isConnected]) {
+
+ [[NSAlert alertWithMessageText:@"Unable to optimize table"
+ defaultButton:@"OK"
+ alternateButton:nil
+ otherButton:nil
+ informativeTextWithFormat:[NSString stringWithFormat:@"An error occurred while trying to optimize the table '%@'. Please try again.\n\n%@", [self table], [mySQLConnection getLastErrorMessage]]]
+ beginSheetModalForWindow:tableWindow
+ modalDelegate:self
+ didEndSelector:NULL
+ contextInfo:NULL];
+ }
+
+ return;
}
// Process result
- theRow = [[theResult fetch2DResultAsType:MCPTypeDictionary] lastObject];
- NSRunInformationalAlertPanel([NSString stringWithFormat:@"Optimize '%@' table", [self table]], [NSString stringWithFormat:@"Optimize: %@", [theRow objectForKey:@"Msg_text"]], @"OK", nil, nil);
+ NSDictionary *result = [[theResult fetch2DResultAsType:MCPTypeDictionary] lastObject];
+
+ NSString *message = @"";
+
+ message = ([[result objectForKey:@"Msg_type"] isEqualToString:@"status"]) ? @"Successfully optimized table" : @"Optimize table failed.";
+
+ message = [NSString stringWithFormat:@"%@\n\nMySQL said: %@", message, [result objectForKey:@"Msg_text"]];
+
+ [[NSAlert alertWithMessageText:[NSString stringWithFormat:@"Optimize table '%@'", [self table]]
+ defaultButton:@"OK"
+ alternateButton:nil
+ otherButton:nil
+ informativeTextWithFormat:message]
+ beginSheetModalForWindow:tableWindow
+ modalDelegate:self
+ didEndSelector:NULL
+ contextInfo:NULL];
}
+/**
+ * Repairs the selected table and presents the result to the user via an alert sheet.
+ */
- (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];
+ CMMCPResult *theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"REPAIR TABLE %@", [[self table] backtickQuotedString]]];
- // Check for errors
+ // Check for errors, only displaying if the connection hasn't been terminated
if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) {
- NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while repairing table.\n\n: %@",[mySQLConnection getLastErrorMessage]], @"OK", nil, nil);
+ if ([mySQLConnection isConnected]) {
+
+ [[NSAlert alertWithMessageText:@"Unable to repair table"
+ defaultButton:@"OK"
+ alternateButton:nil
+ otherButton:nil
+ informativeTextWithFormat:[NSString stringWithFormat:@"An error occurred while trying to repair the table '%@'. Please try again.\n\n%@", [self table], [mySQLConnection getLastErrorMessage]]]
+ beginSheetModalForWindow:tableWindow
+ modalDelegate:self
+ didEndSelector:NULL
+ contextInfo:NULL];
+ }
+
+ return;
}
// Process result
- theRow = [[theResult fetch2DResultAsType:MCPTypeDictionary] lastObject];
- NSRunInformationalAlertPanel([NSString stringWithFormat:@"Repair '%@' table", [self table]], [NSString stringWithFormat:@"Repair: %@", [theRow objectForKey:@"Msg_text"]], @"OK", nil, nil);
+ NSDictionary *result = [[theResult fetch2DResultAsType:MCPTypeDictionary] lastObject];
+
+ NSString *message = @"";
+
+ message = ([[result objectForKey:@"Msg_type"] isEqualToString:@"status"]) ? @"Successfully repaired table" : @"Repair table failed.";
+
+ message = [NSString stringWithFormat:@"%@\n\nMySQL said: %@", message, [result objectForKey:@"Msg_text"]];
+
+ [[NSAlert alertWithMessageText:[NSString stringWithFormat:@"Repair table '%@'", [self table]]
+ defaultButton:@"OK"
+ alternateButton:nil
+ otherButton:nil
+ informativeTextWithFormat:message]
+ beginSheetModalForWindow:tableWindow
+ modalDelegate:self
+ didEndSelector:NULL
+ contextInfo:NULL];
}
+/**
+ * Flush the selected table and inform the user via a dialog sheet.
+ */
- (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];
+ [mySQLConnection queryString:[NSString stringWithFormat:@"FLUSH TABLE %@", [[self table] backtickQuotedString]]];
- // Check for errors
+ // Check for errors, only displaying if the connection hasn't been terminated
if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) {
- NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while flushing table.\n\n: %@",[mySQLConnection getLastErrorMessage]], @"OK", nil, nil);
+ if ([mySQLConnection isConnected]) {
+
+ [[NSAlert alertWithMessageText:@"Unable to flush table"
+ defaultButton:@"OK"
+ alternateButton:nil
+ otherButton:nil
+ informativeTextWithFormat:[NSString stringWithFormat:@"An error occurred while trying to flush the table '%@'. Please try again.\n\n%@", [self table], [mySQLConnection getLastErrorMessage]]]
+ beginSheetModalForWindow:tableWindow
+ modalDelegate:self
+ didEndSelector:NULL
+ contextInfo:NULL];
+ }
+
return;
}
-
- // Process result
- NSRunInformationalAlertPanel([NSString stringWithFormat:@"Flush '%@' table", [self table]], @"Flushed", @"OK", nil, nil);
+
+ [[NSAlert alertWithMessageText:[NSString stringWithFormat:@"Flush table '%@'", [self table]]
+ defaultButton:@"OK"
+ alternateButton:nil
+ otherButton:nil
+ informativeTextWithFormat:@"Table was successfully flushed"]
+ beginSheetModalForWindow:tableWindow
+ modalDelegate:self
+ didEndSelector:NULL
+ contextInfo:NULL];
}
+/**
+ * Runs a MySQL checksum on the selected table and present the result to the user via an alert sheet.
+ */
- (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];
+{
+ CMMCPResult *theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"CHECKSUM TABLE %@", [[self table] backtickQuotedString]]];
- // Check for errors
+ // Check for errors, only displaying if the connection hasn't been terminated
if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) {
- NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while performming checksum on table.\n\n: %@",[mySQLConnection getLastErrorMessage]], @"OK", nil, nil);
+ if ([mySQLConnection isConnected]) {
+
+ [[NSAlert alertWithMessageText:@"Unable to perform checksum"
+ defaultButton:@"OK"
+ alternateButton:nil
+ otherButton:nil
+ informativeTextWithFormat:[NSString stringWithFormat:@"An error occurred while performing the checksum on table '%@'. Please try again.\n\n%@", [self table], [mySQLConnection getLastErrorMessage]]]
+ beginSheetModalForWindow:tableWindow
+ modalDelegate:self
+ didEndSelector:NULL
+ contextInfo:NULL];
+ }
+ return;
}
// Process result
- theRow = [[theResult fetch2DResultAsType:MCPTypeDictionary] lastObject];
- NSRunInformationalAlertPanel([NSString stringWithFormat:@"Checksum '%@' table", [self table]], [NSString stringWithFormat:@"Checksum: %@", [theRow objectForKey:@"Checksum"]], @"OK", nil, nil);
+ NSString *result = [[[theResult fetch2DResultAsType:MCPTypeDictionary] lastObject] objectForKey:@"Checksum"];
+
+ [[NSAlert alertWithMessageText:[NSString stringWithFormat:@"Checksum table '%@'", [self table]]
+ defaultButton:@"OK"
+ alternateButton:nil
+ otherButton:nil
+ informativeTextWithFormat:[NSString stringWithFormat:@"Table checksum: %@", result]]
+ beginSheetModalForWindow:tableWindow
+ modalDelegate:self
+ didEndSelector:NULL
+ contextInfo:NULL];
}
-
#pragma mark Other Methods
+
/**
- * returns the host
+ * Returns the host
*/
- (NSString *)host
{
@@ -965,7 +1131,18 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
}
/**
- * passes query to tablesListInstance
+ * Returns the name
+ */
+- (NSString *)name
+{
+ if ([[nameField stringValue] length]) {
+ return [nameField stringValue];
+ }
+ return [NSString stringWithFormat:@"%@@%@", [userField stringValue], [hostField stringValue]];
+}
+
+/**
+ * Passes query to tablesListInstance
*/
- (void)doPerformQueryService:(NSString *)query
{
@@ -974,7 +1151,7 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
}
/**
- * flushes the mysql privileges
+ * Flushes the mysql privileges
*/
- (void)flushPrivileges:(id)sender
{
@@ -990,10 +1167,10 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
}
}
-- (void)showVariables:(id)sender
-/*
- shows the mysql variables
+/**
+ * Shows the MySQL server variables
*/
+- (void)showVariables:(id)sender
{
CMMCPResult *theResult;
NSMutableArray *tempResult = [NSMutableArray array];
@@ -1031,48 +1208,48 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
notificationName:@"Disconnected"];
}
+// Getter methods
-//getter methods
-- (NSString *)database
-/*
- returns the currently selected database
+/**
+ * Returns the currently selected database
*/
+- (NSString *)database
{
return selectedDatabase;
}
-- (NSString *)table
-/*
- returns the currently selected table (passing the request to TablesList)
+/**
+ * Returns the currently selected table (passing the request to TablesList)
*/
+- (NSString *)table
{
return [tablesListInstance tableName];
}
-- (NSString *)mySQLVersion
-/*
- returns the mysql version
+/**
+ * Returns the MySQL version
*/
+- (NSString *)mySQLVersion
{
return mySQLVersion;
}
-- (NSString *)user
-/*
- returns the mysql version
+/**
+ * Returns the current user
*/
+- (NSString *)user
{
return [userField stringValue];
}
+// Notification center methods
-//notification center methods
-- (void)willPerformQuery:(NSNotification *)notification
-/*
- invoked before a query is performed
+/**
+ * Invoked before a query is performed
*/
+- (void)willPerformQuery:(NSNotification *)notification
{
- // Only start the progress indicator is this document window is key.
+ // Only start the progress indicator if 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:
@@ -1085,46 +1262,53 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
}
}
-- (void)hasPerformedQuery:(NSNotification *)notification
-/*
- invoked after a query has been performed
+/**
+ * Invoked after a query has been performed
*/
+- (void)hasPerformedQuery:(NSNotification *)notification
{
if ([tableWindow isKeyWindow]) {
[queryProgressBar stopAnimation:self];
}
}
-- (void)applicationWillTerminate:(NSNotification *)notification
-/*
- invoked when the application will terminate
+/**
+ * Invoked when the application will terminate
*/
+- (void)applicationWillTerminate:(NSNotification *)notification
{
[tablesListInstance selectionShouldChangeInTableView:nil];
}
-- (void)tunnelStatusChanged:(NSNotification *)notification
-/*
- the status of the tunnel has changed
+/**
+ * The status of the tunnel has changed
*/
+- (void)tunnelStatusChanged:(NSNotification *)notification
{
}
-//menu methods
-- (IBAction)import:(id)sender
-/*
- passes the request to the tableDump object
+// Menu methods
+
+/**
+ * Passes the request to the tableDump object
*/
+- (IBAction)import:(id)sender
{
[tableDumpInstance importFile];
}
-- (IBAction)export:(id)sender
-/*
- passes the request to the tableDump object
+/**
+ * Passes the request to the tableDump object
*/
+- (IBAction)export:(id)sender
{
- [tableDumpInstance exportFile:[sender tag]];
+ if ([sender tag] == -1) {
+ //[tableDumpInstance export];
+
+ [spExportControllerInstance export];
+ } else {
+ [tableDumpInstance exportFile:[sender tag]];
+ }
}
- (IBAction)exportTable:(id)sender
@@ -1141,7 +1325,7 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
* Menu validation
*/
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
-{
+{
if ([menuItem action] == @selector(import:) ||
[menuItem action] == @selector(export:) ||
[menuItem action] == @selector(exportMultipleTables:) ||
@@ -1172,6 +1356,10 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
return ([self table] != nil && [[self table] isNotEqualTo:@""]);
}
+ if ([menuItem action] == @selector(addConnectionToFavorites:)) {
+ return (![self _favoriteAlreadyExists:[self database] host:[self host] user:[self user]]);
+ }
+
return [super validateMenuItem:menuItem];
}
@@ -1219,6 +1407,9 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
[tableTabView selectTabViewItemAtIndex:2];
[mainToolbar setSelectedItemIdentifier:@"SwitchToRunQueryToolbarItemIdentifier"];
+
+ // Set the focus on the text field if no query has been run
+ if (![[customQueryTextView string] length]) [tableWindow makeFirstResponder:customQueryTextView];
}
- (IBAction)viewStatus:(id)sender
@@ -1241,6 +1432,21 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
[mainToolbar setSelectedItemIdentifier:@"SwitchToTableStatusToolbarItemIdentifier"];
}
+/**
+ * Adds the current database connection details to the user's favorites if it doesn't already exist.
+ */
+- (IBAction)addConnectionToFavorites:(id)sender
+{
+ // Obviously don't add if it already exists. We shouldn't really need this as the menu item validation
+ // enables or disables the menu item based on the same method. Although to be safe do the check anyway
+ // as we don't know what's calling this method.
+ if ([self _favoriteAlreadyExists:[self database] host:[self host] user:[self user]]) {
+ return;
+ }
+
+ // Add current connection to favorites using the same method as used on the connection sheet to provide consistency.
+ [self connectSheetAddToFavorites:self];
+}
#pragma mark Toolbar Methods
@@ -1253,8 +1459,8 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
mainToolbar = [[[NSToolbar alloc] initWithIdentifier:@"TableWindowToolbar"] autorelease];
// set up toolbar properties
- [mainToolbar setAllowsUserCustomization: YES];
- [mainToolbar setAutosavesConfiguration: YES];
+ [mainToolbar setAllowsUserCustomization:YES];
+ [mainToolbar setAutosavesConfiguration:YES];
[mainToolbar setDisplayMode:NSToolbarDisplayModeIconAndLabel];
// set ourself as the delegate
@@ -1297,11 +1503,11 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
//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 ([[queryConsoleInstance window] isVisible]) {
- [toolbarItem setLabel:NSLocalizedString(@"Hide Console", @"toolbar item for hide console")];
+ if ([[[SPQueryConsole sharedQueryConsole] window] isVisible]) {
+ [toolbarItem setLabel:NSLocalizedString(@"Hide Console", @"Hide Console")];
[toolbarItem setImage:[NSImage imageNamed:@"hideconsole"]];
} else {
- [toolbarItem setLabel:NSLocalizedString(@"Show Console", @"toolbar item for showconsole")];
+ [toolbarItem setLabel:NSLocalizedString(@"Show Console", @"Show Console")];
[toolbarItem setImage:[NSImage imageNamed:@"showconsole"]];
}
@@ -1317,12 +1523,12 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
[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:queryConsoleInstance];
+ [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")];
+ [toolbarItem setLabel:NSLocalizedString(@"Structure", @"toolbar item label for switching to the Table Structure tab")];
+ [toolbarItem setPaletteLabel:NSLocalizedString(@"Edit 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"]];
@@ -1331,8 +1537,8 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
[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")];
+ [toolbarItem setLabel:NSLocalizedString(@"Content", @"toolbar item label for switching to the Table Content tab")];
+ [toolbarItem setPaletteLabel:NSLocalizedString(@"Browse & Edit 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"]];
@@ -1341,8 +1547,8 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
[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")];
+ [toolbarItem setLabel:NSLocalizedString(@"Query", @"toolbar item label for switching to the Run Query tab")];
+ [toolbarItem setPaletteLabel:NSLocalizedString(@"Run Custom 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"]];
@@ -1396,7 +1602,7 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
{
return [NSArray arrayWithObjects:
@"DatabaseSelectToolbarItemIdentifier",
- NSToolbarFlexibleSpaceItemIdentifier,
+ NSToolbarSeparatorItemIdentifier,
@"SwitchToTableStructureToolbarItemIdentifier",
@"SwitchToTableContentToolbarItemIdentifier",
@"SwitchToRunQueryToolbarItemIdentifier",
@@ -1404,6 +1610,9 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
nil];
}
+/**
+ * toolbar delegate method
+ */
- (NSArray *)toolbarSelectableItemIdentifiers:(NSToolbar *)toolbar
{
return [NSArray arrayWithObjects:
@@ -1416,20 +1625,29 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
}
/**
- * validates the toolbar items
+ * Validates the toolbar items
*/
- (BOOL)validateToolbarItem:(NSToolbarItem *)toolbarItem;
{
- if ([[toolbarItem itemIdentifier] isEqualToString:@"ToggleConsoleIdentifier"]) {
- if ([[queryConsoleInstance window] isVisible]) {
+ NSString *identifier = [toolbarItem itemIdentifier];
+
+ // Toggle console item
+ if ([identifier isEqualToString:@"ToggleConsoleIdentifier"]) {
+ if ([[[SPQueryConsole sharedQueryConsole] window] isVisible]) {
[toolbarItem setLabel:@"Hide Console"];
[toolbarItem setImage:[NSImage imageNamed:@"hideconsole"]];
- } else {
+ }
+ else {
[toolbarItem setLabel:@"Show Console"];
[toolbarItem setImage:[NSImage imageNamed:@"showconsole"]];
}
}
+ // Clear console item
+ if ([identifier isEqualToString:@"ClearConsoleIdentifier"]) {
+ return ([[SPQueryConsole sharedQueryConsole] consoleMessageCount] > 0);
+ }
+
return YES;
}
@@ -1443,11 +1661,11 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
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)
+/**
+ * Code that need to be executed once the windowController has loaded the document's window
+ * sets upt the interface (small fonts).
*/
+- (void)windowControllerDidLoadNib:(NSWindowController *) aController
{
[aController setShouldCascadeWindows:YES];
[super windowControllerDidLoadNib:aController];
@@ -1455,8 +1673,6 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
NSEnumerator *theCols = [[variablesTableView tableColumns] objectEnumerator];
NSTableColumn *theCol;
- // [tableWindow makeKeyAndOrderFront:self];
-
prefs = [[NSUserDefaults standardUserDefaults] retain];
//register for notifications
@@ -1468,15 +1684,15 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
name:@"NSApplicationWillTerminateNotification" object:nil];
//set up interface
- if ( [prefs boolForKey:@"useMonospacedFonts"] ) {
- [[queryConsoleInstance consoleTextView] setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]];
+ if ( [prefs boolForKey:@"UseMonospacedFonts"] ) {
+ [[SPQueryConsole sharedQueryConsole] setConsoleFont:[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 {
- [[queryConsoleInstance consoleTextView] setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
+ [[SPQueryConsole sharedQueryConsole] setConsoleFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
[syntaxViewContent setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
while ( (theCol = [theCols nextObject]) ) {
[[theCol dataCell] setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
@@ -1487,21 +1703,33 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
[self setupToolbar];
// [self connectToDB:nil];
[self performSelector:@selector(connectToDB:) withObject:tableWindow afterDelay:0.0f];
+
+ if([prefs boolForKey:@"SelectLastFavoriteUsed"] == YES){
+ [favoritesController setSelectionIndex:[prefs integerForKey:@"LastFavoriteIndex"]];
+ } else {
+ [favoritesController setSelectionIndex:[prefs integerForKey:@"DefaultFavorite"]];
+ }
}
+// NSWindow delegate methods
+
+/**
+ * Invoked when the document window is about to close
+ */
- (void)windowWillClose:(NSNotification *)aNotification
{
+ //reset print settings, so we're not prompted about saving them
+ [self setPrintInfo:[NSPrintInfo sharedPrintInfo]];
+
if ([mySQLConnection isConnected]) [self closeConnection];
- if ([[queryConsoleInstance window] isVisible]) [self toggleConsole:self];
+ if ([[[SPQueryConsole sharedQueryConsole] window] isVisible]) [self toggleConsole:self];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
-
-//NSWindow delegate methods
-- (BOOL)windowShouldClose:(id)sender
-/*
- invoked when the document window should close
+/**
+ * Invoked when the document window should close
*/
+- (BOOL)windowShouldClose:(id)sender
{
if ( ![tablesListInstance selectionShouldChangeInTableView:nil] ) {
return NO;
@@ -1516,34 +1744,32 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
* Invoked when framework will perform a query
*/
- (void)willQueryString:(NSString *)query
-{
- NSString *currentTime = [[NSDate date] descriptionWithCalendarFormat:@"%H:%M:%S" timeZone:nil locale:nil];
-
- [queryConsoleInstance showMessageInConsole:[NSString stringWithFormat:@"/* MySQL %@ */ %@;\n", currentTime, query]];
+{
+ [[SPQueryConsole sharedQueryConsole] showMessageInConsole:query];
}
/**
* Invoked when query gave an error
*/
- (void)queryGaveError:(NSString *)error
-{
- NSString *currentTime = [[NSDate date] descriptionWithCalendarFormat:@"%H:%M:%S" timeZone:nil locale:nil];
-
- [queryConsoleInstance showErrorInConsole:[NSString stringWithFormat:@"/* ERROR %@ */ %@;\n", currentTime, error]];
+{
+ [[SPQueryConsole sharedQueryConsole] showErrorInConsole:error];
}
+#pragma mark -
#pragma mark Connection sheet delegate methods
/**
* When a favorite is selected, and the connection details are edited, deselect the favorite;
* this is clearer and also prevents a failed connection from being repopulated with the
* favorite's details instead of the last used details.
- * This method allows the password to be changed without altering the selection.
*/
- (void) controlTextDidChange:(NSNotification *)aNotification
{
- if ([aNotification object] == hostField || [aNotification object] == userField || [aNotification object] == databaseField
- || [aNotification object] == socketField || [aNotification object] == portField) {
+ if ([aNotification object] == nameField || [aNotification object] == hostField
+ || [aNotification object] == userField || [aNotification object] == passwordField
+ || [aNotification object] == databaseField || [aNotification object] == socketField
+ || [aNotification object] == portField) {
[favoritesController setSelectionIndexes:[NSIndexSet indexSet]];
}
else if ([aNotification object] == databaseNameField) {
@@ -1615,15 +1841,14 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa
}
-//tableView datasource methods
+#pragma mark TableView datasource methods
+
- (int)numberOfRowsInTableView:(NSTableView *)aTableView
{
return [variables count];
}
-- (id)tableView:(NSTableView *)aTableView
-objectValueForTableColumn:(NSTableColumn *)aTableColumn
- row:(int)rowIndex
+- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex
{
id theValue;
@@ -1642,10 +1867,6 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn
- (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];
}
@@ -1653,7 +1874,6 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn
{
[chooseDatabaseButton release];
[mySQLConnection release];
- [favorites release];
[variables release];
[selectedDatabase release];
[mySQLVersion release];
@@ -1663,3 +1883,33 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn
}
@end
+
+@implementation TableDocument (PrivateAPI)
+
+/**
+ * Checks to see if a favorite with the supplied details already exists.
+ */
+- (BOOL)_favoriteAlreadyExists:(NSString *)database host:(NSString *)host user:(NSString *)user
+{
+ NSArray *favorites = [favoritesController arrangedObjects];
+ int i;
+
+ // Ensure database, host, and user match prefs format
+ if (!database) database = @"";
+ if (!host) host = @"";
+ if (!user) user = @"";
+
+ // Loop the favorites and check their details
+ for (i = 0; i < [favorites count]; i++) {
+ NSDictionary *favorite = [favorites objectAtIndex:i];
+ if ([[favorite objectForKey:@"database"] isEqualToString:database] &&
+ [[favorite objectForKey:@"host"] isEqualToString:host] &&
+ [[favorite objectForKey:@"user"] isEqualToString:user]) {
+ return YES;
+ }
+ }
+
+ return NO;
+}
+
+@end
diff --git a/Source/TableDump.h b/Source/TableDump.h
index 3f89ab68..d35b40c4 100644
--- a/Source/TableDump.h
+++ b/Source/TableDump.h
@@ -58,6 +58,12 @@
IBOutlet id exportMultipleFieldsEscapedField;
IBOutlet id exportMultipleLinesTerminatedField;
+ // New Export Window
+ IBOutlet id exportWindow;
+ IBOutlet id exportTabBar;
+ IBOutlet id exportToolbar;
+ IBOutlet id exportTableList;
+
IBOutlet id importCSVView;
IBOutlet NSPopUpButton *importFormatPopup;
IBOutlet id importCSVBox;
@@ -104,8 +110,9 @@
- (IBAction)closeSheet:(id)sender;
- (IBAction)stepRow:(id)sender;
- (IBAction)cancelProgressBar:(id)sender;
+
//export methods
-//- (IBAction)saveDump:(id)sender;
+- (void)export;
- (void)exportFile:(int)tag;
- (void)savePanelDidEnd:(NSSavePanel *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo;
@@ -132,9 +139,14 @@
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)exportTables:(NSArray *)selectedTables toFileHandle:(NSFileHandle *)fileHandle usingFormat:(NSString *)type usingMulti:(BOOL)multi;
- (BOOL)exportSelectedTablesToFileHandle:(NSFileHandle *)fileHandle usingFormat:(NSString *)type;
+// New Export methods
+- (IBAction)switchTab:(id)sender;
+- (IBAction)switchInput:(id)sender;
+
//additional methods
- (void)setConnection:(CMMCPConnection *)theConnection;
diff --git a/Source/TableDump.m b/Source/TableDump.m
index fb0504a8..958969e9 100644
--- a/Source/TableDump.m
+++ b/Source/TableDump.m
@@ -31,6 +31,8 @@
#import "SPGrowlController.h"
#import "SPSQLParser.h"
#import "SPTableData.h"
+#import "SPStringAdditions.h"
+#import "SPArrayAdditions.h"
@implementation TableDump
@@ -88,12 +90,24 @@
ends the modal session
*/
{
+ [NSApp endSheet:exportWindow];
[NSApp stopModalWithCode:[sender tag]];
}
#pragma mark -
#pragma mark export methods
+- (void)export
+{
+ [self reloadTables:self];
+ [NSApp beginSheet:exportWindow modalForWindow:tableWindow modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:nil];
+}
+
+- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo
+{
+ [sheet orderOut:self];
+}
+
- (void)exportFile:(int)tag
/*
invoked when user clicks on an export menuItem
@@ -104,13 +118,13 @@
NSSavePanel *savePanel = [NSSavePanel savePanel];
[savePanel setAllowsOtherFileTypes:YES];
[savePanel setExtensionHidden:NO];
- NSString *currentDate = [[NSDate date] descriptionWithCalendarFormat:@"%d.%m.%Y" timeZone:nil locale:nil];
+ NSString *currentDate = [[NSDate date] descriptionWithCalendarFormat:@"%Y-%m-%d" timeZone:nil locale:nil];
switch ( tag ) {
case 5:
// export dump
[self reloadTables:self];
- file = [NSString stringWithFormat:@"%@_dump %@.sql", [tableDocumentInstance database], currentDate];
+ file = [NSString stringWithFormat:@"%@_%@.sql", [tableDocumentInstance database], currentDate];
[savePanel setRequiredFileType:@"sql"];
[savePanel setAccessoryView:exportDumpView];
contextInfo = @"exportDump";
@@ -242,11 +256,11 @@
// 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"];
+ success = [self exportTables:[NSArray arrayWithObject:[tableDocumentInstance table]] toFileHandle:fileHandle usingFormat:@"csv" usingMulti:NO];
// 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"];
+ success = [self exportTables:[NSArray arrayWithObject:[tableDocumentInstance table]] toFileHandle:fileHandle usingFormat:@"xml" usingMulti:NO];
// Export the current "browse" view to a file in CSV format
} else if ( [contextInfo isEqualToString:@"exportBrowseViewAsCSV"] ) {
@@ -373,14 +387,30 @@
NSError *errorStr = nil;
NSMutableString *errors = [NSMutableString string];
NSString *fileType = [[importFormatPopup selectedItem] title];
+ BOOL importSQLAsUTF8 = YES;
+
+ // Load file into string. For SQL imports, try UTF8 file encoding before the current encoding.
+ if ([fileType isEqualToString:@"SQL"]) {
+ NSLog(@"Reading as utf8");
+ dumpFile = [SPSQLParser stringWithContentsOfFile:filename
+ encoding:NSUTF8StringEncoding
+ error:&errorStr];
+ NSLog(dumpFile);
+ if (errorStr) {
+ importSQLAsUTF8 = NO;
+ errorStr = nil;
+ }
+ }
- //load file into string
- dumpFile = [SPSQLParser stringWithContentsOfFile:filename
- encoding:[CMMCPConnection encodingForMySQLEncoding:[[tableDocumentInstance connectionEncoding] UTF8String]]
- error:&errorStr];
+ // If the SQL-as-UTF8 read failed, and for CSVs, use the current connection encoding.
+ if (!importSQLAsUTF8 || [fileType isEqualToString:@"CSV"]) {
+ dumpFile = [SPSQLParser stringWithContentsOfFile:filename
+ encoding:[CMMCPConnection encodingForMySQLEncoding:[[tableDocumentInstance connectionEncoding] UTF8String]]
+ error:&errorStr];
+ }
if (errorStr) {
- NSBeginAlertSheet(NSLocalizedString(@"Error", @"Title of error alert"),
+ NSBeginAlertSheet(NSLocalizedString(@"Error", @"Error"),
NSLocalizedString(@"OK", @"OK button"),
nil, nil,
tableWindow, self,
@@ -428,7 +458,16 @@
for ( i = 0 ; i < [queries count] ; i++ ) {
[singleProgressBar setDoubleValue:((i+1)*100/[queries count])];
[singleProgressBar displayIfNeeded];
- [mySQLConnection queryString:[queries objectAtIndex:i]];
+
+ // Skip blank or whitespace-only queries to avoid errors
+ if ([[[queries objectAtIndex:i] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length] == 0)
+ continue;
+
+ if (importSQLAsUTF8) {
+ [mySQLConnection queryString:[queries objectAtIndex:i] usingEncoding:NSUTF8StringEncoding];
+ } else {
+ [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]]];
@@ -497,7 +536,7 @@
[singleProgressBar setIndeterminate:NO];
if([importArray count] == 0){
- NSBeginAlertSheet(NSLocalizedString(@"Error", @"Title of error alert"),
+ NSBeginAlertSheet(NSLocalizedString(@"Error", @"Error"),
NSLocalizedString(@"OK", @"OK button"),
nil, nil,
tableWindow, self,
@@ -584,7 +623,7 @@
if ( [fNames length] )
[fNames appendString:@","];
- [fNames appendString:[NSString stringWithFormat:@"`%@`", [[tableSourceInstance fieldNames] objectAtIndex:i]]];
+ [fNames appendString:[[[tableSourceInstance fieldNames] objectAtIndex:i] backtickQuotedString]];
}
}
@@ -613,8 +652,8 @@
}
//perform query
- [mySQLConnection queryString:[NSString stringWithFormat:@"INSERT INTO `%@` (%@) VALUES (%@)",
- [fieldMappingPopup titleOfSelectedItem],
+ [mySQLConnection queryString:[NSString stringWithFormat:@"INSERT INTO %@ (%@) VALUES (%@)",
+ [[fieldMappingPopup titleOfSelectedItem] backtickQuotedString],
fNames,
fValues]];
@@ -708,7 +747,7 @@
[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"]]];
+ [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]]];
}
@@ -746,20 +785,21 @@
*/
- (BOOL)dumpSelectedTablesAsSqlToFileHandle:(NSFileHandle *)fileHandle
{
- int i,j,t,rowCount, colCount, progressBarWidth, lastProgressValue, queryLength;
+ int i,j,t,rowCount, colCount, progressBarWidth, lastProgressValue, queryLength, tableType;
CMMCPResult *queryResult;
- NSString *tableName, *tableColumnTypeGrouping;
+ NSString *tableName, *tableColumnTypeGrouping, *previousConnectionEncoding;
NSArray *fieldNames;
NSArray *theRow;
NSMutableArray *selectedTables = [NSMutableArray array];
- NSMutableString *headerString = [NSMutableString string];
+ NSMutableString *metaString = [NSMutableString string];
NSMutableString *cellValue = [NSMutableString string];
NSMutableString *sqlString = [NSMutableString string];
NSMutableString *errors = [NSMutableString string];
NSDictionary *tableDetails;
NSMutableArray *tableColumnNumericStatus;
NSStringEncoding connectionEncoding = [mySQLConnection encoding];
- id createTableSyntax;
+ id createTableSyntax = nil;
+ BOOL previousConnectionEncodingViaLatin1;
// Reset the interface
[errorsView setString:@""];
@@ -783,16 +823,38 @@
}
// 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]];
+ [metaString setString:@"# Sequel Pro dump\n"];
+ [metaString appendString:[NSString stringWithFormat:@"# Version %@\n",
+ [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]]];
+ [metaString appendString:@"# http://code.google.com/p/sequel-pro\n#\n"];
+ [metaString appendString:[NSString stringWithFormat:@"# Host: %@ (MySQL %@)\n",
+ [tableDocumentInstance host], [tableDocumentInstance mySQLVersion]]];
+ [metaString appendString:[NSString stringWithFormat:@"# Database: %@\n", [tableDocumentInstance database]]];
+ [metaString appendString:[NSString stringWithFormat:@"# Generation Time: %@\n", [NSDate date]]];
+ [metaString appendString:@"# ************************************************************\n\n"];
+
+ // Add commands to store the client encodings used when importing and set to UTF8 to preserve data
+ [metaString appendString:@"/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n"];
+ [metaString appendString:@"/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n"];
+ [metaString appendString:@"/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n"];
+ [metaString appendString:@"/*!40101 SET NAMES utf8 */;\n"];
+
+ // Add commands to store and disable unique checks, foreign key checks, mode and notes where supported.
+ // Include trailing semicolons to ensure they're run individually. Use mysql-version based comments.
+ if ( [addDropTableSwitch state] == NSOnState )
+ [metaString appendString:@"/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n"];
+ [metaString appendString:@"/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n"];
+ [metaString appendString:@"/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n"];
+ [metaString appendString:@"/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n\n"];
+
+ [fileHandle writeData:[metaString dataUsingEncoding:NSUTF8StringEncoding]];
+
+ // Store the current connection encoding so it can be restored after the dump.
+ previousConnectionEncoding = [tableDocumentInstance connectionEncoding];
+ previousConnectionEncodingViaLatin1 = [tableDocumentInstance connectionEncodingViaLatin1];
+
+ // Set the connection to UTF8 to be able to export correctly.
+ [tableDocumentInstance setConnectionEncoding:@"utf8" reloadingViews:NO];
// Loop through the selected tables
for ( i = 0 ; i < [selectedTables count] ; i++ ) {
@@ -808,36 +870,48 @@
// Add the name of table
[fileHandle writeData:[[NSString stringWithFormat:@"# Dump of table %@\n# ------------------------------------------------------------\n\n", tableName]
- dataUsingEncoding:connectionEncoding]];
+ dataUsingEncoding:NSUTF8StringEncoding]];
+ // Determine whether this table is a table or a view via the create table command, and keep the create table syntax
+ queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW CREATE TABLE %@", [tableName backtickQuotedString]]];
+ if ( [queryResult numOfRows] ) {
+ tableDetails = [[NSDictionary alloc] initWithDictionary:[queryResult fetchRowAsDictionary]];
+ if ([tableDetails objectForKey:@"Create View"]) {
+ createTableSyntax = [[[tableDetails objectForKey:@"Create View"] copy] autorelease];
+ tableType = SP_TABLETYPE_VIEW;
+ } else {
+ createTableSyntax = [[[tableDetails objectForKey:@"Create Table"] copy] autorelease];
+ tableType = SP_TABLETYPE_TABLE;
+ }
+ [tableDetails release];
+ }
+ if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) {
+ [errors appendString:[NSString stringWithFormat:@"%@\n", [mySQLConnection getLastErrorMessage]]];
+ if ( [addErrorsSwitch state] == NSOnState ) {
+ [fileHandle writeData:[[NSString stringWithFormat:@"# Error: %@\n", [mySQLConnection getLastErrorMessage]] dataUsingEncoding:NSUTF8StringEncoding]];
+ }
+ }
+
+
// 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]];
+ [fileHandle writeData:[[NSString stringWithFormat:@"DROP %@ IF EXISTS %@;\n\n", ((tableType == SP_TABLETYPE_TABLE)?@"TABLE":@"VIEW"), [tableName backtickQuotedString]]
+ dataUsingEncoding:NSUTF8StringEncoding]];
+
// 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]];
- }
+ if ( [addCreateTableSwitch state] == NSOnState && createTableSyntax) {
+ if ( [createTableSyntax isKindOfClass:[NSData class]] ) {
+ createTableSyntax = [[[NSString alloc] initWithData:createTableSyntax encoding:connectionEncoding] autorelease];
}
+ [fileHandle writeData:[createTableSyntax dataUsingEncoding:NSUTF8StringEncoding]];
+ [fileHandle writeData:[[NSString stringWithString:@";\n\n"] dataUsingEncoding:NSUTF8StringEncoding]];
}
// Add the table content if required
- if ( [addTableContentSwitch state] == NSOnState ) {
- queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SELECT * FROM `%@`", tableName]];
+ if ( [addTableContentSwitch state] == NSOnState && tableType == SP_TABLETYPE_TABLE ) {
+ queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SELECT * FROM %@", [tableName backtickQuotedString]]];
fieldNames = [queryResult fetchFieldNames];
rowCount = [queryResult numOfRows];
@@ -868,9 +942,15 @@
[queryResult dataSeek:0];
queryLength = 0;
+ // Lock the table for writing and disable keys if supported
+ [metaString setString:@""];
+ [metaString appendString:[NSString stringWithFormat:@"LOCK TABLES %@ WRITE;\n", [tableName backtickQuotedString]]];
+ [metaString appendString:[NSString stringWithFormat:@"/*!40000 ALTER TABLE %@ DISABLE KEYS */;\n", [tableName backtickQuotedString]]];
+ [fileHandle writeData:[metaString dataUsingEncoding:connectionEncoding]];
+
// Construct the start of the insertion command
- [fileHandle writeData:[[NSString stringWithFormat:@"INSERT INTO `%@` (`%@`)\nVALUES\n\t(",
- tableName, [fieldNames componentsJoinedByString:@"`,`"]] dataUsingEncoding:connectionEncoding]];
+ [fileHandle writeData:[[NSString stringWithFormat:@"INSERT INTO %@ (%@)\nVALUES\n\t(",
+ [tableName backtickQuotedString], [fieldNames componentsJoinedAndBacktickQuoted]] dataUsingEncoding:NSUTF8StringEncoding]];
// Iterate through the rows to construct a VALUES group for each
for ( j = 0 ; j < rowCount ; j++ ) {
@@ -929,8 +1009,8 @@
// 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:@"`,`"]]];
+ [sqlString appendString:[NSString stringWithFormat:@");\n\nINSERT INTO %@ (%@)\nVALUES\n\t(",
+ [tableName backtickQuotedString], [fieldNames componentsJoinedAndBacktickQuoted]]];
queryLength = 0;
} else {
[sqlString appendString:@"),\n\t("];
@@ -940,26 +1020,53 @@
}
// Write this row to the file
- [fileHandle writeData:[sqlString dataUsingEncoding:connectionEncoding]];
+ [fileHandle writeData:[sqlString dataUsingEncoding:NSUTF8StringEncoding]];
}
// Complete the command
- [fileHandle writeData:[[NSString stringWithString:@";\n\n"] dataUsingEncoding:connectionEncoding]];
+ [fileHandle writeData:[[NSString stringWithString:@";\n\n"] dataUsingEncoding:NSUTF8StringEncoding]];
+
+ // Unlock the table and re-enable keys if supported
+ [metaString setString:@""];
+ [metaString appendString:[NSString stringWithFormat:@"/*!40000 ALTER TABLE %@ ENABLE KEYS */;\n", [tableName backtickQuotedString]]];
+ [metaString appendString:@"UNLOCK TABLES;\n"];
+ [fileHandle writeData:[metaString dataUsingEncoding:NSUTF8StringEncoding]];
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]];
+ dataUsingEncoding:NSUTF8StringEncoding]];
}
}
}
}
// Add an additional separator between tables
- [fileHandle writeData:[[NSString stringWithString:@"\n\n"] dataUsingEncoding:connectionEncoding]];
+ [fileHandle writeData:[[NSString stringWithString:@"\n\n"] dataUsingEncoding:NSUTF8StringEncoding]];
}
+ // Restore unique checks, foreign key checks, and other settings saved at the start
+ [metaString setString:@"\n\n\n"];
+ [metaString appendString:@"/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n"];
+ [metaString appendString:@"/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n"];
+ [metaString appendString:@"/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n"];
+ if ( [addDropTableSwitch state] == NSOnState )
+ [metaString appendString:@"/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n"];
+
+ // Restore the client encoding to the original encoding before import
+ [metaString appendString:@"/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n"];
+ [metaString appendString:@"/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n"];
+ [metaString appendString:@"/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n"];
+
+ // Write footer-type information to the file
+ [fileHandle writeData:[metaString dataUsingEncoding:NSUTF8StringEncoding]];
+
+ // Restore the connection character set to pre-export details
+ [tableDocumentInstance
+ setConnectionEncoding:[NSString stringWithFormat:@"%@%@", previousConnectionEncoding, previousConnectionEncodingViaLatin1?@"-":@""]
+ reloadingViews:NO];
+
// Close the progress sheet
[NSApp endSheet:singleProgressSheet];
[singleProgressSheet orderOut:nil];
@@ -995,7 +1102,7 @@
NSMutableString *csvCell = [NSMutableString string];
NSMutableArray *csvRow = [NSMutableArray array];
NSMutableString *csvString = [NSMutableString string];
- NSString *nullString = [NSString stringWithString:[prefs objectForKey:@"nullValue"]];
+ NSString *nullString = [NSString stringWithString:[prefs objectForKey:@"NullValue"]];
NSString *escapedEscapeString, *escapedFieldSeparatorString, *escapedEnclosingString, *escapedLineEndString;
NSString *dataConversionString;
NSScanner *csvNumericTester;
@@ -1286,14 +1393,14 @@
fieldCount = [tempRowArray count];
} else {
while ( [tempRowArray count] < fieldCount ) {
- [tempRowArray addObject:[NSString stringWithString:[prefs objectForKey:@"nullValue"]]];
+ [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"]] ) {
+ if ( [[tempRowArray objectAtIndex:i] isEqualToString:@"\\N"] || [[tempRowArray objectAtIndex:i] isEqualToString:[prefs objectForKey:@"NullValue"]] ) {
[tempRowArray replaceObjectAtIndex:i withObject:[NSNull null]];
} else {
@@ -1490,14 +1597,14 @@
}
}
- return [self exportTables:selectedTables toFileHandle:fileHandle usingFormat:type];
+ return [self exportTables:selectedTables toFileHandle:fileHandle usingFormat:type usingMulti:YES];
}
/*
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
+- (BOOL)exportTables:(NSArray *)selectedTables toFileHandle:(NSFileHandle *)fileHandle usingFormat:(NSString *)type usingMulti:(BOOL)multi
{
int i, j;
CMMCPResult *queryResult;
@@ -1586,7 +1693,7 @@
}
// Retrieve all the content within this table
- queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SELECT * FROM `%@`", tableName]];
+ queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SELECT * FROM %@", [tableName backtickQuotedString]]];
// Note any errors during retrieval
if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) {
@@ -1604,15 +1711,27 @@
// 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]
- withNumericColumns:tableColumnNumericStatus
- silently:YES];
+ if (multi) {
+ [self writeCsvForArray:nil orQueryResult:queryResult
+ toFileHandle:fileHandle
+ outputFieldNames:[exportMultipleFieldNamesSwitch state]
+ terminatedBy:[exportMultipleFieldsTerminatedField stringValue]
+ enclosedBy:[exportMultipleFieldsEnclosedField stringValue]
+ escapedBy:[exportMultipleFieldsEscapedField stringValue]
+ lineEnds:[exportMultipleLinesTerminatedField stringValue]
+ withNumericColumns:tableColumnNumericStatus
+ silently:YES];
+ } else {
+ [self writeCsvForArray:nil orQueryResult:queryResult
+ toFileHandle:fileHandle
+ outputFieldNames:[exportFieldNamesSwitch state]
+ terminatedBy:[exportFieldsTerminatedField stringValue]
+ enclosedBy:[exportFieldsEnclosedField stringValue]
+ escapedBy:[exportFieldsEscapedField stringValue]
+ lineEnds:[exportLinesTerminatedField stringValue]
+ withNumericColumns:tableColumnNumericStatus
+ silently:YES];
+ }
// Add a spacer to the file
[fileHandle writeData:[[NSString stringWithFormat:@"%@%@%@", csvLineEnd, csvLineEnd, csvLineEnd] dataUsingEncoding:connectionEncoding]];
@@ -1782,7 +1901,7 @@
[[exportDumpTableView tableColumnWithIdentifier:@"switch"] setDataCell:switchButton];
[[exportMultipleCSVTableView tableColumnWithIdentifier:@"switch"] setDataCell:switchButton];
[[exportMultipleXMLTableView tableColumnWithIdentifier:@"switch"] setDataCell:switchButton];
- if ( [prefs boolForKey:@"useMonospacedFonts"] ) {
+ if ( [prefs boolForKey:@"UseMonospacedFonts"] ) {
[[[exportDumpTableView tableColumnWithIdentifier:@"tables"] dataCell]
setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]];
[[[exportMultipleCSVTableView tableColumnWithIdentifier:@"tables"] dataCell]
@@ -1823,7 +1942,7 @@
forTableColumn:(NSTableColumn *)aTableColumn
row:(int)rowIndex
{
- if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"useMonospacedFonts"] ) {
+ if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"UseMonospacedFonts"] ) {
[aCell setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]];
}
else
@@ -1900,6 +2019,13 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn
#pragma mark -
#pragma mark other
+
+- (void)awakeFromNib
+{
+ [self switchTab:[[exportToolbar items] objectAtIndex:0]];
+ [exportToolbar setSelectedItemIdentifier:[[[exportToolbar items] objectAtIndex:0] itemIdentifier]];
+}
+
//last but not least
- (id)init;
{
@@ -1921,7 +2047,7 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn
[fieldMappingArray release];
[savePath release];
[openPath release];
- [prefs release];
+ [prefs release];
[super dealloc];
}
@@ -1931,4 +2057,41 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn
progressCancelled = YES;
}
+- (NSArray *)toolbarSelectableItemIdentifiers:(NSToolbar *)toolbar
+{
+ NSArray *array = [toolbar items];
+ NSMutableArray *items = [NSMutableArray arrayWithCapacity:6];
+ int i;
+
+ for (i = 0; i < [array count]; i++)
+ {
+ NSToolbarItem *item = [array objectAtIndex:i];
+ [items addObject:[item itemIdentifier]];
+ }
+
+ return items;
+}
+
+#pragma mark New Export methods
+
+- (IBAction)switchTab:(id)sender
+{
+ if ([sender isKindOfClass:[NSToolbarItem class]]) {
+ [exportTabBar selectTabViewItemWithIdentifier:[[sender label] lowercaseString]];
+ }
+}
+
+- (IBAction)switchInput:(id)sender
+{
+ if ([sender isKindOfClass:[NSMatrix class]]) {
+ [exportTableList setEnabled:([[sender selectedCell] tag] == 3)];
+ }
+}
+
+
+- (BOOL)validateToolbarItem:(NSToolbarItem *)toolbarItem
+{
+ return YES;
+}
+
@end
diff --git a/Source/TableSource.m b/Source/TableSource.m
index 5d1c8e43..92aa9811 100644
--- a/Source/TableSource.m
+++ b/Source/TableSource.m
@@ -25,6 +25,8 @@
#import "TableSource.h"
#import "TablesList.h"
#import "SPTableData.h"
+#import "SPStringAdditions.h"
+#import "SPArrayAdditions.h"
@implementation TableSource
@@ -48,6 +50,7 @@ loads aTable, put it in an array, update the tableViewColumns and reload the tab
selectedTable = aTable;
[tableSourceView deselectAll:self];
+ [indexView deselectAll:self];
if ( isEditingRow )
return;
@@ -80,7 +83,7 @@ loads aTable, put it in an array, update the tableViewColumns and reload the tab
[[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self];
//perform queries and load results in array (each row as a dictionary)
- tableSourceResult = [[mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM `%@`", selectedTable]] retain];
+ tableSourceResult = [[mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM %@", [selectedTable backtickQuotedString]]] retain];
// listFieldsFromTable is broken in the current version of the framework (no back-ticks for table name)!
// tableSourceResult = [[mySQLConnection listFieldsFromTable:selectedTable] retain];
@@ -88,7 +91,7 @@ loads aTable, put it in an array, update the tableViewColumns and reload the tab
[tableFields setArray:[self fetchResultAsArray:tableSourceResult]];
[tableSourceResult release];
- indexResult = [[mySQLConnection queryString:[NSString stringWithFormat:@"SHOW INDEX FROM `%@`", selectedTable]] retain];
+ indexResult = [[mySQLConnection queryString:[NSString stringWithFormat:@"SHOW INDEX FROM %@", [selectedTable backtickQuotedString]]] retain];
// [indexes setArray:[[self fetchResultAsArray:indexResult] retain]];
[indexes setArray:[self fetchResultAsArray:indexResult]];
[indexResult release];
@@ -179,10 +182,12 @@ loads aTable, put it in an array, update the tableViewColumns and reload the tab
// If a view is selected, disable the buttons; otherwise enable.
BOOL editingEnabled = ([tablesListInstance tableType] == SP_TABLETYPE_TABLE);
[addFieldButton setEnabled:editingEnabled];
- [copyFieldButton setEnabled:editingEnabled];
- [removeFieldButton setEnabled:editingEnabled];
[addIndexButton setEnabled:editingEnabled];
- [removeIndexButton setEnabled:editingEnabled];
+
+ //the following three buttons will only be enabled if a row field/index is selected!
+ [copyFieldButton setEnabled:NO];
+ [removeFieldButton setEnabled:NO];
+ [removeIndexButton setEnabled:NO];
//add columns to indexedColumnsField
[indexedColumnsField removeAllItems];
@@ -231,7 +236,7 @@ adds an empty row to the tableSource-array and goes into edit mode
if ( ![self saveRowOnDeselect] ) return;
[tableFields addObject:[NSMutableDictionary
- dictionaryWithObjects:[NSArray arrayWithObjects:@"",@"int",@"",@"0",@"0",@"0",@"YES",@"",[prefs stringForKey:@"nullValue"],@"None",nil]
+ 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]]];
[tableSourceView reloadData];
@@ -293,7 +298,7 @@ adds the index to the mysql-db and stops modal session with code 1 when success,
{
indexName = @"";
} else {
- indexName = [NSString stringWithFormat:@"`%@`", [indexNameField stringValue]];
+ indexName = [[indexNameField stringValue] backtickQuotedString];
}
}
indexedColumns = [[indexedColumnsField stringValue] componentsSeparatedByString:@","];
@@ -306,14 +311,14 @@ adds the index to the mysql-db and stops modal session with code 1 when success,
}
}
- [mySQLConnection queryString:[NSString stringWithFormat:@"ALTER TABLE `%@` ADD %@ %@ (`%@`)",
- selectedTable, [indexTypeField titleOfSelectedItem], indexName,
- [tempIndexedColumns componentsJoinedByString:@"`,`"]]];
+ [mySQLConnection queryString:[NSString stringWithFormat:@"ALTER TABLE %@ ADD %@ %@ (%@)",
+ [selectedTable backtickQuotedString], [indexTypeField titleOfSelectedItem], indexName,
+ [tempIndexedColumns componentsJoinedAndBacktickQuoted]]];
/*
-NSLog([NSString stringWithFormat:@"ALTER TABLE `%@` ADD %@ %@ (`%@`)",
- selectedTable, [indexTypeField titleOfSelectedItem], indexName,
- [[[indexedColumnsField stringValue] componentsSeparatedByString:@","] componentsJoinedByString:@"`,`"]]);
+NSLog([NSString stringWithFormat:@"ALTER TABLE %@ ADD %@ %@ (%@)",
+ [selectedTable backtickQuotedString], [indexTypeField titleOfSelectedItem], indexName,
+ [tempIndexedColumns componentsJoinedAndBacktickQuoted]]);
*/
if ( [[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) {
@@ -372,7 +377,7 @@ opens alertsheet and asks for confirmation
// 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];
+ NSString *query = [NSString stringWithFormat:@"ALTER TABLE %@ TYPE = %@",[selectedTable backtickQuotedString],selectedItem];
[mySQLConnection queryString:query];
// The query is now complete.
@@ -498,7 +503,7 @@ sets the connection (received from TableDocument) and makes things that have to
[tableSourceView registerForDraggedTypes:[NSArray arrayWithObjects:@"SequelProPasteboard", nil]];
while ( (indexColumn = [indexColumnsEnumerator nextObject]) ) {
- if ( [prefs boolForKey:@"useMonospacedFonts"] ) {
+ if ( [prefs boolForKey:@"UseMonospacedFonts"] ) {
[[indexColumn dataCell] setFont:[NSFont fontWithName:@"Monaco" size:10]];
}
else
@@ -507,7 +512,7 @@ sets the connection (received from TableDocument) and makes things that have to
}
}
while ( (fieldColumn = [fieldColumnsEnumerator nextObject]) ) {
- if ( [prefs boolForKey:@"useMonospacedFonts"] ) {
+ if ( [prefs boolForKey:@"UseMonospacedFonts"] ) {
[[fieldColumn dataCell] setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]];
}
else
@@ -537,7 +542,7 @@ fetches the result as an array with a dictionary for each row in it
for (int i = 0; i < [keys count] ; i++) {
key = [keys objectAtIndex:i];
if ( [[tempRow objectForKey:key] isMemberOfClass:[NSNull class]] )
- [tempRow setObject:[prefs objectForKey:@"nullValue"] forKey:key];
+ [tempRow setObject:[prefs objectForKey:@"NullValue"] forKey:key];
}
// change some fields to be more human-readable or GUI compatible
if ( [[tempRow objectForKey:@"Extra"] isEqualToString:@""] ) {
@@ -606,22 +611,22 @@ returns YES if no row is beeing edited and nothing has to be written to db
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"]];
+ queryString = [NSMutableString stringWithFormat:@"ALTER TABLE %@ ADD %@ %@",
+ [selectedTable backtickQuotedString], [[theRow objectForKey:@"Field"] backtickQuotedString], [theRow objectForKey:@"Type"]];
} else {
- queryString = [NSMutableString stringWithFormat:@"ALTER TABLE `%@` ADD `%@` %@(%@)",
- selectedTable, [theRow objectForKey:@"Field"], [theRow objectForKey:@"Type"],
+ queryString = [NSMutableString stringWithFormat:@"ALTER TABLE %@ ADD %@ %@(%@)",
+ [selectedTable backtickQuotedString], [[theRow objectForKey:@"Field"] backtickQuotedString], [theRow objectForKey:@"Type"],
[theRow objectForKey:@"Length"]];
}
} else {
//CHANGE syntax
if (([[theRow objectForKey:@"Length"] isEqualToString:@""]) || (![theRow objectForKey:@"Length"]) || ([[theRow objectForKey:@"Type"] isEqualToString:@"datetime"])) {
- queryString = [NSMutableString stringWithFormat:@"ALTER TABLE `%@` CHANGE `%@` `%@` %@",
- selectedTable, [oldRow objectForKey:@"Field"], [theRow objectForKey:@"Field"],
+ queryString = [NSMutableString stringWithFormat:@"ALTER TABLE %@ CHANGE %@ %@ %@",
+ [selectedTable backtickQuotedString], [[oldRow objectForKey:@"Field"] backtickQuotedString], [[theRow objectForKey:@"Field"] backtickQuotedString],
[theRow objectForKey:@"Type"]];
} else {
- queryString = [NSMutableString stringWithFormat:@"ALTER TABLE `%@` CHANGE `%@` `%@` %@(%@)",
- selectedTable, [oldRow objectForKey:@"Field"], [theRow objectForKey:@"Field"],
+ queryString = [NSMutableString stringWithFormat:@"ALTER TABLE %@ CHANGE %@ %@ %@(%@)",
+ [selectedTable backtickQuotedString], [[oldRow objectForKey:@"Field"] backtickQuotedString], [[theRow objectForKey:@"Field"] backtickQuotedString],
[theRow objectForKey:@"Type"], [theRow objectForKey:@"Length"]];
}
}
@@ -641,7 +646,7 @@ returns YES if no row is beeing edited and nothing has to be written to db
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:@"Default"] isEqualToString:[prefs objectForKey:@"NullValue"]] ) {
if ([[theRow objectForKey:@"Null"] isEqualToString:@"YES"] ) {
[queryString appendString:@" DEFAULT NULL "];
}
@@ -675,8 +680,8 @@ returns YES if no row is beeing edited and nothing has to be written to db
if ( [chooseKeyButton indexOfSelectedItem] == 0 ) {
[queryString appendString:@" PRIMARY KEY"];
} else {
- [queryString appendString:[NSString stringWithFormat:@", ADD %@ (`%@`)",
- [chooseKeyButton titleOfSelectedItem], [theRow objectForKey:@"Field"]]];
+ [queryString appendString:[NSString stringWithFormat:@", ADD %@ (%@)",
+ [chooseKeyButton titleOfSelectedItem], [[theRow objectForKey:@"Field"] backtickQuotedString]]];
}
}
}
@@ -743,8 +748,8 @@ returns YES if no row is beeing edited and nothing has to be written to db
} else if ( [contextInfo isEqualToString:@"removefield"] ) {
if ( returnCode == NSAlertDefaultReturn ) {
//remove row
- [mySQLConnection queryString:[NSString stringWithFormat:@"ALTER TABLE `%@` DROP `%@`",
- selectedTable, [[tableFields objectAtIndex:[tableSourceView selectedRow]] objectForKey:@"Field"]]];
+ [mySQLConnection queryString:[NSString stringWithFormat:@"ALTER TABLE %@ DROP %@",
+ [selectedTable backtickQuotedString], [[[tableFields objectAtIndex:[tableSourceView selectedRow]] objectForKey:@"Field"] backtickQuotedString]]];
if ( [[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) {
[self loadTable:selectedTable];
@@ -763,10 +768,10 @@ returns YES if no row is beeing edited and nothing has to be written to db
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]];
+ [mySQLConnection queryString:[NSString stringWithFormat:@"ALTER TABLE %@ DROP PRIMARY KEY", [selectedTable backtickQuotedString]]];
} else {
- [mySQLConnection queryString:[NSString stringWithFormat:@"ALTER TABLE `%@` DROP INDEX `%@`",
- selectedTable, [[indexes objectAtIndex:[indexView selectedRow]] objectForKey:@"Key_name"]]];
+ [mySQLConnection queryString:[NSString stringWithFormat:@"ALTER TABLE %@ DROP INDEX %@",
+ [selectedTable backtickQuotedString], [[[indexes objectAtIndex:[indexView selectedRow]] objectForKey:@"Key_name"] backtickQuotedString]]];
}
if ( [[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) {
@@ -788,9 +793,9 @@ get the default value for a specified field
- (NSString *)defaultValueForField:(NSString *)field
{
if ( ![defaultValues objectForKey:field] ) {
- return [prefs objectForKey:@"nullValue"];
+ return [prefs objectForKey:@"NullValue"];
} else if ( [[defaultValues objectForKey:field] isMemberOfClass:[NSNull class]] ) {
- return [prefs objectForKey:@"nullValue"];
+ return [prefs objectForKey:@"NullValue"];
} else {
return [defaultValues objectForKey:field];
}
@@ -859,6 +864,9 @@ returns a dictionary containing enum/set field names as key and possible values
forTableColumn:(NSTableColumn *)aTableColumn
row:(int)rowIndex
{
+ //make sure that the drag operation is for the right table view
+ if (aTableView!=tableSourceView) return;
+
if ( !isEditingRow ) {
[oldRow setDictionary:[tableFields objectAtIndex:rowIndex]];
isEditingRow = YES;
@@ -876,6 +884,10 @@ Begin a drag and drop operation from the table - copy a single dragged row to th
*/
- (BOOL)tableView:(NSTableView *)tableView writeRows:(NSArray*)rows toPasteboard:(NSPasteboard*)pboard
{
+ //make sure that the drag operation is started from the right table view
+ if (tableView!=tableSourceView) return NO;
+
+
int originalRow;
NSArray *pboardTypes;
@@ -903,6 +915,9 @@ would result in a position change.
- (NSDragOperation)tableView:(NSTableView*)tableView validateDrop:(id <NSDraggingInfo>)info proposedRow:(int)row
proposedDropOperation:(NSTableViewDropOperation)operation
{
+ //make sure that the drag operation is for the right table view
+ if (tableView!=tableSourceView) return NO;
+
NSArray *pboardTypes = [[info draggingPasteboard] types];
int originalRow;
@@ -927,6 +942,9 @@ would result in a position change.
*/
- (BOOL)tableView:(NSTableView*)tableView acceptDrop:(id <NSDraggingInfo>)info row:(int)destinationRowIndex dropOperation:(NSTableViewDropOperation)operation
{
+ //make sure that the drag operation is for the right table view
+ if (tableView!=tableSourceView) return NO;
+
int originalRowIndex;
NSMutableString *queryString;
NSDictionary *originalRow;
@@ -938,8 +956,8 @@ would result in a position change.
[[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self];
// Begin construction of the reordering query
- queryString = [NSMutableString stringWithFormat:@"ALTER TABLE `%@` MODIFY COLUMN `%@` %@", selectedTable,
- [originalRow objectForKey:@"Field"],
+ queryString = [NSMutableString stringWithFormat:@"ALTER TABLE %@ MODIFY COLUMN %@ %@", [selectedTable backtickQuotedString],
+ [[originalRow objectForKey:@"Field"] backtickQuotedString],
[originalRow objectForKey:@"Type"]];
// Add the length parameter if necessary
@@ -962,7 +980,7 @@ would result in a position change.
}
// Add the default value
- if ([[originalRow objectForKey:@"Default"] isEqualToString:[prefs objectForKey:@"nullValue"]]) {
+ if ([[originalRow objectForKey:@"Default"] isEqualToString:[prefs objectForKey:@"NullValue"]]) {
if ([[originalRow objectForKey:@"Null"] isEqualToString:@"YES"]) {
[queryString appendString:@" DEFAULT NULL"];
}
@@ -976,8 +994,8 @@ would result in a position change.
if ( destinationRowIndex == 0 ){
[queryString appendString:@" FIRST"];
} else {
- [queryString appendString:[NSString stringWithFormat:@" AFTER `%@`",
- [[tableFields objectAtIndex:destinationRowIndex-1] objectForKey:@"Field"]]];
+ [queryString appendString:[NSString stringWithFormat:@" AFTER %@",
+ [[[tableFields objectAtIndex:destinationRowIndex-1] objectForKey:@"Field"] backtickQuotedString]]];
}
// Run the query; report any errors, or reload the table on success
@@ -1008,13 +1026,32 @@ would result in a position change.
- (void)tableViewSelectionDidChange:(NSNotification *)aNotification
{
-
- // Check our notification object is our table fields view
- if ([aNotification object] != tableSourceView)
- return;
-
- // If we are editing a row, attempt to save that row - if saving failed, reselect the edit row.
- if ( isEditingRow && [tableSourceView selectedRow] != currentlyEditingRow && ![self saveRowOnDeselect] ) return;
+ //check for which table view the selection changed
+ if ([aNotification object] == tableSourceView) {
+ // If we are editing a row, attempt to save that row - if saving failed, reselect the edit row.
+ if ( isEditingRow && [tableSourceView selectedRow] != currentlyEditingRow ) {
+ [self saveRowOnDeselect];
+ }
+
+ // check if there is currently a field selected
+ // and change button state accordingly
+ if ([tableSourceView numberOfSelectedRows] > 0 && [tablesListInstance tableType] == SP_TABLETYPE_TABLE) {
+ [removeFieldButton setEnabled:YES];
+ [copyFieldButton setEnabled:YES];
+ } else {
+ [removeFieldButton setEnabled:NO];
+ [copyFieldButton setEnabled:NO];
+ }
+ }
+ else if ([aNotification object] == indexView) {
+ // check if there is currently an index selected
+ // and change button state accordingly
+ if ([indexView numberOfSelectedRows] > 0 && [tablesListInstance tableType] == SP_TABLETYPE_TABLE) {
+ [removeIndexButton setEnabled:YES];
+ } else {
+ [removeIndexButton setEnabled:NO];
+ }
+ }
}
/*
@@ -1078,7 +1115,11 @@ traps enter and esc and make/cancel editing without entering next row
* Modify cell display by disabling table cells when a view is selected, meaning structure/index
* is uneditable.
*/
-- (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex {
+- (void)tableView:(NSTableView *)tableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex {
+
+ //make sure that the message is from the right table view
+ if (tableView!=tableSourceView) return;
+
[aCell setEnabled:([tablesListInstance tableType] == SP_TABLETYPE_TABLE)];
}
diff --git a/Source/TableStatus.h b/Source/TableStatus.h
index 73b68fd7..51c8b132 100644
--- a/Source/TableStatus.h
+++ b/Source/TableStatus.h
@@ -49,7 +49,7 @@
CMMCPResult *tableStatusResult;
NSString *selectedTable;
- NSDictionary* statusFields;
+ NSDictionary *statusFields;
}
// Table methods
@@ -58,8 +58,5 @@
// Additional methods
- (void)setConnection:(CMMCPConnection *)theConnection;
-- (void)awakeFromNib;
-// Initialization
-- (id)init;
@end
diff --git a/Source/TableStatus.m b/Source/TableStatus.m
index 4edaa4e0..9d4cf03f 100644
--- a/Source/TableStatus.m
+++ b/Source/TableStatus.m
@@ -4,11 +4,6 @@
@implementation TableStatus
-- (void)awakeFromNib
-{
- // TODO: implement awake code.
-}
-
- (void)setConnection:(CMMCPConnection *)theConnection
{
mySQLConnection = theConnection;
@@ -34,28 +29,20 @@
// Format date strings to the user's long date format
else if ([aKey isEqualToString:@"Create_time"] ||
[aKey isEqualToString:@"Update_time"]) {
-
+
// Create date formatter
NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
- // Set the date format returned by MySQL
- [dateFormatter setDateFormat:@"%Y-%m-%d %H:%M:%S"];
-
- // Get the date instance
- NSDate *date = [dateFormatter dateFromString:value];
-
- // This behaviour should be set after the above date string is parsed to a date object so we can
- // use the below style methods.
[dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
-
+
[dateFormatter setDateStyle:NSDateFormatterLongStyle];
[dateFormatter setTimeStyle:NSDateFormatterMediumStyle];
-
- value = [dateFormatter stringFromDate:date];
+
+ value = [dateFormatter stringFromDate:[NSDate dateWithNaturalLanguageString:value]];
}
}
- NSString* labelVal = [NSString stringWithFormat:@"%@: %@", label, value];
+ NSString *labelVal = [NSString stringWithFormat:@"%@: %@", label, value];
return labelVal;
}
@@ -64,11 +51,21 @@
{
// Store the table name away for future use...
selectedTable = aTable;
+
+ // Retrieve the table status information via the table data cache
+ statusFields = [tableDataInstance statusValues];
+
+ // No table selected or view selected
+ if([aTable isEqualToString:@""] || !aTable || [[statusFields objectForKey:@"Engine"] isEqualToString:@"View"]) {
- // No table selected
- if([aTable isEqualToString:@""] || !aTable) {
- [tableName setStringValue:@"Name: --"];
- [tableType setStringValue:@"Type: --"];
+ if ([[statusFields objectForKey:@"Engine"] isEqualToString:@"View"]) {
+ [tableName setStringValue:[NSString stringWithFormat:@"Name: %@", selectedTable]];
+ [tableType setStringValue:@"Type: View"];
+ } else {
+ [tableName setStringValue:@"Name: --"];
+ [tableType setStringValue:@"Type: --"];
+ }
+
[tableCreatedAt setStringValue:@"Created At: --"];
[tableUpdatedAt setStringValue:@"Updated At: --"];
@@ -90,9 +87,6 @@
return;
}
- // Retrieve the table status information via the table data cache
- statusFields = [tableDataInstance statusValues];
-
// Assign the table values...
[tableName setStringValue:[NSString stringWithFormat:@"Name: %@",selectedTable]];
[tableType setStringValue:[self formatValueWithKey:@"Engine" inDictionary:statusFields withLabel:@"Type"]];
@@ -129,4 +123,5 @@
return self;
}
+
@end
diff --git a/Source/TablesList.h b/Source/TablesList.h
index b4875dd9..76b49609 100644
--- a/Source/TablesList.h
+++ b/Source/TablesList.h
@@ -31,8 +31,7 @@ enum sp_table_types
SP_TABLETYPE_VIEW = 1
};
-@class CMMCResult;
-@class CMMCPConnection;
+@class CMMCResult, CMMCPConnection;
@interface TablesList : NSObject {
@@ -50,32 +49,35 @@ enum sp_table_types
IBOutlet id copyTableNameField;
IBOutlet id copyTableContentSwitch;
IBOutlet id tabView;
+ IBOutlet id tableSheet;
+ IBOutlet id tableNameField;
+ IBOutlet id tableEncodingButton;
+ IBOutlet id addTableButton;
CMMCPConnection *mySQLConnection;
+
NSMutableArray *tables;
NSMutableArray *tableTypes;
-// NSUserDefaults *prefs;
+
BOOL structureLoaded, contentLoaded, statusLoaded, alertSheetOpened;
}
-//IBAction methods
+// IBAction methods
- (IBAction)updateTables:(id)sender;
- (IBAction)addTable:(id)sender;
+- (IBAction)closeTableSheet:(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
+// copyTableSheet methods
- (IBAction)closeCopyTableSheet:(id)sender;
-//additional methods
+// Additional methods
- (void)removeTable;
- (void)setConnection:(CMMCPConnection *)theConnection;
- (void)doPerformQueryService:(NSString *)query;
-//getter methods
+// Getters
- (NSString *)tableName;
- (int)tableType;
- (NSArray *)tables;
@@ -84,22 +86,7 @@ enum sp_table_types
- (BOOL)contentLoaded;
- (BOOL)statusLoaded;
-// Setter methods
+// Setters
- (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
index ac1445a2..f6a5e441 100644
--- a/Source/TablesList.m
+++ b/Source/TablesList.m
@@ -31,21 +31,28 @@
#import "ImageAndTextCell.h"
#import "CMMCPConnection.h"
#import "CMMCPResult.h"
+#import "SPStringAdditions.h"
@implementation TablesList
-
#pragma mark IBAction methods
-/*
-loads all table names in array tables and reload the tableView
-*/
+/**
+ * Loads all table names in array tables and reload the tableView
+ */
- (IBAction)updateTables:(id)sender
{
CMMCPResult *theResult;
NSArray *resultRow;
int i;
BOOL containsViews = NO;
+ NSString *selectedTable = nil;
+ int selectedRowIndex;
+
+ selectedRowIndex = [tablesListView selectedRow];
+ if(selectedRowIndex > 0 && [tables count]){
+ selectedTable = [NSString stringWithString:[tables objectAtIndex:selectedRowIndex]];
+ }
[tablesListView deselectAll:self];
[tables removeAllObjects];
@@ -89,58 +96,150 @@ loads all table names in array tables and reload the tableView
// Notify listeners that the query has finished
[[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self];
- [tablesListView reloadData];
+ [tablesListView reloadData];
+
+ //if the previous selected table still exists, select it
+ if( selectedTable != nil && [tables indexOfObject:selectedTable] < [tables count]) {
+ [tablesListView selectRowIndexes:[NSIndexSet indexSetWithIndex:[tables indexOfObject:selectedTable]] byExtendingSelection:NO];
+ }
}
-/*
-adds a new table to the tables-array (no changes in mysql-db)
-*/
+/**
+ * Adds a new table to the tables-array (no changes in mysql-db)
+ */
- (IBAction)addTable:(id)sender
{
- if ( ![tableSourceInstance saveRowOnDeselect] ||
- ![tableContentInstance saveRowOnDeselect] ||
- ![tableDocumentInstance database] )
+ if ((![tableSourceInstance saveRowOnDeselect]) || (![tableContentInstance saveRowOnDeselect]) || (![tableDocumentInstance database])) {
return;
+ }
+
[tableWindow endEditingFor:nil];
+
+ [NSApp beginSheet:tableSheet
+ modalForWindow:tableWindow
+ modalDelegate:self
+ didEndSelector:nil
+ contextInfo:nil];
+
+ int returnCode = [NSApp runModalForWindow:tableSheet];
+
+ [NSApp endSheet:tableSheet];
+ [tableSheet orderOut:nil];
+
+ if (!returnCode) {
+ // Clear table name
+ [tableNameField setStringValue:@""];
+
+ return;
+ }
+
+ NSString *tableName = [tableNameField stringValue];
+ NSString *createStatement = [NSString stringWithFormat:@"CREATE TABLE %@ (id INT)", [tableName backtickQuotedString]];
+
+ // If there is an encoding selected other than the default we must specify it in CREATE TABLE statement
+ if ([tableEncodingButton indexOfSelectedItem] > 0) {
+ createStatement = [NSString stringWithFormat:@"%@ DEFAULT CHARACTER SET %@", createStatement, [[tableDocumentInstance mysqlEncodingFromDisplayEncoding:[tableEncodingButton title]] backtickQuotedString]];
+ }
+
+ // Create the table
+ [mySQLConnection queryString:createStatement];
+
+ if ([[mySQLConnection getLastErrorMessage] isEqualToString:@""]) {
+
+ // Table creation was successful
+ [tables addObject:tableName];
+ [tableTypes addObject:[NSNumber numberWithInt:SP_TABLETYPE_TABLE]];
+ [tablesListView reloadData];
+ [tablesListView selectRow:([tables count] - 1) byExtendingSelection:NO];
+
+ int selectedIndex = [tabView indexOfTabViewItem:[tabView selectedTabViewItem]];
+
+ if (selectedIndex == 0) {
+ [tableSourceInstance loadTable:tableName];
+ structureLoaded = YES;
+ contentLoaded = NO;
+ statusLoaded = NO;
+ }
+ else if (selectedIndex == 1) {
+ [tableContentInstance loadTable:tableName];
+ structureLoaded = NO;
+ contentLoaded = YES;
+ statusLoaded = NO;
+ }
+ else if (selectedIndex == 3) {
+ [tableStatusInstance loadTable:tableName];
+ structureLoaded = NO;
+ contentLoaded = NO;
+ statusLoaded = YES;
+ }
+ else {
+ statusLoaded = NO;
+ structureLoaded = NO;
+ contentLoaded = NO;
+ }
+
+ // Set window title
+ [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@/%@/%@", [tableDocumentInstance mySQLVersion],
+ [tableDocumentInstance name], [tableDocumentInstance database], tableName]];
+ }
+ else {
+ // Error while creating 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"),
+ tableName, [mySQLConnection getLastErrorMessage]]);
+
+ [tableTypes removeObjectAtIndex:([tableTypes count] - 1)];
+ [tables removeObjectAtIndex:([tables count] - 1)];
+ [tablesListView reloadData];
+ }
+
+ // Clear table name
+ [tableNameField setStringValue:@""];
+}
- [tables addObject:@""];
- [tableTypes addObject:[NSNumber numberWithInt:SP_TABLETYPE_TABLE]];
- [tablesListView reloadData];
- [tablesListView selectRow:[tables count]-1 byExtendingSelection:NO];
- [tablesListView editColumn:0 row:[tables count]-1 withEvent:nil select:YES];
+/**
+ * Closes the add table sheet and stops the modal session
+ */
+- (IBAction)closeTableSheet:(id)sender
+{
+ [NSApp stopModalWithCode:[sender tag]];
}
-/*
-invoked when user hits the remove button
-alert sheet to ask user if he really wants to delete the table
-*/
+/**
+ * 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] )
+ if (![tablesListView numberOfSelectedRows])
return;
+
[tableWindow endEditingFor:nil];
+
+ NSAlert *alert = [NSAlert alertWithMessageText:@"" defaultButton:NSLocalizedString(@"Delete", @"delete button") alternateButton:NSLocalizedString(@"Cancel", @"cancel button") otherButton:nil informativeTextWithFormat:@""];
- 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]]]);
+ [alert setAlertStyle:NSCriticalAlertStyle];
+
+ if ([tablesListView numberOfSelectedRows] == 1) {
+ [alert setMessageText:[NSString stringWithFormat:NSLocalizedString(@"Delete table '%@'?", @"delete table message"), [tables objectAtIndex:[tablesListView selectedRow]]]];
+ [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to delete the table '%@'. This operation cannot be undone.", @"delete table informative message"), [tables objectAtIndex:[tablesListView selectedRow]]]];
+ }
+ else {
+ [alert setMessageText:NSLocalizedString(@"Delete selected tables?", @"delete tables message")];
+ [alert setInformativeText:NSLocalizedString(@"Are you sure you want to delete the selected tables. This operation cannot be undone.", @"delete tables informative message")];
}
+
+ [alert beginSheetModalForWindow:tableWindow modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:@"removeRow"];
}
-/*
-copies a table, if desired with content
-*/
+/**
+ * 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];
@@ -151,8 +250,9 @@ copies a table, if desired with content
if ( [tablesListView numberOfSelectedRows] != 1 )
return;
- if ( ![tableSourceInstance saveRowOnDeselect] || ![tableContentInstance saveRowOnDeselect] )
+ if ( ![tableSourceInstance saveRowOnDeselect] || ![tableContentInstance saveRowOnDeselect] ) {
return;
+ }
[tableWindow endEditingFor:nil];
//open copyTableSheet
@@ -177,8 +277,9 @@ copies a table, if desired with content
}
//get table structure
- queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW CREATE TABLE `%@`",
- [tables objectAtIndex:[tablesListView selectedRow]]]];
+ queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW CREATE TABLE %@",
+ [[tables objectAtIndex:[tablesListView selectedRow]] backtickQuotedString]
+ ]];
if ( ![queryResult numOfRows] ) {
//error while getting table structure
@@ -187,10 +288,14 @@ copies a table, if desired with content
} else {
//insert new table name in create syntax and create new table
+ NSScanner *scanner = [NSScanner alloc];
+ NSString *scanString;
+
[scanner initWithString:[[queryResult fetchRowAsDictionary] objectForKey:@"Create Table"]];
[scanner scanUpToString:@"(" intoString:nil];
[scanner scanUpToString:@"" intoString:&scanString];
- [mySQLConnection queryString:[NSString stringWithFormat:@"CREATE TABLE `%@` %@", [copyTableNameField stringValue], scanString]];
+ [mySQLConnection queryString:[NSString stringWithFormat:@"CREATE TABLE %@ %@", [[copyTableNameField stringValue] backtickQuotedString], scanString]];
+ [scanner release];
if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) {
//error while creating new table
@@ -201,9 +306,9 @@ copies a table, if desired with content
if ( [copyTableContentSwitch state] == NSOnState ) {
//copy table content
[mySQLConnection queryString:[NSString stringWithFormat:
- @"INSERT INTO `%@` SELECT * FROM `%@`",
- [copyTableNameField stringValue],
- [tables objectAtIndex:[tablesListView selectedRow]]
+ @"INSERT INTO %@ SELECT * FROM %@",
+ [[copyTableNameField stringValue] backtickQuotedString],
+ [[tables objectAtIndex:[tablesListView selectedRow]] backtickQuotedString]
]];
if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) {
@@ -231,28 +336,25 @@ copies a table, if desired with content
}
}
-
#pragma mark Alert sheet methods
-/*
-method for alert sheets
-invoked when user wants to delete a table
-*/
+/**
+ * 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
-*/
+/**
+ * Closes copyTableSheet and stops modal session
+ */
- (IBAction)closeCopyTableSheet:(id)sender
{
[NSApp stopModalWithCode:[sender tag]];
@@ -260,10 +362,10 @@ closes copyTableSheet and stops modal session
#pragma mark Additional methods
-/*
-removes selected table(s) from mysql-db and tableView
-*/
-- (void)removeTable;
+/**
+ * Removes selected table(s) from mysql-db and tableView
+ */
+- (void)removeTable
{
NSIndexSet *indexes = [tablesListView selectedRowIndexes];
NSString *errorText;
@@ -273,7 +375,9 @@ removes selected table(s) from mysql-db and tableView
unsigned currentIndex = [indexes lastIndex];
while (currentIndex != NSNotFound)
{
- [mySQLConnection queryString:[NSString stringWithFormat:@"DROP TABLE `%@`", [tables objectAtIndex:currentIndex]]];
+ [mySQLConnection queryString: [NSString stringWithFormat: @"DROP TABLE %@",
+ [[tables objectAtIndex:currentIndex] backtickQuotedString]
+ ]];
if ( [[mySQLConnection getLastErrorMessage] isEqualTo:@""] ) {
//dropped table with success
@@ -296,37 +400,46 @@ removes selected table(s) from mysql-db and tableView
[tablesListView reloadData];
// set window title
- [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@@%@/%@", [tableDocumentInstance mySQLVersion], [tableDocumentInstance user],
- [tableDocumentInstance host], [tableDocumentInstance database]]];
+ [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@/%@", [tableDocumentInstance mySQLVersion],
+ [tableDocumentInstance name], [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
-*/
+/**
+ * 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
-*/
+/**
+ * Selects customQuery tab and passes query to customQueryInstance
+ */
- (void)doPerformQueryService:(NSString *)query
{
[tabView selectTabViewItemAtIndex:2];
[customQueryInstance doPerformQueryService:query];
}
+/**
+ * Performs interface validation for various controls.
+ */
+- (void)controlTextDidChange:(NSNotification *)notification
+{
+ if ([notification object] == tableNameField) {
+ [addTableButton setEnabled:([[tableNameField stringValue] length] > 0)];
+ }
+}
#pragma mark Getter methods
-/*
-returns the currently selected table or nil if no table or mulitple tables are selected
-*/
+/**
+ * Returns the currently selected table or nil if no table or mulitple tables are selected
+ */
- (NSString *)tableName
{
if ( [tablesListView numberOfSelectedRows] == 1 ) {
@@ -352,35 +465,41 @@ returns the currently selected table or nil if no table or mulitple tables are s
}
}
+/**
+ * Database tables accessor
+ */
- (NSArray *)tables
{
return tables;
}
+/**
+ * Database table types accessor
+ */
- (NSArray *)tableTypes
{
return tableTypes;
}
-/*
-returns YES if table source has already been loaded
-*/
+/**
+ * Returns YES if table source has already been loaded
+ */
- (BOOL)structureLoaded
{
return structureLoaded;
}
-/*
-returns YES if table content has already been loaded
-*/
+/**
+ * Returns YES if table content has already been loaded
+ */
- (BOOL)contentLoaded
{
return contentLoaded;
}
-/*
-returns YES if table status has already been loaded
-*/
+/**
+ * Returns YES if table status has already been loaded
+ */
- (BOOL)statusLoaded
{
return statusLoaded;
@@ -388,9 +507,9 @@ returns YES if table status has already been loaded
#pragma mark Setter methods
-/*
-Mark the content table for refresh when it's next switched to
-*/
+/**
+ * Mark the content table for refresh when it's next switched to
+ */
- (void)setContentRequiresReload:(BOOL)reload
{
contentLoaded = !reload;
@@ -398,147 +517,90 @@ Mark the content table for refresh when it's next switched to
#pragma mark Datasource methods
+/**
+ * Returns the number of tables in the current database.
+ */
- (int)numberOfRowsInTableView:(NSTableView *)aTableView
{
return [tables count];
}
-- (id)tableView:(NSTableView *)aTableView
- objectValueForTableColumn:(NSTableColumn *)aTableColumn
- row:(int)rowIndex
+/**
+ * Returns the table names to be displayed in the tables list table view.
+ */
+- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex
{
- return [tables objectAtIndex: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
+ * 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
+- (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];
- [tableTypes removeObjectAtIndex:rowIndex];
- [tablesListView reloadData];
- } else {
- if ( [tableDocumentInstance supportsEncoding] ) {
- [mySQLConnection queryString:[NSString stringWithFormat:@"CREATE TABLE `%@` (id int) DEFAULT CHARACTER SET %@", anObject, [tableDocumentInstance connectionEncoding]]];
- } else {
- [mySQLConnection queryString:[NSString stringWithFormat:@"CREATE TABLE `%@` (id int)", anObject]];
- }
+ if ([[tables objectAtIndex:rowIndex] isEqualToString:anObject]) {
+ // No changes in table name
+ }
+ else 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"));
+ }
+ else {
+ [mySQLConnection queryString:[NSString stringWithFormat:@"RENAME TABLE %@ TO %@", [[tables objectAtIndex:rowIndex] backtickQuotedString], [anObject backtickQuotedString]]];
+
+ if ([[mySQLConnection getLastErrorMessage] isEqualToString:@""]) {
+ // Renamed with success
+ [tables replaceObjectAtIndex:rowIndex withObject: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 ) {
- [tableContentInstance loadTable:anObject];
- structureLoaded = NO;
- 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]]);
- [tableTypes removeObjectAtIndex:rowIndex];
- [tables removeObjectAtIndex:rowIndex];
- [tablesListView reloadData];
+ int selectedIndex = [tabView indexOfTabViewItem:[tabView selectedTabViewItem]];
+
+ if (selectedIndex == 0) {
+ [tableSourceInstance loadTable:anObject];
+ structureLoaded = YES;
+ contentLoaded = NO;
+ statusLoaded = NO;
+ }
+ else if (selectedIndex == 1) {
+ [tableContentInstance loadTable:anObject];
+ structureLoaded = NO;
+ contentLoaded = YES;
+ statusLoaded = NO;
+ }
+ else if (selectedIndex == 3) {
+ [tableStatusInstance loadTable:anObject];
+ structureLoaded = NO;
+ contentLoaded = NO;
+ statusLoaded = YES;
+ }
+ else {
+ statusLoaded = NO;
+ structureLoaded = NO;
+ contentLoaded = NO;
}
- }
- } 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");
+
+ // Set window title
+ [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@/%@/%@", [tableDocumentInstance mySQLVersion],
+ [tableDocumentInstance name], [tableDocumentInstance database], anObject]];
+ }
+ else {
+ // Error while renaming
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 ) {
- [tableContentInstance loadTable:anObject];
- structureLoaded = NO;
- 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]]);
- }
+ @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
-*/
+/**
+ * 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:)] ) {
@@ -565,28 +627,19 @@ traps enter and esc and edit/cancel without entering next row
}
}
+/**
+ * Table view delegate method
+ */
- (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)
+ // 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] ) {
+ // We have to be sure that TableSource and TableContent have finished editing
if ( ![tableSourceInstance saveRowOnDeselect] || ![tableContentInstance saveRowOnDeselect] ) {
return NO;
} else {
@@ -606,7 +659,7 @@ traps enter and esc and edit/cancel without entering next row
// If encoding is set to Autodetect, update the connection character set encoding
// based on the newly selected table's encoding - but only if it differs from the current encoding.
- if ([[[NSUserDefaults standardUserDefaults] objectForKey:@"encoding"] isEqualToString:@"Autodetect"]) {
+ if ([[[NSUserDefaults standardUserDefaults] objectForKey:@"DefaultEncoding"] isEqualToString:@"Autodetect"]) {
if (![[tableDataInstance tableEncoding] isEqualToString:[tableDocumentInstance connectionEncoding]]) {
[tableDocumentInstance setConnectionEncoding:[tableDataInstance tableEncoding] reloadingViews:NO];
[tableDataInstance resetAllData];
@@ -635,8 +688,8 @@ traps enter and esc and edit/cancel without entering next row
}
// set window title
- [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@@%@/%@/%@", [tableDocumentInstance mySQLVersion], [tableDocumentInstance user],
- [tableDocumentInstance host], [tableDocumentInstance database], [tables objectAtIndex:[tablesListView selectedRow]]]];
+ [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@/%@/%@", [tableDocumentInstance mySQLVersion],
+ [tableDocumentInstance name], [tableDocumentInstance database], [tables objectAtIndex:[tablesListView selectedRow]]]];
} else {
[tableSourceInstance loadTable:nil];
[tableContentInstance loadTable:nil];
@@ -646,26 +699,31 @@ traps enter and esc and edit/cancel without entering next row
statusLoaded = NO;
// set window title
- [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@@%@/%@", [tableDocumentInstance mySQLVersion], [tableDocumentInstance user],
- [tableDocumentInstance host], [tableDocumentInstance database]]];
+ [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@/%@", [tableDocumentInstance mySQLVersion],
+ [tableDocumentInstance name], [tableDocumentInstance database]]];
}
}
+/**
+ * Table view delegate method
+ */
- (BOOL)tableView:(NSTableView *)aTableView shouldSelectRow:(int)rowIndex
{
return (rowIndex != 0);
}
-
+/**
+ * Table view delegate method
+ */
- (BOOL)tableView:(NSTableView *)aTableView isGroupRow:(int)row
{
return (row == 0);
}
-- (void)tableView:(NSTableView *)aTableView
- willDisplayCell:(id)aCell
- forTableColumn:(NSTableColumn *)aTableColumn
- row:(int)rowIndex
+/**
+ * Table view delegate method
+ */
+- (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex
{
if (rowIndex > 0 && [[aTableColumn identifier] isEqualToString:@"tables"]) {
if ([[tableTypes objectAtIndex:rowIndex] intValue] == SP_TABLETYPE_VIEW) {
@@ -673,36 +731,28 @@ traps enter and esc and edit/cancel without entering next row
} else {
[(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]]];
- }
+ [(ImageAndTextCell*)aCell setIndentationLevel:1];
+ [(ImageAndTextCell*)aCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
} else {
[(ImageAndTextCell*)aCell setImage:nil];
[(ImageAndTextCell*)aCell setIndentationLevel:0];
}
}
+/**
+ * Table view delegate method
+ */
- (float)tableView:(NSTableView *)tableView heightOfRow:(int)row
{
- if (row == 0) {
- return 18;
- } else {
- return 17;
- }
+ return (row == 0) ? 18 : 17;
}
-#pragma mark -
#pragma mark TabView delegate methods
-/*
-loads structure or source if tab selected the first time
-*/
+/**
+ * Loads structure or source if tab selected the first time
+ */
- (void)tabView:(NSTabView *)aTabView didSelectTabViewItem:(NSTabViewItem *)tabViewItem
{
if ( [tablesListView numberOfSelectedRows] == 1 ) {
@@ -722,37 +772,51 @@ loads structure or source if tab selected the first time
statusLoaded = YES;
}
}
-/*
- if ( [tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 3 ) {
- [tableDumpInstance reloadTables:self];
- }
-*/
}
-#pragma mark -
-//last but not least
+/**
+ * Menu item interface validation
+ */
+- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
+{
+ // popup button below table list
+ if ([menuItem action] == @selector(copyTable:) ||
+ [menuItem action] == @selector(removeTable:))
+ {
+ return [tablesListView numberOfSelectedRows] > 0;
+ }
+
+ return [super validateMenuItem:menuItem];
+}
+
+#pragma mark Other
+
+/**
+ * Standard init method. Performs various ivar initialisations.
+ */
- (id)init
{
- self = [super init];
+ if ((self = [super init])) {
+ tables = [[NSMutableArray alloc] init];
+ tableTypes = [[NSMutableArray alloc] init];
+ structureLoaded = NO;
+ contentLoaded = NO;
+ statusLoaded = NO;
+ [tables addObject:NSLocalizedString(@"TABLES",@"header for table list")];
+ }
- tables = [[NSMutableArray alloc] init];
- tableTypes = [[NSMutableArray alloc] init];
- structureLoaded = NO;
- contentLoaded = NO;
- statusLoaded = NO;
- [tables addObject:NSLocalizedString(@"TABLES",@"header for table list")];
return self;
}
+/**
+ * Standard dealloc method.
+ */
- (void)dealloc
-{
-// NSLog(@"TableList dealloc");
-
- [tables release];
- [tableTypes release];
+{
+ [tables release], tables = nil;
+ [tableTypes release], tableTypes = nil;
[super dealloc];
}
-
@end