diff options
author | rowanbeentje <rowan@beent.je> | 2009-04-19 14:34:30 +0000 |
---|---|---|
committer | rowanbeentje <rowan@beent.je> | 2009-04-19 14:34:30 +0000 |
commit | d4dd7e79ce8373fe94521da2294a076887758ee2 (patch) | |
tree | 85d6828f2131b3e14afee0bcb0d88d09cb23fa1e /Source | |
parent | 39adddc081ea010756886ce5819ec4565469d893 (diff) | |
download | sequelpro-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')
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 |