diff options
author | stuconnolly <stuart02@gmail.com> | 2012-05-02 13:12:38 +0000 |
---|---|---|
committer | stuconnolly <stuart02@gmail.com> | 2012-05-02 13:12:38 +0000 |
commit | 5d87f0f50fc90c7ed47ff82b35f07b2749262132 (patch) | |
tree | 37ab959b7385c8f5aaf2d1465e1f19fbd47247c7 /Source | |
parent | dea294a90f9bf6017986f9c950991a7fc7c9645e (diff) | |
parent | fe555b6d511a51f3bdfb5c0a2b00a3206993076b (diff) | |
download | sequelpro-5d87f0f50fc90c7ed47ff82b35f07b2749262132.tar.gz sequelpro-5d87f0f50fc90c7ed47ff82b35f07b2749262132.tar.bz2 sequelpro-5d87f0f50fc90c7ed47ff82b35f07b2749262132.zip |
Merge outline view branch into trunk.
Adds support for managing and grouping favorites into folders in the connection view and removes the associated favorites management from the preferences window.
NOTE: On first launch your connection favorites will be migrated from Sequel Pro's preference file to a new file in ~/Application Support/Sequel Pro/Data. Your old favorites will remain in the preference file until removed in a future version.
Outstanding known issues:
- Removing a group node with no child favorites presents a warning about also removing the non-existent favorites.
- Starting the application with no favorites, creating a group node then selecting, hides the connection details input. Doesn't support emoty selection.
- Setting the name of a connection, adding it to the favorites and then swicthing to a different connection type, screws with the favorite name.
- The preservation between launches of whether group nodes are collapsed or not is currently not supported.
Diffstat (limited to 'Source')
46 files changed, 3914 insertions, 2386 deletions
diff --git a/Source/ImageAndTextCell.m b/Source/ImageAndTextCell.m index e3c835f0..61c37564 100644 --- a/Source/ImageAndTextCell.m +++ b/Source/ImageAndTextCell.m @@ -167,12 +167,10 @@ - (NSSize)cellSize { NSSize cellSize = [super cellSize]; + cellSize.width += (image ? [image size].width : 0) + ((1 - MIN(1,INDENT_AMOUNT)) * 3) + (INDENT_AMOUNT * _indentationLevel) + 2; - // TODO : this has to be generalized yet - if (image != nil) - cellSize.height += 2; - else - cellSize.height += 8; + cellSize.height += image ? 2 : 8; + return cellSize; } diff --git a/Source/SPAppController.m b/Source/SPAppController.m index 5686854b..da25b07f 100644 --- a/Source/SPAppController.m +++ b/Source/SPAppController.m @@ -94,7 +94,7 @@ YY_BUFFER_STATE yy_scan_string (const char *); [[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"PreferenceDefaults" ofType:@"plist"]]]; // Migrate old connection favorites to the app's support directory (only uncomment when ready) - //SPMigrateConnectionFavoritesData(); + SPMigrateConnectionFavoritesData(); } /** diff --git a/Source/SPArrayAdditions.h b/Source/SPArrayAdditions.h index abeec095..5d8114ef 100644 --- a/Source/SPArrayAdditions.h +++ b/Source/SPArrayAdditions.h @@ -77,6 +77,7 @@ static inline void NSMutableArrayReplaceObject(NSArray *self, CFIndex idx, id an - (NSString *)componentsJoinedByPeriodAndBacktickQuoted; - (NSString *)componentsJoinedByPeriodAndBacktickQuotedAndIgnoreFirst; - (NSString *)componentsJoinedAsCSV; + - (NSArray *)subarrayWithIndexes:(NSIndexSet *)indexes; @end diff --git a/Source/SPCategoryAdditions.h b/Source/SPCategoryAdditions.h index eac323a2..90c29d95 100644 --- a/Source/SPCategoryAdditions.h +++ b/Source/SPCategoryAdditions.h @@ -30,6 +30,7 @@ */ #import "SPArrayAdditions.h" +#import "SPMutableArrayAdditions.h" #import "SPStringAdditions.h" #import "SPObjectAdditions.h" #import "SPTextViewAdditions.h" diff --git a/Source/SPConnectionController.h b/Source/SPConnectionController.h index bc593685..eeb7f2d0 100644 --- a/Source/SPConnectionController.h +++ b/Source/SPConnectionController.h @@ -27,12 +27,18 @@ #import <SPMySQL/SPMySQLConnectionDelegate.h> #ifndef SP_REFACTOR /* headers */ -#import "SPFavoritesOutlineView.h" #endif -@class SPDatabaseDocument, SPSSHTunnel, SPKeychain, SPMySQLConnection +@class SPDatabaseDocument, + SPFavoritesController, + SPSSHTunnel, + SPTreeNode, + SPFavoritesOutlineView, + SPMySQLConnection #ifndef SP_REFACTOR /* class decl */ -, BWAnchoredButtonBar, SPFavoriteNode + ,SPKeychain, + BWAnchoredButtonBar, + SPFavoriteNode #endif ; @@ -44,36 +50,31 @@ @end -@interface SPFlippedView : NSView - -- (BOOL)isFlipped; - -@end #endif @interface SPConnectionController : NSViewController <SPMySQLConnectionDelegate> { id <SPConnectionControllerDelegateProtocol, NSObject> delegate; - SPDatabaseDocument *tableDocument; + SPDatabaseDocument *dbDocument; + SPSSHTunnel *sshTunnel; + SPMySQLConnection *mySQLConnection; + #ifndef SP_REFACTOR /* ivars */ + SPKeychain *keychain; NSView *databaseConnectionSuperview; NSSplitView *databaseConnectionView; NSOpenPanel *keySelectionPanel; #endif - SPKeychain *keychain; NSUserDefaults *prefs; -#ifndef SP_REFACTOR NSMutableArray *favorites; -#endif - SPSSHTunnel *sshTunnel; - SPMySQLConnection *mySQLConnection; + #ifndef SP_REFACTOR /* ivars */ BOOL automaticFavoriteSelection; -#endif BOOL cancellingConnection; BOOL isConnecting; -#ifndef SP_REFACTOR /* ivars */ + + // Standard details NSInteger previousType; #endif NSInteger type; @@ -84,37 +85,39 @@ NSString *database; NSString *socket; NSString *port; - int useSSL; - int sslKeyFileLocationEnabled; + + // SSL details + NSInteger useSSL; + NSInteger sslKeyFileLocationEnabled; NSString *sslKeyFileLocation; - int sslCertificateFileLocationEnabled; + NSInteger sslCertificateFileLocationEnabled; NSString *sslCertificateFileLocation; - int sslCACertFileLocationEnabled; + NSInteger sslCACertFileLocationEnabled; NSString *sslCACertFileLocation; + + // SSH details NSString *sshHost; NSString *sshUser; NSString *sshPassword; - int sshKeyLocationEnabled; + NSInteger sshKeyLocationEnabled; NSString *sshKeyLocation; NSString *sshPort; -#ifndef SP_REFACTOR /* ivars */ - @private NSString *favoritesPBoardType; -#endif NSString *connectionKeychainID; NSString *connectionKeychainItemName; +#ifndef SP_REFACTOR /* ivars */ NSString *connectionKeychainItemAccount; NSString *connectionSSHKeychainItemName; NSString *connectionSSHKeychainItemAccount; -#ifndef SP_REFACTOR /* ivars */ NSMutableArray *nibObjectsToRelease; IBOutlet NSView *connectionView; IBOutlet NSSplitView *connectionSplitView; IBOutlet NSScrollView *connectionDetailsScrollView; + IBOutlet NSTextField *connectionInstructionsTextField; IBOutlet BWAnchoredButtonBar *connectionSplitViewButtonBar; - IBOutlet SPFavoritesOutlineView *favoritesTable; + IBOutlet SPFavoritesOutlineView *favoritesOutlineView; IBOutlet NSWindow *errorDetailWindow; IBOutlet NSTextView *errorDetailText; @@ -130,8 +133,14 @@ IBOutlet NSView *sslCertificateLocationHelp; IBOutlet NSView *sslCACertLocationHelp; + IBOutlet NSTextField *standardNameField; + IBOutlet NSTextField *sshNameField; + IBOutlet NSTextField *socketNameField; IBOutlet NSTextField *standardSQLHostField; IBOutlet NSTextField *sshSQLHostField; + IBOutlet NSTextField *standardUserField; + IBOutlet NSTextField *socketUserField; + IBOutlet NSTextField *sshUserField; IBOutlet NSSecureTextField *standardPasswordField; IBOutlet NSSecureTextField *socketPasswordField; IBOutlet NSSecureTextField *sshPasswordField; @@ -150,15 +159,22 @@ IBOutlet NSProgressIndicator *progressIndicator; IBOutlet NSTextField *progressIndicatorText; IBOutlet NSMenuItem *favoritesSortByMenuItem; + IBOutlet NSView *exportPanelAccessoryView; + BOOL isEditing; BOOL reverseFavoritesSort; #endif - BOOL mySQLConnectionCancelled; + BOOL favoriteNameFieldWasTouched; + #ifndef SP_REFACTOR /* ivars */ - SPFavoritesSortItem previousSortItem, currentSortItem; + NSArray *draggedNodes; + NSImage *folderImage; - SPFavoriteNode *favoritesRoot; + SPTreeNode *favoritesRoot; + SPFavoriteNode *currentFavorite; + SPFavoritesController *favoritesController; + SPFavoritesSortItem previousSortItem, currentSortItem; #endif } @@ -171,64 +187,66 @@ @property (readwrite, retain) NSString *database; @property (readwrite, retain) NSString *socket; @property (readwrite, retain) NSString *port; -@property (readwrite, assign) int useSSL; -@property (readwrite, assign) int sslKeyFileLocationEnabled; +@property (readwrite, assign) NSInteger useSSL; +@property (readwrite, assign) NSInteger sslKeyFileLocationEnabled; @property (readwrite, retain) NSString *sslKeyFileLocation; -@property (readwrite, assign) int sslCertificateFileLocationEnabled; +@property (readwrite, assign) NSInteger sslCertificateFileLocationEnabled; @property (readwrite, retain) NSString *sslCertificateFileLocation; -@property (readwrite, assign) int sslCACertFileLocationEnabled; +@property (readwrite, assign) NSInteger sslCACertFileLocationEnabled; @property (readwrite, retain) NSString *sslCACertFileLocation; @property (readwrite, retain) NSString *sshHost; @property (readwrite, retain) NSString *sshUser; @property (readwrite, retain) NSString *sshPassword; -@property (readwrite, assign) int sshKeyLocationEnabled; +@property (readwrite, assign) NSInteger sshKeyLocationEnabled; @property (readwrite, retain) NSString *sshKeyLocation; @property (readwrite, retain) NSString *sshPort; +#ifndef SP_REFACTOR /* ivars */ + @property (readwrite, retain) NSString *connectionKeychainItemName; @property (readwrite, retain) NSString *connectionKeychainItemAccount; @property (readwrite, retain) NSString *connectionSSHKeychainItemName; @property (readwrite, retain) NSString *connectionSSHKeychainItemAccount; - -@property (readonly, assign) BOOL isConnecting; -#ifndef SP_REFACTOR /* ivars */ -@property (readonly, assign) NSString *favoritesPBoardType; #endif -- (id)initWithDocument:(SPDatabaseDocument *)theTableDocument; +@property (readonly, assign) BOOL isConnecting; // Connection processes - (IBAction)initiateConnection:(id)sender; +#ifndef SP_REFACTOR /* method decls */ - (IBAction)cancelMySQLConnection:(id)sender; -- (void)initiateSSHTunnelConnection; -- (void)sshTunnelCallback:(SPSSHTunnel *)theTunnel; -- (void)initiateMySQLConnection; -- (void)cancelConnection; -- (void)failConnectionWithTitle:(NSString *)theTitle errorMessage:(NSString *)theErrorMessage detail:(NSString *)errorDetail; -- (void)addConnectionToDocument; // Interface interaction -- (IBAction)chooseKeyLocation:(NSButton *)sender; -#ifndef SP_REFACTOR /* method decls */ -- (IBAction)editFavorites:(id)sender; +- (IBAction)nodeDoubleClicked:(id)sender; +- (IBAction)chooseKeyLocation:(id)sender; - (IBAction)showHelp:(id)sender; - (IBAction)updateSSLInterface:(id)sender; - (IBAction)updateKeyLocationFileVisibility:(id)sender; - (void)resizeTabViewToConnectionType:(NSUInteger)theType animating:(BOOL)animate; - (IBAction)sortFavorites:(id)sender; - (IBAction)reverseSortFavorites:(NSMenuItem *)sender; -#endif -// Connection details interaction -- (BOOL)checkHost; +- (void)resizeTabViewToConnectionType:(NSUInteger)theType animating:(BOOL)animate; -#ifndef SP_REFACTOR // Favorites interaction -- (void)updateFavorites; - (void)updateFavoriteSelection:(id)sender; -- (id)selectedFavorite; -- (IBAction)addFavorite:(id)sender; +- (NSMutableDictionary *)selectedFavorite; +- (SPTreeNode *)selectedFavoriteNode; +- (NSArray *)selectedFavoriteNodes; -- (void)scrollViewFrameChanged:(NSNotification *)aNotification; +- (IBAction)addFavorite:(id)sender; +- (IBAction)addFavoriteUsingCurrentDetails:(id)sender; +- (IBAction)addGroup:(id)sender; +- (IBAction)removeNode:(id)sender; +- (IBAction)duplicateFavorite:(id)sender; +- (IBAction)renameNode:(id)sender; +- (IBAction)makeSelectedFavoriteDefault:(id)sender; + +// Import/export favorites +- (IBAction)importFavorites:(id)sender; +- (IBAction)exportFavorites:(id)sender; + +// Accessors +- (SPFavoritesOutlineView *)favoritesOutlineView; #endif @end diff --git a/Source/SPConnectionController.m b/Source/SPConnectionController.m index 5575d8b7..6b43f01a 100644 --- a/Source/SPConnectionController.m +++ b/Source/SPConnectionController.m @@ -25,40 +25,53 @@ #import "SPConnectionController.h" #import "SPDatabaseDocument.h" -#import <SPMySQL/SPMySQL.h> - -#ifndef SP_REFACTOR /* headers */ #import "SPAppController.h" #import "SPPreferenceController.h" #import "ImageAndTextCell.h" #import "RegexKitLite.h" -#endif #import "SPAlertSheets.h" #import "SPKeychain.h" -#ifndef SP_REFACTOR /* headers */ -#import "SPFavoritesPreferencePane.h" -#endif #import "SPSSHTunnel.h" -#ifndef SP_REFACTOR /* headers */ -#import "SPFavoriteNode.h" #import "SPTableTextFieldCell.h" +#import "SPFavoritesController.h" +#import "SPFavoriteNode.h" #import "SPGeneralPreferencePane.h" #import "SPDatabaseViewController.h" -#endif +#import "SPTreeNode.h" +#import "SPFavoritesExporter.h" +#import "SPFavoritesImporter.h" + +#import <SPMySQL/SPMySQL.h> + +// Constants +static NSString *SPRemoveNode = @"RemoveNode"; +static NSString *SPImportFavorites = @"ImportFavorites"; +static NSString *SPExportFavorites = @"ExportFavorites"; +static NSString *SPExportFavoritesFilename = @"SequelProFavorites.plist"; @interface NSSavePanel (NSSavePanel_unpublishedUntilSnowLeopardAPI) + - (void)setShowsHiddenFiles:(BOOL)flag; + @end -@interface SPConnectionController (PrivateAPI) +@interface SPConnectionController () -#ifndef SP_REFACTOR /* @interface */ +- (BOOL)_checkHost; - (void)_sortFavorites; -- (void)_buildFavoritesTree; -#endif +- (void)_sortTreeNode:(SPTreeNode *)node usingKey:(NSString *)key; +- (void)_favoriteTypeDidChange; +- (void)_reloadFavoritesViewData; - (void)_restoreConnectionInterface; -- (void)_mySQLConnectionEstablished; -- (void)_initiateMySQLConnectionInBackground; +- (void)_selectNode:(SPTreeNode *)node; + +- (NSNumber *)_createNewFavoriteID; +- (SPTreeNode *)_favoriteNodeForFavoriteID:(NSInteger)favoriteID; +- (NSString *)_stripInvalidCharactersFromString:(NSString *)subject; + +- (void)_updateFavoritePasswordsFromField:(NSControl *)control; + +static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, void *key); @end @@ -93,130 +106,11 @@ @synthesize connectionSSHKeychainItemAccount; @synthesize isConnecting; -#ifndef SP_REFACTOR /* ivars */ -@synthesize favoritesPBoardType; -#endif - -#pragma mark - - -/** - * Initialise the connection controller, linking it to the - * parent document and setting up the parent window. - */ -- (id) initWithDocument:(SPDatabaseDocument *)theTableDocument -{ - if ((self = [super init])) { - tableDocument = theTableDocument; -#ifndef SP_REFACTOR /* ivars */ - databaseConnectionSuperview = [tableDocument databaseView]; - databaseConnectionView = [tableDocument valueForKey:@"contentViewSplitter"]; -#endif - connectionKeychainID = nil; - connectionKeychainItemName = nil; - connectionKeychainItemAccount = nil; - connectionSSHKeychainItemName = nil; - connectionSSHKeychainItemAccount = nil; - mySQLConnection = nil; - sshTunnel = nil; - cancellingConnection = NO; - isConnecting = NO; - mySQLConnectionCancelled = NO; -#ifndef SP_REFACTOR /* ui init */ - favoritesPBoardType = @"FavoritesPBoardType"; - - // Load the connection nib, keeping references to the top-level objects for later release - nibObjectsToRelease = [[NSMutableArray alloc] init]; - NSArray *connectionViewTopLevelObjects = nil; - NSNib *nibLoader = [[NSNib alloc] initWithNibNamed:@"ConnectionView" bundle:[NSBundle mainBundle]]; - [nibLoader instantiateNibWithOwner:self topLevelObjects:&connectionViewTopLevelObjects]; - [nibObjectsToRelease addObjectsFromArray:connectionViewTopLevelObjects]; - [nibLoader release]; - - // Hide the main view and position and display the connection view - [databaseConnectionView setHidden:YES]; - [connectionView setFrame:[databaseConnectionView frame]]; - [databaseConnectionSuperview addSubview:connectionView]; - [connectionSplitView setPosition:[[tableDocument valueForKey:@"dbTablesTableView"] frame].size.width ofDividerAtIndex:0]; - [connectionSplitView setDelegate:self]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollViewFrameChanged:) name:NSViewFrameDidChangeNotification object:nil]; - - // Set up a keychain instance and preferences reference, and create the initial favorites list -#endif - keychain = [[SPKeychain alloc] init]; - prefs = [[NSUserDefaults standardUserDefaults] retain]; -#ifndef SP_REFACTOR - favorites = nil; - - // Load favorites - [self updateFavorites]; - - // Expand the favorites heading - [favoritesTable expandItem:[[favoritesRoot nodeChildren] objectAtIndex:0]]; - - // Register an observer for changes within the favorites - [prefs addObserver:self forKeyPath:SPFavorites options:NSKeyValueObservingOptionNew context:NULL]; - - // Set sort items - currentSortItem = [prefs integerForKey:SPFavoritesSortedBy]; - reverseFavoritesSort = [prefs boolForKey:SPFavoritesSortedInReverse]; - - // Register double click for the favorites view (double click favorite to connect) - [favoritesTable setTarget:self]; - [favoritesTable setDoubleAction:@selector(initiateConnection:)]; - [favoritesTable registerForDraggedTypes:[NSArray arrayWithObject:favoritesPBoardType]]; - [favoritesTable setDraggingSourceOperationMask:NSDragOperationMove forLocal:YES]; - - // Sort the favourites to match prefs and select the appropriate row - if a valid sort option is selected - if (currentSortItem != SPFavoritesSortUnsorted) [self _sortFavorites]; - - NSUInteger tableRow = [prefs integerForKey:[prefs boolForKey:SPSelectLastFavoriteUsed] ? SPLastFavoriteIndex : SPDefaultFavorite]; - - if (tableRow < [favorites count]) { - previousType = [[[favorites objectAtIndex:tableRow] objectForKey:SPFavoriteTypeKey] integerValue]; - [favoritesTable selectRowIndexes:[NSIndexSet indexSetWithIndex:(tableRow + 1)] byExtendingSelection:NO]; - [self resizeTabViewToConnectionType:[[[favorites objectAtIndex:tableRow] objectForKey:SPFavoriteTypeKey] integerValue] animating:NO]; - [favoritesTable scrollRowToVisible:[favoritesTable selectedRow]]; - } - else { - previousType = SPTCPIPConnection; - [self resizeTabViewToConnectionType:SPTCPIPConnection animating:NO]; - } -#endif - } - - return self; -} - -- (void) dealloc -{ -#ifndef SP_REFACTOR /* remove prefs observer */ - [prefs removeObserver:self forKeyPath:SPFavorites]; -#endif - [[NSNotificationCenter defaultCenter] removeObserver:self]; - [keychain release]; -#ifndef SP_REFACTOR /* dealloc ivars */ - [prefs release]; - - for (id retainedObject in nibObjectsToRelease) [retainedObject release]; - [nibObjectsToRelease release]; - - if (favorites) [favorites release]; - if (favoritesRoot) [favoritesRoot release], favoritesRoot = nil; -#endif - if (mySQLConnection) [mySQLConnection release]; - if (sshTunnel) [sshTunnel setConnectionStateChangeSelector:nil delegate:nil], [sshTunnel disconnect], [sshTunnel release]; - if (connectionKeychainID) [connectionKeychainID release]; - if (connectionKeychainItemName) [connectionKeychainItemName release]; - if (connectionKeychainItemAccount) [connectionKeychainItemAccount release]; - if (connectionSSHKeychainItemName) [connectionSSHKeychainItemName release]; - if (connectionSSHKeychainItemAccount) [connectionSSHKeychainItemAccount release]; - [super dealloc]; -} #pragma mark - #pragma mark Connection processes -/* +/** * Starts the connection process; invoked when user hits the connect button * or double-clicks on a favourite. * Error-checks fields as required, and triggers connection of MySQL or any @@ -224,22 +118,21 @@ */ - (IBAction)initiateConnection:(id)sender { - // If this action was triggered via a double-click on the favorites outline view, // ensure that one of the connections was double-clicked, not the area above or below #ifndef SP_REFACTOR - if (sender == favoritesTable && [favoritesTable clickedRow] <= 0) return; + if (sender == favoritesOutlineView && [favoritesOutlineView clickedRow] <= 0) return; #endif // Ensure that host is not empty if this is a TCP/IP or SSH connection if (([self type] == SPTCPIPConnection || [self type] == SPSSHTunnelConnection) && ![[self host] length]) { - SPBeginAlertSheet(NSLocalizedString(@"Insufficient connection details", @"insufficient details message"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocument parentWindow], self, nil, nil, NSLocalizedString(@"Insufficient details provided to establish a connection. Please enter at least the hostname.", @"insufficient details informative message")); + SPBeginAlertSheet(NSLocalizedString(@"Insufficient connection details", @"insufficient details message"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [dbDocument parentWindow], self, nil, nil, NSLocalizedString(@"Insufficient details provided to establish a connection. Please enter at least the hostname.", @"insufficient details informative message")); return; } - + // If SSH is enabled, ensure that the SSH host is not nil if ([self type] == SPSSHTunnelConnection && ![[self sshHost] length]) { - SPBeginAlertSheet(NSLocalizedString(@"Insufficient connection details", @"insufficient details message"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocument parentWindow], self, nil, nil, NSLocalizedString(@"Insufficient details provided to establish a connection. Please enter the hostname for the SSH Tunnel, or disable the SSH Tunnel.", @"insufficient SSH tunnel details informative message")); + SPBeginAlertSheet(NSLocalizedString(@"Insufficient connection details", @"insufficient details message"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [dbDocument parentWindow], self, nil, nil, NSLocalizedString(@"Insufficient details provided to establish a connection. Please enter the hostname for the SSH Tunnel, or disable the SSH Tunnel.", @"insufficient SSH tunnel details informative message")); return; } @@ -247,38 +140,47 @@ if ([self type] == SPSSHTunnelConnection && sshKeyLocationEnabled && sshKeyLocation) { if (![[NSFileManager defaultManager] fileExistsAtPath:[sshKeyLocation stringByExpandingTildeInPath]]) { [self setSshKeyLocationEnabled:NSOffState]; - SPBeginAlertSheet(NSLocalizedString(@"SSH Key not found", @"SSH key check error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocument parentWindow], self, nil, nil, NSLocalizedString(@"A SSH key location was specified, but no file was found in the specified location. Please re-select the key and try again.", @"SSH key not found message")); + SPBeginAlertSheet(NSLocalizedString(@"SSH Key not found", @"SSH key check error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [dbDocument parentWindow], self, nil, nil, NSLocalizedString(@"A SSH key location was specified, but no file was found in the specified location. Please re-select the key and try again.", @"SSH key not found message")); return; } } // Ensure that a socket connection is not inadvertently used - if (![self checkHost]) return; + if (![self _checkHost]) return; // If SSL keys have been supplied, verify they exist if (([self type] == SPTCPIPConnection || [self type] == SPSocketConnection) && [self useSSL]) { - if (sslKeyFileLocationEnabled && sslKeyFileLocation - && ![[NSFileManager defaultManager] fileExistsAtPath:[sslKeyFileLocation stringByExpandingTildeInPath]]) + + if (sslKeyFileLocationEnabled && sslKeyFileLocation && + ![[NSFileManager defaultManager] fileExistsAtPath:[sslKeyFileLocation stringByExpandingTildeInPath]]) { [self setSslKeyFileLocationEnabled:NSOffState]; [self setSslKeyFileLocation:nil]; - SPBeginAlertSheet(NSLocalizedString(@"SSL Key File not found", @"SSL key file check error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocument parentWindow], self, nil, nil, NSLocalizedString(@"A SSL key file location was specified, but no file was found in the specified location. Please re-select the key file and try again.", @"SSL key file not found message")); + + SPBeginAlertSheet(NSLocalizedString(@"SSL Key File not found", @"SSL key file check error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [dbDocument parentWindow], self, nil, nil, NSLocalizedString(@"A SSL key file location was specified, but no file was found in the specified location. Please re-select the key file and try again.", @"SSL key file not found message")); + return; } - if (sslCertificateFileLocationEnabled && sslCertificateFileLocation - && ![[NSFileManager defaultManager] fileExistsAtPath:[sslCertificateFileLocation stringByExpandingTildeInPath]]) + + if (sslCertificateFileLocationEnabled && sslCertificateFileLocation && + ![[NSFileManager defaultManager] fileExistsAtPath:[sslCertificateFileLocation stringByExpandingTildeInPath]]) { [self setSslCertificateFileLocationEnabled:NSOffState]; [self setSslCertificateFileLocation:nil]; - SPBeginAlertSheet(NSLocalizedString(@"SSL Certificate File not found", @"SSL certificate file check error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocument parentWindow], self, nil, nil, NSLocalizedString(@"A SSL certificate location was specified, but no file was found in the specified location. Please re-select the certificate and try again.", @"SSL certificate file not found message")); + + SPBeginAlertSheet(NSLocalizedString(@"SSL Certificate File not found", @"SSL certificate file check error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [dbDocument parentWindow], self, nil, nil, NSLocalizedString(@"A SSL certificate location was specified, but no file was found in the specified location. Please re-select the certificate and try again.", @"SSL certificate file not found message")); + return; } - if (sslCACertFileLocationEnabled && sslCACertFileLocation - && ![[NSFileManager defaultManager] fileExistsAtPath:[sslCACertFileLocation stringByExpandingTildeInPath]]) + + if (sslCACertFileLocationEnabled && sslCACertFileLocation && + ![[NSFileManager defaultManager] fileExistsAtPath:[sslCACertFileLocation stringByExpandingTildeInPath]]) { [self setSslCACertFileLocationEnabled:NSOffState]; [self setSslCACertFileLocation:nil]; - SPBeginAlertSheet(NSLocalizedString(@"SSL Certificate Authority File not found", @"SSL certificate authority file check error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocument parentWindow], self, nil, nil, NSLocalizedString(@"A SSL Certificate Authority certificate location was specified, but no file was found in the specified location. Please re-select the Certificate Authority certificate and try again.", @"SSL CA certificate file not found message")); + + SPBeginAlertSheet(NSLocalizedString(@"SSL Certificate Authority File not found", @"SSL certificate authority file check error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [dbDocument parentWindow], self, nil, nil, NSLocalizedString(@"A SSL Certificate Authority certificate location was specified, but no file was found in the specified location. Please re-select the Certificate Authority certificate and try again.", @"SSL CA certificate file not found message")); + return; } } @@ -286,20 +188,18 @@ // Basic details have validated - start the connection process animating isConnecting = YES; cancellingConnection = NO; - -#ifndef SP_REFACTOR + // Disable the favorites outline view to prevent further connections attempts - [favoritesTable setEnabled:NO]; - + [favoritesOutlineView setEnabled:NO]; + [addToFavoritesButton setHidden:YES]; [helpButton setHidden:YES]; [connectButton setEnabled:NO]; [progressIndicator startAnimation:self]; [progressIndicatorText setHidden:NO]; -#endif - + // Start the current tab's progress indicator - [tableDocument setIsProcessing:YES]; + [dbDocument setIsProcessing:YES]; // If the password(s) are marked as having been originally sourced from a keychain, check whether they // have been changed or not; if not, leave the mark in place and remove the password from the field @@ -307,43 +207,44 @@ if (connectionKeychainItemName) { if ([[keychain getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount] isEqualToString:[self password]]) { [self setPassword:[[NSString string] stringByPaddingToLength:[[self password] length] withString:@"sp" startingAtIndex:0]]; -#ifndef SP_REFACTOR + [[standardPasswordField undoManager] removeAllActionsWithTarget:standardPasswordField]; [[socketPasswordField undoManager] removeAllActionsWithTarget:socketPasswordField]; [[sshPasswordField undoManager] removeAllActionsWithTarget:sshPasswordField]; -#endif - } else { + } + else { [connectionKeychainItemName release], connectionKeychainItemName = nil; [connectionKeychainItemAccount release], connectionKeychainItemAccount = nil; } } + if (connectionSSHKeychainItemName) { if ([[keychain getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount] isEqualToString:[self sshPassword]]) { [self setSshPassword:[[NSString string] stringByPaddingToLength:[[self sshPassword] length] withString:@"sp" startingAtIndex:0]]; -#ifndef SP_REFACTOR [[sshSSHPasswordField undoManager] removeAllActionsWithTarget:sshSSHPasswordField]; -#endif - } else { + } + else { [connectionSSHKeychainItemName release], connectionSSHKeychainItemName = nil; [connectionSSHKeychainItemAccount release], connectionSSHKeychainItemAccount = nil; } } - + // Inform the delegate that we are starting the connection process if (delegate && [delegate respondsToSelector:@selector(connectionControllerInitiatingConnection:)]) { [delegate connectionControllerInitiatingConnection:self]; } - + // Trim whitespace and newlines from the host field before attempting to connect [self setHost:[[self host] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]]; - + // Initiate the SSH connection process for tunnels if ([self type] == SPSSHTunnelConnection) { [self performSelector:@selector(initiateSSHTunnelConnection) withObject:nil afterDelay:0.0]; + return; } - - // ...or start the MySQL connection process directly + + // ...or start the MySQL connection process directly [self performSelector:@selector(initiateMySQLConnection) withObject:nil afterDelay:0.0]; } @@ -356,264 +257,40 @@ */ - (IBAction)cancelMySQLConnection:(id)sender { -#ifndef SP_REFACTOR [connectButton setEnabled:NO]; - + [progressIndicatorText setStringValue:NSLocalizedString(@"Cancelling...", @"cancelling task status message")]; [progressIndicatorText display]; -#endif - + mySQLConnectionCancelled = YES; } -/* - * Initiate the SSH connection process. - * This should only be called as part of initiateConnection:, and will indirectly - * call initiateMySQLConnection if it's successful. - */ -- (void)initiateSSHTunnelConnection -{ -#ifndef SP_REFACTOR - [progressIndicatorText setStringValue:NSLocalizedString(@"SSH connecting...", @"SSH connecting very short status message")]; - [progressIndicatorText display]; -#endif - - // Trim whitespace and newlines from the SSH host field before attempting to connect - [self setSshHost:[[self sshHost] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]]; - - // Set up the tunnel details - sshTunnel = [[SPSSHTunnel alloc] initToHost:[self sshHost] port:[[self sshPort] integerValue] login:[self sshUser] tunnellingToPort:([[self port] length]?[[self port] integerValue]:3306) onHost:[self host]]; - [sshTunnel setParentWindow:[tableDocument parentWindow]]; - - // Add keychain or plaintext password as appropriate - note the checks in initiateConnection. - if (connectionSSHKeychainItemName) { - [sshTunnel setPasswordKeychainName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]; - } else if (sshPassword) { - [sshTunnel setPassword:[self sshPassword]]; - } - - // Set the public key path if appropriate - if (sshKeyLocationEnabled && sshKeyLocation) { - [sshTunnel setKeyFilePath:sshKeyLocation]; - } - - // Set the callback function on the tunnel - [sshTunnel setConnectionStateChangeSelector:@selector(sshTunnelCallback:) delegate:self]; - - // Ask the tunnel to connect. This will call the callback below on success or failure, passing - // itself as an argument - retain count should be one at this point. - [sshTunnel connect]; -} - -/* - * Cancel connection. - * Currently only cleans up the SSH connection (MySQL connection isn't threaded) - */ -- (void)cancelConnection -{ - if (!sshTunnel) return; - - cancellingConnection = YES; - - [sshTunnel disconnect]; - [sshTunnel release]; - - sshTunnel = nil; -} +#pragma mark - +#pragma mark Interface interaction -/* - * A callback function for the SSH Tunnel setup process - will be called on a connection - * state change, allowing connection to fail or proceed as appropriate. If successful, - * will call initiateMySQLConnection. +/** + * Registered in initWithDocument: to be the double click action of the favorites outline view. */ -- (void)sshTunnelCallback:(SPSSHTunnel *)theTunnel +- (IBAction)nodeDoubleClicked:(id)sender { - if (cancellingConnection) return; - - NSInteger newState = [theTunnel state]; + SPTreeNode *node = [self selectedFavoriteNode]; - // If the user cancelled the password prompt dialog - if ([theTunnel passwordPromptCancelled]) { - [self _restoreConnectionInterface]; - - return; + // Only proceed to initiate a connection if a leaf node (i.e. a favorite and not a group) was double clicked. + if (![node isGroup]) { + [self initiateConnection:self]; } - - if (newState == SPMySQLProxyIdle) { -#ifndef SP_REFACTOR - [tableDocument setTitlebarStatus:NSLocalizedString(@"SSH Disconnected", @"SSH disconnected titlebar marker")]; -#endif - - [self failConnectionWithTitle:NSLocalizedString(@"SSH connection failed!", @"SSH connection failed title") errorMessage:[theTunnel lastError] detail:[sshTunnel debugMessages]]; - [self _restoreConnectionInterface]; - } - else if (newState == SPMySQLProxyConnected) { -#ifndef SP_REFACTOR - [tableDocument setTitlebarStatus:NSLocalizedString(@"SSH Connected", @"SSH connected titlebar marker")]; -#endif - - [self initiateMySQLConnection]; - } + // Otherwise start editing the group node's name else { -#ifndef SP_REFACTOR - [tableDocument setTitlebarStatus:NSLocalizedString(@"SSH Connecting…", @"SSH connecting titlebar marker")]; -#endif + [favoritesOutlineView editColumn:0 row:[favoritesOutlineView selectedRow] withEvent:nil select:YES]; } } - -/* - * Set up the MySQL connection, either through a successful tunnel or directly in the background. - */ -- (void)initiateMySQLConnection -{ -#ifndef SP_REFACTOR /* ui manipulation */ - if (sshTunnel) - [progressIndicatorText setStringValue:NSLocalizedString(@"MySQL connecting...", @"MySQL connecting very short status message")]; - else - [progressIndicatorText setStringValue:NSLocalizedString(@"Connecting...", @"Generic connecting very short status message")]; - - [progressIndicatorText display]; - - [connectButton setTitle:NSLocalizedString(@"Cancel", @"cancel button")]; - [connectButton setAction:@selector(cancelMySQLConnection:)]; - [connectButton setEnabled:YES]; - [connectButton display]; -#endif - [NSThread detachNewThreadSelector:@selector(_initiateMySQLConnectionInBackground) toTarget:self withObject:nil]; -} - -/* - * Ends a connection attempt by stopping the connection animation and - * displaying a specified error message. - */ -- (void)failConnectionWithTitle:(NSString *)theTitle errorMessage:(NSString *)theErrorMessage detail:(NSString *)errorDetail -{ - BOOL isSSHTunnelBindError = NO; - -#ifndef SP_REFACTOR /* ui manipulation */ - // Clean up the interface - [progressIndicator stopAnimation:self]; - [progressIndicator display]; - [progressIndicatorText setHidden:YES]; - [progressIndicatorText display]; - [addToFavoritesButton setHidden:NO]; - [addToFavoritesButton display]; - [connectButton setEnabled:YES]; - [tableDocument clearStatusIcon]; -#endif - - // Release as appropriate - if (sshTunnel) { - [sshTunnel disconnect], [sshTunnel release], sshTunnel = nil; - - // If the SSH tunnel connection failed because the port it was trying to bind to was already in use take note - // of it so we can give the user the option of connecting via standard connection and use the existing tunnel. - if ([theErrorMessage rangeOfString:@"bind"].location != NSNotFound) { - isSSHTunnelBindError = YES; - } - } - -#ifndef SP_REFACTOR /* [errorDetailText setString:errorDetail] */ - if (errorDetail) [errorDetailText setString:errorDetail]; -#endif - - // Inform the delegate that the connection attempt failed - if (delegate && [delegate respondsToSelector:@selector(connectionControllerConnectAttemptFailed:)]) { - [delegate connectionControllerConnectAttemptFailed:self]; - } - - // Only display the connection error message if there is a window visible and the connection attempt - // wasn't cancelled even though it failed. - if ([[tableDocument parentWindow] isVisible] && (!mySQLConnectionCancelled)) { -#ifdef SP_REFACTOR - if ( errorDetail ) - NSLog(@"%@", errorDetail); - - SPBeginAlertSheet(theTitle, NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocument parentWindow], self, @selector(connectionFailureSheetDidEnd:returnCode:contextInfo:), @"connect", theErrorMessage); -#else - SPBeginAlertSheet(theTitle, NSLocalizedString(@"OK", @"OK button"), (errorDetail) ? NSLocalizedString(@"Show Detail", @"Show detail button") : nil, (isSSHTunnelBindError) ? NSLocalizedString(@"Use Standard Connection", @"use standard connection button") : nil, [tableDocument parentWindow], self, @selector(connectionFailureSheetDidEnd:returnCode:contextInfo:), @"connect", theErrorMessage); -#endif - } -} - -/** - * Alert sheet callback method - invoked when an error sheet is closed. - */ -- (void)connectionFailureSheetDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo -{ -#ifndef SP_REFACTOR /* connectionFailureSheetDidEnd: */ - // Restore the passwords from keychain for editing if appropriate - if (connectionKeychainItemName) { - [self setPassword:[keychain getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount]]; - } - if (connectionSSHKeychainItemName) { - [self setSshPassword:[keychain getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]]; - } - - if (returnCode == NSAlertAlternateReturn) { - [errorDetailText setFont:[NSFont userFontOfSize:12]]; - [errorDetailText setAlignment:NSLeftTextAlignment]; - [errorDetailWindow makeKeyAndOrderFront:self]; - } - // Currently only SSH port bind errors offer a 3rd option in the error dialog, but if this ever changes - // this will definitely need to be updated. - else if (returnCode == NSAlertOtherReturn) { - // Extract the local port number that SSH attempted to bind to from the debug output - NSString *tunnelPort = [[[errorDetailText string] componentsMatchedByRegex:@"LOCALHOST:([0-9]+)" capture:1L] lastObject]; - - // Change the connection type to standard TCP/IP - [self setType:SPTCPIPConnection]; - - // Change connection details - [self setPort:tunnelPort]; - [self setHost:@"127.0.0.1"]; - - // Change to standard TCP/IP connection view - [self resizeTabViewToConnectionType:SPTCPIPConnection animating:YES]; - - // Initiate the connection after half a second to give the connection view a chance to resize - [self performSelector:@selector(initiateConnection:) withObject:self afterDelay:0.5]; - } -#endif -} - -/** - * Add the connection to the parent document and restore the - * interface, allowing the application to run as normal. - */ -- (void)addConnectionToDocument -{ -#ifndef SP_REFACTOR /* ui manipulation */ - // Hide the connection view and restore the main view - [connectionView removeFromSuperviewWithoutNeedingDisplay]; - [databaseConnectionView setHidden:NO]; - - // Restore the toolbar icons - NSArray *toolbarItems = [[[tableDocument parentWindow] toolbar] items]; - - for (NSInteger i = 0; i < (NSInteger)[toolbarItems count]; i++) [[toolbarItems objectAtIndex:i] setEnabled:YES]; -#endif - - // Set keychain id for saving SPF files - if (connectionKeychainID) [tableDocument setKeychainID:connectionKeychainID]; - - // Pass the connection to the table document, allowing it to set - // up the other classes and the rest of the interface. - [tableDocument setConnection:mySQLConnection]; -} - -#pragma mark - -#pragma mark Interface interaction - /** * Opens the SSH/SSL key selection window, ready to select a key file. */ -- (IBAction)chooseKeyLocation:(NSButton *)sender +- (IBAction)chooseKeyLocation:(id)sender { -#ifndef SP_REFACTOR /* favorites */ - [favoritesTable deselectAll:self]; - + [favoritesOutlineView deselectAll:self]; NSString *directoryPath = nil; NSString *filePath = nil; NSArray *permittedFileTypes = nil; @@ -637,110 +314,54 @@ } permittedFileTypes = [NSArray arrayWithObjects:@"pem", @"", nil]; + [keySelectionPanel setAccessoryView:sshKeyLocationHelp]; - + } // SSL key file location: - } else if (sender == standardSSLKeyFileButton || sender == socketSSLKeyFileButton) { + else if (sender == standardSSLKeyFileButton || sender == socketSSLKeyFileButton) { if ([sender state] == NSOffState) { [self setSslKeyFileLocation:nil]; return; } + permittedFileTypes = [NSArray arrayWithObjects:@"pem", @"key", @"", nil]; + [keySelectionPanel setAccessoryView:sslKeyFileLocationHelp]; - + } // SSL certificate file location: - } else if (sender == standardSSLCertificateButton || sender == socketSSLCertificateButton) { + else if (sender == standardSSLCertificateButton || sender == socketSSLCertificateButton) { if ([sender state] == NSOffState) { [self setSslCertificateFileLocation:nil]; return; } + permittedFileTypes = [NSArray arrayWithObjects:@"pem", @"cert", @"crt", @"", nil]; + [keySelectionPanel setAccessoryView:sslCertificateLocationHelp]; - + } // SSL CA certificate file location: - } else if (sender == standardSSLCACertButton || sender == socketSSLCACertButton) { + else if (sender == standardSSLCACertButton || sender == socketSSLCACertButton) { if ([sender state] == NSOffState) { [self setSslCACertFileLocation:nil]; return; } + permittedFileTypes = [NSArray arrayWithObjects:@"pem", @"cert", @"crt", @"", nil]; + [keySelectionPanel setAccessoryView:sslCACertLocationHelp]; } [keySelectionPanel beginSheetForDirectory:directoryPath - file:filePath - types:permittedFileTypes - modalForWindow:[tableDocument parentWindow] - modalDelegate:self - didEndSelector:@selector(chooseKeyLocationSheetDidEnd:returnCode:contextInfo:) - contextInfo:sender]; -#endif -} - -/** - * Called after closing the SSH/SSL key selection sheet. - */ -- (void)chooseKeyLocationSheetDidEnd:(NSOpenPanel *)openPanel returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo -{ -#ifndef SP_REFACTOR /* !!! ssh key location */ - NSString *abbreviatedFileName = [[[openPanel URL] path] stringByAbbreviatingWithTildeInPath]; - - // SSH key file selection - if (contextInfo == sshSSHKeyButton) { - if (returnCode == NSCancelButton) { - [self setSshKeyLocationEnabled:NSOffState]; - return; - } - [self setSshKeyLocation:abbreviatedFileName]; - - // SSL key file selection - } else if (contextInfo == standardSSLKeyFileButton || contextInfo == socketSSLKeyFileButton) { - if (returnCode == NSCancelButton) { - [self setSslKeyFileLocationEnabled:NSOffState]; - [self setSslKeyFileLocation:nil]; - return; - } - [self setSslKeyFileLocation:abbreviatedFileName]; - - // SSL certificate file selection - } else if (contextInfo == standardSSLCertificateButton || contextInfo == socketSSLCertificateButton) { - if (returnCode == NSCancelButton) { - [self setSslCertificateFileLocationEnabled:NSOffState]; - [self setSslCertificateFileLocation:nil]; - return; - } - [self setSslCertificateFileLocation:abbreviatedFileName]; - - // SSL CA certificate file selection - } else if (contextInfo == standardSSLCACertButton || contextInfo == socketSSLCACertButton) { - if (returnCode == NSCancelButton) { - [self setSslCACertFileLocationEnabled:NSOffState]; - [self setSslCACertFileLocation:nil]; - return; - } - [self setSslCACertFileLocation:abbreviatedFileName]; - } -#endif -} - - -#ifndef SP_REFACTOR /* connection favorites and ssh key handling */ -/** - * 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. - */ -- (IBAction)editFavorites:(id)sender -{ - SPPreferenceController *prefsController = [[NSApp delegate] preferenceController]; - - [prefsController showWindow:self]; - [prefsController displayFavoritePreferences:self]; - - if ([favoritesTable numberOfSelectedRows]) [[prefsController favoritesPreferencePane] selectFavorites:[NSArray arrayWithObject:[self valueForKeyPath:@"selectedFavorite"]]]; + file:filePath + types:permittedFileTypes + modalForWindow:[dbDocument parentWindow] + modalDelegate:self + didEndSelector:@selector(chooseKeyLocationSheetDidEnd:returnCode:contextInfo:) + contextInfo:sender]; } /** - * Show connection help. + * Show connection help webpage. */ - (IBAction)showHelp:(id)sender { @@ -767,83 +388,6 @@ #pragma mark Connection details interaction and display /** - * Trigger a resize action whenever the tab view changes. The connection - * detail forms are held within container views, which are of a fixed width; - * the tabview and buttons are contained within a resizable view which - * is set to dimensions based on the container views, allowing the view - * to be sized according to the detail type. - */ -- (void)tabView:(NSTabView *)tabView didSelectTabViewItem:(NSTabViewItem *)tabViewItem -{ - NSInteger selectedTabView = [tabView indexOfTabViewItem:tabViewItem]; - - // Deselect any selected favorite for manual changes - if (!automaticFavoriteSelection) [favoritesTable deselectAll:self]; - automaticFavoriteSelection = NO; - - if (selectedTabView == previousType) return; - - [self resizeTabViewToConnectionType:selectedTabView animating:YES]; - - // Update the host as appropriate - if ((selectedTabView != SPSocketConnection) && [[self host] isEqualToString:@"localhost"]) { - [self setHost:@""]; - } - - previousType = selectedTabView; -} - -/** - * As the scrollview resizes, keep the details centered within it if - * the detail frame is larger than the scrollview size; otherwise, pin - * the detail frame to the top of the scrollview. - */ -- (void)scrollViewFrameChanged:(NSNotification *)aNotification -{ - NSRect scrollViewFrame = [connectionDetailsScrollView frame]; - NSRect scrollDocumentFrame = [[connectionDetailsScrollView documentView] frame]; - NSRect connectionDetailsFrame = [connectionResizeContainer frame]; - - // Scroll view is smaller than contents - keep positioned at top. - if (scrollViewFrame.size.height < connectionDetailsFrame.size.height + 10) { - if (connectionDetailsFrame.origin.y != 0) { - connectionDetailsFrame.origin.y = 0; - [connectionResizeContainer setFrame:connectionDetailsFrame]; - scrollDocumentFrame.size.height = connectionDetailsFrame.size.height + 10; - [[connectionDetailsScrollView documentView] setFrame:scrollDocumentFrame]; - } - - // Otherwise, center. - } else { - connectionDetailsFrame.origin.y = (scrollViewFrame.size.height - connectionDetailsFrame.size.height)/3; - [connectionResizeContainer setFrame:connectionDetailsFrame]; - scrollDocumentFrame.size.height = scrollViewFrame.size.height; - [[connectionDetailsScrollView documentView] setFrame:scrollDocumentFrame]; - } -} - -/** - * 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. - */ -- (void)controlTextDidChange:(NSNotification *)aNotification -{ - [favoritesTable deselectAll:self]; -} - -/** - * When a host field finishes editing, ensure that it hasn't been set to "localhost" - * to ensure that socket connections don't inadvertently occur. - */ -- (void)controlTextDidEndEditing:(NSNotification *)notification -{ - if ([notification object] == standardSQLHostField || [notification object] == sshSQLHostField) { - [self checkHost]; - } -} - -/** * Control tab view resizing based on the supplied connection type, * with an option defining whether it should be animated or not. */ @@ -866,65 +410,18 @@ case SPSSHTunnelConnection: targetResizeRect = [sshConnectionFormContainer frame]; break; - } + } frameRect.size.height = targetResizeRect.size.height + additionalFormHeight; if (animate) { [[connectionResizeContainer animator] setFrame:frameRect]; - } else { - [connectionResizeContainer setFrame:frameRect]; - } -} -#endif - -/** - * Check the host field and ensure it isn't set to "localhost" for - * non-socket connections. - */ -- (BOOL)checkHost -{ - if ([self type] != SPSocketConnection && [[self host] isEqualToString:@"localhost"]) { - SPBeginAlertSheet(NSLocalizedString(@"You have entered 'localhost' for a non-socket connection", @"title of error when using 'localhost' for a network connection"), -#ifndef SP_REFACTOR - NSLocalizedString(@"Use 127.0.0.1", @"Use 127.0.0.1 button"), // Main button - NSLocalizedString(@"Connect via socket", @"Connect via socket button"), // Alternate button -#else - NSLocalizedString(@"OK", @"OK"), // Main button - nil, // Alternate button -#endif - nil, // Other button - [tableDocument parentWindow], // Window to attach to - self, // Modal delegate - @selector(localhostErrorSheetDidEnd:returnCode:contextInfo:), // Did end selector - nil, // Contextual info for selectors -#ifndef SP_REFACTOR - NSLocalizedString(@"To MySQL, 'localhost' is a special host and means that a socket connection should be used.\n\nDid you mean to use a socket connection, or to connect to the local machine via a port? If you meant to connect via a port, '127.0.0.1' should be used instead of 'localhost'.", @"message of error when using 'localhost' for a network connection")); -#else - NSLocalizedString(@"In MySQL, \"localhost\" is a special host and implies that a socket connection should be used.\n\nTo make a socket connection to a local server, choose \"MySQL Socket\" from the \"Connect to\" pop-up.\n\nTo connect via port instead, please change the \"MySQL Server\" field from \"localhost\" to \"127.0.0.1\".", @"message of error when using 'localhost' for a network connection")); -#endif - return NO; - } - - return YES; -} - -/** - * Alert sheet callback method - invoked when the error sheet is closed. - */ -- (void)localhostErrorSheetDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo -{ -#ifndef SP_REFACTOR - if (returnCode == NSAlertAlternateReturn) { - [self setType:SPSocketConnection]; - [self setHost:@""]; - } else { - [self setHost:@"127.0.0.1"]; + } + else { + [connectionResizeContainer setFrame:frameRect]; } -#endif } -#ifndef SP_REFACTOR #pragma mark - #pragma mark Favorites interaction @@ -932,56 +429,33 @@ * Sorts the favorites table view based on the selected sort by item. */ - (void)sortFavorites:(id)sender -{ +{ previousSortItem = currentSortItem; currentSortItem = [[sender menu] indexOfItem:sender]; - + [prefs setInteger:currentSortItem forKey:SPFavoritesSortedBy]; - + // Perform sorting [self _sortFavorites]; - - if ((NSInteger)previousSortItem > -1) [[[sender menu] itemAtIndex:previousSortItem] setState:NSOffState]; - + + if (previousSortItem > SPFavoritesSortUnsorted) [[[sender menu] itemAtIndex:previousSortItem] setState:NSOffState]; + [[[sender menu] itemAtIndex:currentSortItem] setState:NSOnState]; } /** * Reverses the favorites table view sorting based on the selected criteria. */ -- (void)reverseSortFavorites:(NSMenuItem *)sender +- (void)reverseSortFavorites:(id)sender { reverseFavoritesSort = (![sender state]); - + [prefs setBool:reverseFavoritesSort forKey:SPFavoritesSortedInReverse]; - + // Perform re-sorting [self _sortFavorites]; - - [(NSMenuItem *)sender setState:reverseFavoritesSort]; -} - -/** - * Updates the local favorites array from the user defaults - */ -- (void)updateFavorites -{ - [favoritesTable deselectAll:self]; - - if (favorites) [favorites release]; - - if ([prefs objectForKey:SPFavorites]) { - favorites = [[NSMutableArray alloc] initWithArray:[prefs objectForKey:SPFavorites]]; - } - else { - favorites = [[NSMutableArray alloc] init]; - } - - [self _buildFavoritesTree]; - - [favoritesTable reloadData]; - - [favoritesTable expandItem:[[favoritesRoot nodeChildren] objectAtIndex:0]]; + + [(NSMenuItem *)sender setState:reverseFavoritesSort]; } /** @@ -989,21 +463,26 @@ */ - (void)updateFavoriteSelection:(id)sender { - // If nothing is selected, return without updating the interface - if (![self selectedFavorite]) return; - automaticFavoriteSelection = YES; // Clear the keychain referral items as appropriate - if (connectionKeychainID) [connectionKeychainID release], connectionKeychainID = nil; if (connectionKeychainItemName) [connectionKeychainItemName release], connectionKeychainItemName = nil; if (connectionKeychainItemAccount) [connectionKeychainItemAccount release], connectionKeychainItemAccount = nil; if (connectionSSHKeychainItemName) [connectionSSHKeychainItemName release], connectionSSHKeychainItemName = nil; if (connectionSSHKeychainItemAccount) [connectionSSHKeychainItemAccount release], connectionSSHKeychainItemAccount = nil; - + + SPTreeNode *node = [self selectedFavoriteNode]; + // Update key-value properties from the selected favourite, using empty strings where not found - NSDictionary *fav = [self selectedFavorite]; - + NSDictionary *fav = [[node representedObject] nodeFavorite]; + + // Keep a copy of the favorite as it currently stands + if (currentFavorite) [currentFavorite release], currentFavorite = nil; + + currentFavorite = [[node representedObject] copy]; + + [connectionResizeContainer setHidden:NO]; + // Standard details [self setType:([fav objectForKey:SPFavoriteTypeKey] ? [[fav objectForKey:SPFavoriteTypeKey] integerValue] : SPTCPIPConnection)]; [self setName:([fav objectForKey:SPFavoriteNameKey] ? [fav objectForKey:SPFavoriteNameKey] : @"")]; @@ -1012,7 +491,7 @@ [self setUser:([fav objectForKey:SPFavoriteUserKey] ? [fav objectForKey:SPFavoriteUserKey] : @"")]; [self setPort:([fav objectForKey:SPFavoritePortKey] ? [fav objectForKey:SPFavoritePortKey] : @"")]; [self setDatabase:([fav objectForKey:SPFavoriteDatabaseKey] ? [fav objectForKey:SPFavoriteDatabaseKey] : @"")]; - + // SSL details [self setUseSSL:([fav objectForKey:SPFavoriteUseSSLKey] ? [[fav objectForKey:SPFavoriteUseSSLKey] intValue] : NSOffState)]; [self setSslKeyFileLocationEnabled:([fav objectForKey:SPFavoriteSSLKeyFileLocationEnabledKey] ? [[fav objectForKey:SPFavoriteSSLKeyFileLocationEnabledKey] intValue] : NSOffState)]; @@ -1021,91 +500,173 @@ [self setSslCertificateFileLocation:([fav objectForKey:SPFavoriteSSLCertificateFileLocationKey] ? [fav objectForKey:SPFavoriteSSLCertificateFileLocationKey] : @"")]; [self setSslCACertFileLocationEnabled:([fav objectForKey:SPFavoriteSSLCACertFileLocationEnabledKey] ? [[fav objectForKey:SPFavoriteSSLCACertFileLocationEnabledKey] intValue] : NSOffState)]; [self setSslCACertFileLocation:([fav objectForKey:SPFavoriteSSLCACertFileLocationKey] ? [fav objectForKey:SPFavoriteSSLCACertFileLocationKey] : @"")]; - + // SSH details [self setSshHost:([fav objectForKey:SPFavoriteSSHHostKey] ? [fav objectForKey:SPFavoriteSSHHostKey] : @"")]; [self setSshUser:([fav objectForKey:SPFavoriteSSHUserKey] ? [fav objectForKey:SPFavoriteSSHUserKey] : @"")]; [self setSshKeyLocationEnabled:([fav objectForKey:SPFavoriteSSHKeyLocationEnabledKey] ? [[fav objectForKey:SPFavoriteSSHKeyLocationEnabledKey] intValue] : NSOffState)]; [self setSshKeyLocation:([fav objectForKey:SPFavoriteSSHKeyLocationKey] ? [fav objectForKey:SPFavoriteSSHKeyLocationKey] : @"")]; [self setSshPort:([fav objectForKey:SPFavoriteSSHPortKey] ? [fav objectForKey:SPFavoriteSSHPortKey] : @"")]; - + // Trigger an interface update [self resizeTabViewToConnectionType:[self type] animating:YES]; - + // Check whether the password exists in the keychain, and if so add it; also record the // keychain details so we can pass around only those details if the password doesn't change - connectionKeychainItemName = [[keychain nameForFavoriteName:[self valueForKeyPath:@"selectedFavorite.name"] id:[self valueForKeyPath:@"selectedFavorite.id"]] retain]; - connectionKeychainItemAccount = [[keychain accountForUser:[self valueForKeyPath:@"selectedFavorite.user"] host:(([self type] == SPSocketConnection)?@"localhost":[self valueForKeyPath:@"selectedFavorite.host"]) database:[self valueForKeyPath:@"selectedFavorite.database"]] retain]; - + connectionKeychainItemName = [[keychain nameForFavoriteName:[fav objectForKey:SPFavoriteNameKey] id:[fav objectForKey:SPFavoriteIDKey]] retain]; + connectionKeychainItemAccount = [[keychain accountForUser:[fav objectForKey:SPFavoriteUserKey] host:(([self type] == SPSocketConnection) ? @"localhost" : [fav objectForKey:SPFavoriteHostKey]) database:[fav objectForKey:SPFavoriteDatabaseKey]] retain]; + [self setPassword:[keychain getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount]]; - + if (![[self password] length]) { [self setPassword:nil]; [connectionKeychainItemName release], connectionKeychainItemName = nil; [connectionKeychainItemAccount release], connectionKeychainItemAccount = nil; } - - if ([self valueForKeyPath:@"selectedFavorite.id"]) connectionKeychainID = [[[self valueForKeyPath:@"selectedFavorite.id"] stringValue] retain]; - + // And the same for the SSH password - connectionSSHKeychainItemName = [[keychain nameForSSHForFavoriteName:[self valueForKeyPath:@"selectedFavorite.name"] id:[self valueForKeyPath:@"selectedFavorite.id"]] retain]; - connectionSSHKeychainItemAccount = [[keychain accountForSSHUser:[self valueForKeyPath:@"selectedFavorite.sshUser"] sshHost:[self valueForKeyPath:@"selectedFavorite.sshHost"]] retain]; - + connectionSSHKeychainItemName = [[keychain nameForSSHForFavoriteName:[fav objectForKey:SPFavoriteNameKey] id:[fav objectForKey:SPFavoriteIDKey]] retain]; + connectionSSHKeychainItemAccount = [[keychain accountForSSHUser:[fav objectForKey:SPFavoriteSSHUserKey] sshHost:[fav objectForKey:SPFavoriteSSHHostKey]] retain]; + [self setSshPassword:[keychain getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]]; - + if (![[self sshPassword] length]) { [self setSshPassword:nil]; [connectionSSHKeychainItemName release], connectionSSHKeychainItemName = nil; [connectionSSHKeychainItemAccount release], connectionSSHKeychainItemAccount = nil; } - - [prefs setInteger:([favoritesTable selectedRow] - 1) forKey:SPLastFavoriteIndex]; - - // Set next KeyView to password field if it is empty - switch ([self type]) + + [prefs setInteger:[[fav objectForKey:SPFavoriteIDKey] integerValue] forKey:SPLastFavoriteID]; + + // Set first responder to password field if it is empty + switch ([self type]) { case SPTCPIPConnection: - if (![[standardPasswordField stringValue] length]) [favoritesTable setNextKeyView:standardPasswordField]; + if (![[standardPasswordField stringValue] length]) [[dbDocument parentWindow] makeFirstResponder:standardPasswordField]; break; case SPSocketConnection: - if (![[socketPasswordField stringValue] length]) [favoritesTable setNextKeyView:socketPasswordField]; + if (![[socketPasswordField stringValue] length]) [[dbDocument parentWindow] makeFirstResponder:socketPasswordField]; break; case SPSSHTunnelConnection: - if (![[sshPasswordField stringValue] length]) [favoritesTable setNextKeyView:sshPasswordField]; + if (![[sshPasswordField stringValue] length]) [[dbDocument parentWindow] makeFirstResponder:sshPasswordField]; break; } } /** - * Returns a KVC-compliant proxy to the currently selected favorite, or nil if nothing selected. + * Returns the selected favorite data dictionary or nil if nothing is selected. */ -- (id)selectedFavorite +- (NSMutableDictionary *)selectedFavorite { - if ([favoritesTable selectedRow] == -1) return nil; + SPTreeNode *node = [self selectedFavoriteNode]; + + return (![node isGroup]) ? [[node representedObject] nodeFavorite] : nil; +} - return [favorites objectAtIndex:([favoritesTable selectedRow] - 1)]; +/** + * Returns the selected favorite node or nil if nothing is selected. + */ +- (SPTreeNode *)selectedFavoriteNode +{ + NSArray *nodes = [self selectedFavoriteNodes]; + + return ([nodes count]) ? (SPTreeNode *)[[self selectedFavoriteNodes] objectAtIndex:0] : nil; } /** - * Adds the current details as a new favorite, select it, and scroll the selected - * row to visible. + * Returns an array of selected favorite nodes. + */ +- (NSArray *)selectedFavoriteNodes +{ + NSMutableArray *nodes = [NSMutableArray array]; + NSIndexSet *indexes = [favoritesOutlineView selectedRowIndexes]; + + NSUInteger currentIndex = [indexes firstIndex]; + + while (currentIndex != NSNotFound) + { + [nodes addObject:[favoritesOutlineView itemAtRow:currentIndex]]; + + currentIndex = [indexes indexGreaterThanIndex:currentIndex]; + } + + return nodes; +} + +/** + * Adds a new connection favorite. */ - (IBAction)addFavorite:(id)sender { - NSString *thePassword, *theSSHPassword; - NSNumber *favoriteid = [NSNumber numberWithInteger:[[NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]] hash]]; - NSString *favoriteName = [[self name] length] ? [self name]:[NSString stringWithFormat:@"%@@%@", ([self user] && [[self user] length])?[self user] : @"anonymous", (([self type] == SPSocketConnection) ? @"localhost" : [self host])]; + NSNumber *favoriteID = [self _createNewFavoriteID]; + + NSArray *objects = [NSArray arrayWithObjects:NSLocalizedString(@"New Favorite", @"new favorite name"), + [NSNumber numberWithInteger:0], @"", @"", @"", @"", + [NSNumber numberWithInt:NSOffState], + [NSNumber numberWithInt:NSOffState], + [NSNumber numberWithInt:NSOffState], + [NSNumber numberWithInt:NSOffState], @"", @"", @"", + [NSNumber numberWithInt:NSOffState], @"", @"", favoriteID, nil]; + + NSArray *keys = [NSArray arrayWithObjects: + SPFavoriteNameKey, + SPFavoriteTypeKey, + SPFavoriteHostKey, + SPFavoriteSocketKey, + SPFavoriteUserKey, + SPFavoritePortKey, + SPFavoriteUseSSLKey, + SPFavoriteSSLKeyFileLocationEnabledKey, + SPFavoriteSSLCertificateFileLocationEnabledKey, + SPFavoriteSSLCACertFileLocationEnabledKey, + SPFavoriteDatabaseKey, + SPFavoriteSSHHostKey, + SPFavoriteSSHUserKey, + SPFavoriteSSHKeyLocationEnabledKey, + SPFavoriteSSHKeyLocationKey, + SPFavoriteSSHPortKey, + SPFavoriteIDKey, + nil]; + + // Create default favorite + NSMutableDictionary *favorite = [NSMutableDictionary dictionaryWithObjects:objects forKeys:keys]; + + SPTreeNode *selectedNode = [self selectedFavoriteNode]; + + SPTreeNode *parent = ([selectedNode isGroup]) ? selectedNode : (SPTreeNode *)[selectedNode parentNode]; + + SPTreeNode *node = [favoritesController addFavoriteNodeWithData:favorite asChildOfNode:parent]; + + [self _reloadFavoritesViewData]; + [self _selectNode:node]; + + [[[[NSApp delegate] preferenceController] generalPreferencePane] updateDefaultFavoritePopup]; + + favoriteNameFieldWasTouched = NO; + + [favoritesOutlineView editColumn:0 row:[favoritesOutlineView selectedRow] withEvent:nil select:YES]; +} +/** + * Adds the current details as a new connection favorite, selects it, and scrolls the selected + * row to be visible. + */ +- (IBAction)addFavoriteUsingCurrentDetails:(id)sender +{ + NSString *thePassword, *theSSHPassword; + NSNumber *favoriteid = [self _createNewFavoriteID]; + NSString *favoriteName = [[self name] length] ? [self name] : [NSString stringWithFormat:@"%@@%@", ([self user] && [[self user] length])?[self user] : @"anonymous", (([self type] == SPSocketConnection) ? @"localhost" : [self host])]; + if (![[self name] length] && [self database] && ![[self database] isEqualToString:@""]) { favoriteName = [NSString stringWithFormat:@"%@ %@", [self database], favoriteName]; } - + // Ensure that host is not empty if this is a TCP/IP or SSH connection if (([self type] == SPTCPIPConnection || [self type] == SPSSHTunnelConnection) && ![[self host] length]) { NSRunAlertPanel(NSLocalizedString(@"Insufficient connection details", @"insufficient details message"), NSLocalizedString(@"Insufficient details provided to establish a connection. Please provide at least a host.", @"insufficient details informative message"), NSLocalizedString(@"OK", @"OK button"), nil, nil); return; } - + // If SSH is enabled, ensure that the SSH host is not nil if ([self type] == SPSSHTunnelConnection && ![[self sshHost] length]) { NSRunAlertPanel(NSLocalizedString(@"Insufficient connection details", @"insufficient details message"), NSLocalizedString(@"Please enter the hostname for the SSH Tunnel, or disable the SSH Tunnel.", @"message of panel when ssh details are incomplete"), NSLocalizedString(@"OK", @"OK button"), nil, nil); @@ -1113,22 +674,22 @@ } // Ensure that a socket connection is not inadvertently used - if (![self checkHost]) return; - + if (![self _checkHost]) return; + // Construct the favorite details - cannot use only dictionaryWithObjectsAndKeys for possible nil values. NSMutableDictionary *newFavorite = [NSMutableDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInteger:[self type]], SPFavoriteTypeKey, favoriteName, SPFavoriteNameKey, favoriteid, SPFavoriteIDKey, nil]; - + // Standard details if ([self host]) [newFavorite setObject:[self host] forKey:SPFavoriteHostKey]; if ([self socket]) [newFavorite setObject:[self socket] forKey:SPFavoriteSocketKey]; if ([self user]) [newFavorite setObject:[self user] forKey:SPFavoriteUserKey]; if ([self port]) [newFavorite setObject:[self port] forKey:SPFavoritePortKey]; if ([self database]) [newFavorite setObject:[self database] forKey:SPFavoriteDatabaseKey]; - + // SSL details if ([self useSSL]) [newFavorite setObject:[NSNumber numberWithInt:[self useSSL]] forKey:SPFavoriteUseSSLKey]; [newFavorite setObject:[NSNumber numberWithInt:[self sslKeyFileLocationEnabled]] forKey:SPFavoriteSSLKeyFileLocationEnabledKey]; @@ -1137,7 +698,7 @@ if ([self sslCertificateFileLocation]) [newFavorite setObject:[self sslCertificateFileLocation] forKey:SPFavoriteSSLCertificateFileLocationKey]; [newFavorite setObject:[NSNumber numberWithInt:[self sslCACertFileLocationEnabled]] forKey:SPFavoriteSSLCACertFileLocationEnabledKey]; if ([self sslCACertFileLocation]) [newFavorite setObject:[self sslCACertFileLocation] forKey:SPFavoriteSSLCACertFileLocationKey]; - + // SSH details if ([self sshHost]) [newFavorite setObject:[self sshHost] forKey:SPFavoriteSSHHostKey]; if ([self sshUser]) [newFavorite setObject:[self sshUser] forKey:SPFavoriteSSHUserKey]; @@ -1145,22 +706,13 @@ [newFavorite setObject:[NSNumber numberWithInt:[self sshKeyLocationEnabled]] forKey:SPFavoriteSSHKeyLocationEnabledKey]; if ([self sshKeyLocation]) [newFavorite setObject:[self sshKeyLocation] forKey:SPFavoriteSSHKeyLocationKey]; - // Add the new favorite to the user defaults array - NSMutableArray *currentFavorites = ([prefs objectForKey:SPFavorites]) ? [[NSMutableArray alloc] initWithArray:[prefs objectForKey:SPFavorites]] : [[NSMutableArray alloc] init]; - - [currentFavorites addObject:newFavorite]; - - [prefs setObject:[NSArray arrayWithArray:currentFavorites] forKey:SPFavorites]; - - [currentFavorites release]; - // Add the password to keychain as appropriate thePassword = [self password]; - + if (mySQLConnection && connectionKeychainItemName) { thePassword = [keychain getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount]; } - + if (thePassword && (![thePassword isEqualToString:@""])) { [keychain addPassword:thePassword forName:[keychain nameForFavoriteName:favoriteName id:[NSString stringWithFormat:@"%lld", [favoriteid longLongValue]]] @@ -1169,71 +721,403 @@ // Add the SSH password to keychain as appropriate theSSHPassword = [self sshPassword]; - + if (mySQLConnection && connectionSSHKeychainItemName) { theSSHPassword = [keychain getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]; } - + if (theSSHPassword && (![theSSHPassword isEqualToString:@""])) { [keychain addPassword:theSSHPassword forName:[keychain nameForSSHForFavoriteName:favoriteName id:[NSString stringWithFormat:@"%lld", [favoriteid longLongValue]]] account:[keychain accountForSSHUser:[self sshUser] sshHost:[self sshHost]]]; } - - // Update the favorites list and selection - [self updateFavorites]; - - [favoritesTable selectRowIndexes:[NSIndexSet indexSetWithIndex:[favorites count]] byExtendingSelection:NO]; - [favoritesTable scrollRowToVisible:[favoritesTable selectedRow]]; + + SPTreeNode *node = [favoritesController addFavoriteNodeWithData:newFavorite asChildOfNode:nil]; + + [self _reloadFavoritesViewData]; + [self _selectNode:node]; // Update the favorites popup button in the preferences [[[[NSApp delegate] preferenceController] generalPreferencePane] updateDefaultFavoritePopup]; } /** - * If the favorites list in the preferences change, trigger a reload of - * the favorites table data. + * Adds a new group node to the favorites tree with a default name. Once added it is selected for editing. */ -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +- (IBAction)addGroup:(id)sender +{ + SPTreeNode *selectedNode = [self selectedFavoriteNode]; + + SPTreeNode *parent = ([selectedNode isGroup]) ? selectedNode : (SPTreeNode *)[selectedNode parentNode]; + + SPTreeNode *node = [favoritesController addGroupNodeWithName:NSLocalizedString(@"New Folder", @"new folder placeholder name") asChildOfNode:parent]; + + [self _reloadFavoritesViewData]; + [self _selectNode:node]; + + isEditing = YES; + + [favoritesOutlineView editColumn:0 row:[favoritesOutlineView selectedRow] withEvent:nil select:YES]; +} + +/** + * Removes the selected node. + */ +- (IBAction)removeNode:(id)sender +{ + if ([favoritesOutlineView numberOfSelectedRows] == 1) { + + SPTreeNode *node = [self selectedFavoriteNode]; + + NSString *message = @""; + NSString *informativeMessage = @""; + + if (![node isGroup]) { + message = [NSString stringWithFormat:NSLocalizedString(@"Delete favorite '%@'?", @"delete database message"), [[[node representedObject] nodeFavorite] objectForKey:SPFavoriteNameKey]]; + informativeMessage = [NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to delete the favorite '%@'? This operation cannot be undone.", @"delete database informative message"), [[[node representedObject] nodeFavorite] objectForKey:SPFavoriteNameKey]]; + } + else { + message = [NSString stringWithFormat:NSLocalizedString(@"Delete group '%@'?", @"delete database message"), [[node representedObject] nodeName]]; + informativeMessage = [NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to delete the group '%@'? All favorites within this group will also be deleted. This operation cannot be undone.", @"delete database informative message"), [[node representedObject] nodeName]]; + } + + NSAlert *alert = [NSAlert alertWithMessageText:message + defaultButton:NSLocalizedString(@"Delete", @"delete button") + alternateButton:NSLocalizedString(@"Cancel", @"cancel button") + otherButton:nil + informativeTextWithFormat:informativeMessage]; + + NSArray *buttons = [alert buttons]; + + // Change the alert's cancel button to have the key equivalent of return + [[buttons objectAtIndex:0] setKeyEquivalent:@"d"]; + [[buttons objectAtIndex:0] setKeyEquivalentModifierMask:NSCommandKeyMask]; + [[buttons objectAtIndex:1] setKeyEquivalent:@"\r"]; + + [alert setAlertStyle:NSCriticalAlertStyle]; + + [alert beginSheetModalForWindow:[dbDocument parentWindow] + modalDelegate:self + didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) + contextInfo:SPRemoveNode]; + } +} + +/** + * Duplicates the selected connection favorite. + */ +- (IBAction)duplicateFavorite:(id)sender +{ + if ([favoritesOutlineView numberOfSelectedRows] == 1) { + + NSMutableDictionary *favorite = [NSMutableDictionary dictionaryWithDictionary:[self selectedFavorite]]; + + NSNumber *favoriteID = [self _createNewFavoriteID]; + + NSInteger duplicatedFavoriteType = [[favorite objectForKey:SPFavoriteTypeKey] integerValue]; + + // Update the unique ID + [favorite setObject:favoriteID forKey:SPFavoriteIDKey]; + + // Alter the name for clarity + [favorite setObject:[NSString stringWithFormat:NSLocalizedString(@"%@ Copy", @"Initial favourite name after duplicating a previous favourite"), [favorite objectForKey:SPFavoriteNameKey]] forKey:SPFavoriteNameKey]; + + // Create new keychain items if appropriate + if (password && [password length]) { + NSString *keychainName = [keychain nameForFavoriteName:[favorite objectForKey:SPFavoriteNameKey] id:[favorite objectForKey:SPFavoriteIDKey]]; + NSString *keychainAccount = [keychain accountForUser:[favorite objectForKey:SPFavoriteUserKey] host:((duplicatedFavoriteType == SPSocketConnection) ? @"localhost" : [favorite objectForKey:SPFavoriteHostKey]) database:[favorite objectForKey:SPFavoriteDatabaseKey]]; + NSString *favoritePassword = [keychain getPasswordForName:keychainName account:keychainAccount]; + + keychainName = [keychain nameForFavoriteName:[favorite objectForKey:SPFavoriteNameKey] id:[favorite objectForKey:SPFavoriteIDKey]]; + + [keychain addPassword:favoritePassword forName:keychainName account:keychainAccount]; + + favoritePassword = nil; + } + + if (sshPassword && [sshPassword length]) { + NSString *keychainSSHName = [keychain nameForSSHForFavoriteName:[favorite objectForKey:SPFavoriteNameKey] id:[favorite objectForKey:SPFavoriteIDKey]]; + NSString *keychainSSHAccount = [keychain accountForSSHUser:[favorite objectForKey:SPFavoriteSSHUserKey] sshHost:[favorite objectForKey:SPFavoriteSSHHostKey]]; + NSString *favoriteSSHPassword = [keychain getPasswordForName:keychainSSHName account:keychainSSHAccount]; + + keychainSSHName = [keychain nameForSSHForFavoriteName:[favorite objectForKey:SPFavoriteNameKey] id:[favorite objectForKey:SPFavoriteIDKey]]; + + [keychain addPassword:favoriteSSHPassword forName:keychainSSHName account:keychainSSHAccount]; + + favoriteSSHPassword = nil; + } + + SPTreeNode *selectedNode = [self selectedFavoriteNode]; + + SPTreeNode *parent = ([selectedNode isGroup]) ? selectedNode : (SPTreeNode *)[selectedNode parentNode]; + + SPTreeNode *node = [favoritesController addFavoriteNodeWithData:favorite asChildOfNode:parent]; + + [self _reloadFavoritesViewData]; + [self _selectNode:node]; + + [[(SPPreferenceController *)[[NSApp delegate] preferenceController] generalPreferencePane] updateDefaultFavoritePopup]; + } +} + +/** + * Switches the selected favorite/group to editing mode so it can be renamed. + */ +- (IBAction)renameNode:(id)sender { - if ([keyPath isEqualToString:SPFavorites]) { - [self updateFavorites]; + if ([favoritesOutlineView numberOfSelectedRows] == 1) { + [favoritesOutlineView editColumn:0 row:[favoritesOutlineView selectedRow] withEvent:nil select:YES]; } } +/** + * Marks the selected favorite as the default. + */ +- (IBAction)makeSelectedFavoriteDefault:(id)sender +{ + NSInteger favoriteID = [[[self selectedFavorite] objectForKey:SPFavoriteIDKey] integerValue]; + + [prefs setInteger:favoriteID forKey:SPDefaultFavorite]; +} + #pragma mark - -#pragma mark Menu Validation +#pragma mark Import/export favorites --(BOOL)validateMenuItem:(NSMenuItem *)menuItem +/** + * Displays an open panel, allowing the user to import their favorites. + */ +- (IBAction)importFavorites:(id)sender { - SEL action = [menuItem action]; - if ((action == @selector(sortFavorites:)) || (action == @selector(reverseSortFavorites:))) { + NSOpenPanel *openPanel = [NSOpenPanel openPanel]; + + [openPanel beginSheetForDirectory:nil + file:nil + types:[NSArray arrayWithObject:@"plist"] + modalForWindow:[dbDocument parentWindow] + modalDelegate:self + didEndSelector:@selector(importExportFavoritesSheetDidEnd:returnCode:contextInfo:) + contextInfo:SPImportFavorites]; +} - // Loop all the items in the sort by menu only checking the currently selected one - for (NSMenuItem *item in [[menuItem menu] itemArray]) - { - [item setState:([[menuItem menu] indexOfItem:item] == currentSortItem) ? NSOnState : NSOffState]; +/** + * Displays a save panel, allowing the user to export their favorites. + */ +- (IBAction)exportFavorites:(id)sender +{ + NSSavePanel *savePanel = [NSSavePanel savePanel]; + + NSString *fileName = [[self selectedFavoriteNodes] count] > 1 ? SPExportFavoritesFilename : [[[self selectedFavorite] objectForKey:SPFavoriteNameKey] stringByAppendingPathExtension:@"plist"]; + + [savePanel setAccessoryView:exportPanelAccessoryView]; + + [savePanel beginSheetForDirectory:nil + file:fileName + modalForWindow:[dbDocument parentWindow] + modalDelegate:self + didEndSelector:@selector(importExportFavoritesSheetDidEnd:returnCode:contextInfo:) + contextInfo:SPExportFavorites]; +} + +#pragma mark - +#pragma mark Accessors + +/** + * Returns the main outline view instance. + */ +- (SPFavoritesOutlineView *)favoritesOutlineView +{ + return favoritesOutlineView; +} + +#pragma mark - +#pragma mark Key Value Observing + +/** + * This method is called as part of Key Value Observing. + */ +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + id oldObject = [change objectForKey:NSKeyValueChangeOldKey]; + id newObject = [change objectForKey:NSKeyValueChangeNewKey]; + + if (oldObject != newObject) { + [[self selectedFavorite] setObject:(newObject) ? newObject : @"" forKey:keyPath]; + + // Save the new data to disk + [favoritesController saveFavorites]; + + [self _reloadFavoritesViewData]; + } +} + +#pragma mark - +#pragma mark Sheet methods + +/** + * Called when the user dismisses the remove node sheet. + */ +- (void)sheetDidEnd:(id)sheet returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo +{ + // Remove the current favorite/group node + if ([contextInfo isEqualToString:SPRemoveNode]) { + if (returnCode == NSAlertDefaultReturn) { + + SPTreeNode *node = [self selectedFavoriteNode]; + + if (![node isGroup]) { + NSDictionary *favorite = [[node representedObject] nodeFavorite]; + + // Get selected favorite's details + NSString *favoriteName = [favorite objectForKey:SPFavoriteNameKey]; + NSString *favoriteUser = [favorite objectForKey:SPFavoriteUserKey]; + NSString *favoriteHost = [favorite objectForKey:SPFavoriteHostKey]; + NSString *favoriteDatabase = [favorite objectForKey:SPFavoriteDatabaseKey]; + NSString *favoriteSSHUser = [favorite objectForKey:SPFavoriteSSHUserKey]; + NSString *favoriteSSHHost = [favorite objectForKey:SPFavoriteSSHHostKey]; + NSString *favoriteID = [favorite objectForKey:SPFavoriteIDKey]; + + // Remove passwords from the Keychain + [keychain deletePasswordForName:[keychain nameForFavoriteName:favoriteName id:favoriteID] + account:[keychain accountForUser:favoriteUser host:((type == SPSocketConnection) ? @"localhost" : favoriteHost) database:favoriteDatabase]]; + [keychain deletePasswordForName:[keychain nameForSSHForFavoriteName:favoriteName id:favoriteID] + account:[keychain accountForSSHUser:favoriteSSHUser sshHost:favoriteSSHHost]]; + + // Reset last used favorite + if ([[favorite objectForKey:SPFavoriteIDKey] integerValue] == [prefs integerForKey:SPLastFavoriteID]) { + [prefs setInteger:0 forKey:SPLastFavoriteID]; + } + + // If required, reset the default favorite + if ([[favorite objectForKey:SPFavoriteIDKey] integerValue] == [prefs integerForKey:SPDefaultFavorite]) { + [prefs setInteger:[prefs integerForKey:SPLastFavoriteID] forKey:SPDefaultFavorite]; + } + } + + [favoritesController removeFavoriteNode:node]; + + [self _reloadFavoritesViewData]; + + [[(SPPreferenceController *)[[NSApp delegate] preferenceController] generalPreferencePane] updateDefaultFavoritePopup]; } + } +} - // Check or uncheck the reverse sort item - if (action == @selector(reverseSortFavorites:)) { - [menuItem setState:reverseFavoritesSort]; +/** + * Called after closing the SSH/SSL key selection sheet. + */ +- (void)chooseKeyLocationSheetDidEnd:(NSOpenPanel *)openPanel returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo +{ + NSString *abbreviatedFileName = [[openPanel filename] stringByAbbreviatingWithTildeInPath]; + + // SSH key file selection + if (contextInfo == sshSSHKeyButton) { + if (returnCode == NSCancelButton) { + [self setSshKeyLocationEnabled:NSOffState]; + return; + } + + [self setSshKeyLocation:abbreviatedFileName]; + } + // SSL key file selection + else if (contextInfo == standardSSLKeyFileButton || contextInfo == socketSSLKeyFileButton) { + if (returnCode == NSCancelButton) { + [self setSslKeyFileLocationEnabled:NSOffState]; + [self setSslKeyFileLocation:nil]; + return; } - } - return YES; + + [self setSslKeyFileLocation:abbreviatedFileName]; + } + // SSL certificate file selection + else if (contextInfo == standardSSLCertificateButton || contextInfo == socketSSLCertificateButton) { + if (returnCode == NSCancelButton) { + [self setSslCertificateFileLocationEnabled:NSOffState]; + [self setSslCertificateFileLocation:nil]; + return; + } + + [self setSslCertificateFileLocation:abbreviatedFileName]; + } + // SSL CA certificate file selection + else if (contextInfo == standardSSLCACertButton || contextInfo == socketSSLCACertButton) { + if (returnCode == NSCancelButton) { + [self setSslCACertFileLocationEnabled:NSOffState]; + [self setSslCACertFileLocation:nil]; + return; + } + + [self setSslCACertFileLocation:abbreviatedFileName]; + } +} +/** + * Called when the user dismisses either the import of export favorites panels. + */ +- (void)importExportFavoritesSheetDidEnd:(NSOpenPanel *)panel returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo +{ + if (returnCode == NSOKButton) { + if (contextInfo == SPExportFavorites) { + SPFavoritesExporter *exporter = [[[SPFavoritesExporter alloc] init] autorelease]; + + [exporter setDelegate:self]; + + [exporter writeFavorites:[self selectedFavoriteNodes] toFile:[panel filename]]; + } + else if (contextInfo == SPImportFavorites) { + SPFavoritesImporter *importer = [[SPFavoritesImporter alloc] init]; + + [importer setDelegate:self]; + + [importer importFavoritesFromFileAtPath:[panel filename]]; + } + } +} + +/** + * Alert sheet callback method - invoked when the error sheet is closed. + */ +- (void)localhostErrorSheetDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo +{ + if (returnCode == NSAlertAlternateReturn) { + [self setType:SPSocketConnection]; + [self setHost:@""]; + } + else { + [self setHost:@"127.0.0.1"]; + } } #pragma mark - #pragma mark Private API /** + * Check the host field and ensure it isn't set to 'localhost' for non-socket connections. + */ +- (BOOL)_checkHost +{ + if ([self type] != SPSocketConnection && [[self host] isEqualToString:@"localhost"]) { + SPBeginAlertSheet(NSLocalizedString(@"You have entered 'localhost' for a non-socket connection", @"title of error when using 'localhost' for a network connection"), + NSLocalizedString(@"Use 127.0.0.1", @"Use 127.0.0.1 button"), // Main button + NSLocalizedString(@"Connect via socket", @"Connect via socket button"), // Alternate button + nil, // Other button + [dbDocument parentWindow], // Window to attach to + self, // Modal delegate + @selector(localhostErrorSheetDidEnd:returnCode:contextInfo:), // Did end selector + nil, // Contextual info for selectors + NSLocalizedString(@"To MySQL, 'localhost' is a special host and means that a socket connection should be used.\n\nDid you mean to use a socket connection, or to connect to the local machine via a port? If you meant to connect via a port, '127.0.0.1' should be used instead of 'localhost'.", @"message of error when using 'localhost' for a network connection")); + return NO; + } + + return YES; +} + +/** * Sorts the connection favorites based on the selected criteria. */ - (void)_sortFavorites { NSString *sortKey = SPFavoriteNameKey; - + switch (currentSortItem) { case SPFavoritesSortNameItem: @@ -1245,60 +1129,120 @@ case SPFavoritesSortTypeItem: sortKey = SPFavoriteTypeKey; break; - default: - return; + case SPFavoritesSortUnsorted: + break; } + + [self _sortTreeNode:[[favoritesRoot childNodes] objectAtIndex:0] usingKey:sortKey]; + + [favoritesController saveFavorites]; + + [self _reloadFavoritesViewData]; +} - NSSortDescriptor *sortDescriptor = nil; - - if (currentSortItem == SPFavoritesSortTypeItem) { - sortDescriptor = [[[NSSortDescriptor alloc] initWithKey:sortKey ascending:(!reverseFavoritesSort)] autorelease]; +/** + * Sorts the supplied tree node using the supplied sort key. + * + * @param node The tree node to sort + * @param key The sort key to sort by + */ +- (void)_sortTreeNode:(SPTreeNode *)node usingKey:(NSString *)key +{ + NSMutableArray *nodes = [[node mutableChildNodes] mutableCopy]; + + // If this node only has one child and it's not another group node, don't bother proceeding + if (([nodes count] == 1) && (![[nodes objectAtIndex:0] isGroup])) return; + + for (SPTreeNode *treeNode in nodes) + { + if ([treeNode isGroup]) { + [self _sortTreeNode:treeNode usingKey:key]; + } } - else { - sortDescriptor = [[[NSSortDescriptor alloc] initWithKey:sortKey ascending:(!reverseFavoritesSort) selector:@selector(caseInsensitiveCompare:)] autorelease]; + + NSMutableIndexSet *indexes = [[NSMutableIndexSet alloc] init]; + NSMutableArray *groupNodes = [[NSMutableArray alloc] init]; + + for (SPTreeNode *innerNode in nodes) + { + if ([innerNode isGroup]) { + [groupNodes addObject:innerNode]; + [indexes addIndex:[nodes indexOfObject:innerNode]]; + } } - - [favorites sortUsingDescriptors:[NSArray arrayWithObject:sortDescriptor]]; - - // Rebuild the favorites tree - [self _buildFavoritesTree]; - - [favoritesTable reloadData]; - - [favoritesTable expandItem:[[favoritesRoot nodeChildren] objectAtIndex:0]]; + + NSUInteger i = [indexes firstIndex]; + + while (i != NSNotFound) + { + [nodes removeObjectAtIndex:i]; + + i = [indexes indexGreaterThanIndex:i]; + } + + [indexes release]; + + [nodes sortUsingFunction:_compareFavoritesUsingKey context:key]; + + [nodes addObjectsFromArray:groupNodes]; + + if (reverseFavoritesSort) [nodes reverse]; + + [[node mutableChildNodes] setArray:nodes]; + + [groupNodes release]; } /** - * Builds a tree structure from the user's connection favorties by wrapping them in SPFavoriteNode instances. + * Sort function used by NSMutableArray's sortUsingFunction: + * + * @param favorite1 The first of the favorites to compare (and determine sort order) + * @param favorite2 The second of the favorites to compare + * @param key The sort key to perform the comparison by + * + * @return An integer (NSComparisonResult) indicating the order of the comparison */ -- (void)_buildFavoritesTree +static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, void *key) { - if (favoritesRoot) [favoritesRoot release], favoritesRoot = nil; - - favoritesRoot = [[SPFavoriteNode alloc] init]; - - // Add a dummy item to represent the favorites heading - SPFavoriteNode *favoritesNode = [[SPFavoriteNode alloc] init]; - - [favoritesNode setNodeIsGroup:YES]; - [favoritesNode setNodeName:NSLocalizedString(@"FAVORITES", @"Favorites title at the top of the sidebar")]; - - for (NSDictionary *favorite in favorites) - { - SPFavoriteNode *node2 = [[SPFavoriteNode alloc] init]; - - [node2 setNodeFavorite:favorite]; - - [[favoritesNode nodeChildren] addObject:node2]; + NSString *dictKey = (NSString *)key; + + id value1 = [[(SPFavoriteNode *)[(SPTreeNode *)favorite1 representedObject] nodeFavorite] objectForKey:dictKey]; + id value2 = [[(SPFavoriteNode *)[(SPTreeNode *)favorite2 representedObject] nodeFavorite] objectForKey:dictKey]; + + return [value1 compare:value2]; +} - [node2 release]; +/** + * Updates the favorite's host when the type changes. + */ +- (void)_favoriteTypeDidChange +{ + NSDictionary *favorite = [[[self selectedFavoriteNode] representedObject] nodeFavorite]; + + // If either socket or host is localhost, clear. + if ((previousType != SPSocketConnection) && [[favorite objectForKey:SPFavoriteHostKey] isEqualToString:@"localhost"]) { + [self setHost:@""]; } + + // Update the name for newly added favorites if not already touched by the user, by triggering a KVO update + if (!favoriteNameFieldWasTouched) { + [self setName:[NSString stringWithFormat:@"%@@%@", + ([favorite objectForKey:SPFavoriteUserKey]) ? [favorite objectForKey:SPFavoriteUserKey] : @"", + ((previousType == SPSocketConnection) ? @"localhost" : + (([favorite objectForKey:SPFavoriteHostKey]) ? [favorite valueForKeyPath:SPFavoriteHostKey] : @"")) + ]]; + } +} - [[favoritesRoot nodeChildren] addObject:favoritesNode]; - - [favoritesNode release]; +/** + * Convenience method for reloading the outline view, expanding the root item and scrolling to the selected item. + */ +- (void)_reloadFavoritesViewData +{ + [favoritesOutlineView reloadData]; + [favoritesOutlineView expandItem:[[favoritesRoot childNodes] objectAtIndex:0] expandChildren:YES]; + [favoritesOutlineView scrollRowToVisible:[favoritesOutlineView selectedRow]]; } -#endif /** * Restores the connection interface to its original state. @@ -1307,15 +1251,14 @@ { // Must be performed on the main thread if (![NSThread isMainThread]) return [[self onMainThread] _restoreConnectionInterface]; - + // Reset the window title - [[tableDocument parentWindow] setTitle:[tableDocument displayName]]; - + [[dbDocument parentWindow] setTitle:[dbDocument displayName]]; + // Stop the current tab's progress indicator - [tableDocument setIsProcessing:NO]; - + [dbDocument setIsProcessing:NO]; + // Reset the UI -#ifndef SP_REFACTOR [addToFavoritesButton setHidden:NO]; [addToFavoritesButton display]; [helpButton setHidden:NO]; @@ -1327,241 +1270,180 @@ [progressIndicator display]; [progressIndicatorText setHidden:YES]; [progressIndicatorText display]; - + // Re-enable favorites table view - [favoritesTable setEnabled:YES]; - [favoritesTable display]; -#endif - + [favoritesOutlineView setEnabled:YES]; + [(NSView *)favoritesOutlineView display]; + mySQLConnectionCancelled = NO; - + // Revert the connect button back to its original selector -#ifndef SP_REFACTOR [connectButton setAction:@selector(initiateConnection:)]; -#endif } /** - * Called on the main thread once the MySQL connection is established on the background thread. Either the - * connection was cancelled or it was successful. + * Selected the supplied node in the favorites outline view. */ -- (void)_mySQLConnectionEstablished +- (void)_selectNode:(SPTreeNode *)node { - isConnecting = NO; - - // If the user hit cancel during the connection attempt, kill the connection once - // established and reset the UI. - if (mySQLConnectionCancelled) { - if ([mySQLConnection isConnected]) { - [mySQLConnection disconnect]; - [mySQLConnection release], mySQLConnection = nil; - } - - // Kill the SSH connection if present - [self cancelConnection]; - - [self _restoreConnectionInterface]; - - return; - } - -#ifndef SP_REFACTOR /* ui manipulation */ - [progressIndicatorText setStringValue:NSLocalizedString(@"Connected", @"connection established message")]; - [progressIndicatorText display]; -#endif - - // Stop the current tab's progress indicator - [tableDocument setIsProcessing:NO]; + [favoritesOutlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[favoritesOutlineView rowForItem:node]] byExtendingSelection:NO]; + [favoritesOutlineView scrollRowToVisible:[favoritesOutlineView selectedRow]]; +} -#ifndef SP_REFACTOR /* ui manipulation */ - // Successful connection! - [connectButton setEnabled:NO]; - [connectButton display]; - [progressIndicator stopAnimation:self]; - [progressIndicatorText setHidden:YES]; - [addToFavoritesButton setHidden:NO]; -#endif +/** + * Creates a new favorite ID based on the UNIX epoch time. + */ +- (NSNumber *)_createNewFavoriteID +{ + return [NSNumber numberWithInteger:[[NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]] hash]]; +} - // If SSL was enabled, check it was established correctly - if (useSSL && ([self type] == SPTCPIPConnection || [self type] == SPSocketConnection)) { - if (![mySQLConnection isConnectedViaSSL]) { - SPBeginAlertSheet(NSLocalizedString(@"SSL connection not established", @"SSL requested but not used title"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocument parentWindow], nil, nil, nil, NSLocalizedString(@"You requested that the connection should be established using SSL, but MySQL made the connection without SSL.\n\nThis may be because the server does not support SSL connections, or has SSL disabled; or insufficient details were supplied to establish an SSL connection.\n\nThis connection is not encrypted.", @"SSL connection requested but not established error detail")); - } else { -#ifndef SP_REFACTOR /* [tableDocument setStatusIconToImageWithName:@"titlebarlock"] */ - [tableDocument setStatusIconToImageWithName:@"titlebarlock"]; -#endif - } +/** + * Returns the favorite node for the conection favorite with the supplied ID. + */ +- (SPTreeNode *)_favoriteNodeForFavoriteID:(NSInteger)favoriteID +{ + SPTreeNode *favoriteNode = nil; + + if (!favoritesRoot) return favoriteNode; + + for (SPTreeNode *node in [favoritesRoot allChildLeafs]) + { + if ([[[[node representedObject] nodeFavorite] objectForKey:SPFavoriteIDKey] integerValue] == favoriteID) { + favoriteNode = node; + } } -#ifndef SP_REFACTOR /* ui manipulation */ - // Re-enable favorites table view - [favoritesTable setEnabled:YES]; - [favoritesTable display]; -#endif - - // Release the tunnel if set - will now be retained by the connection - if (sshTunnel) [sshTunnel release], sshTunnel = nil; - - // Pass the connection to the document and clean up the interface - [self addConnectionToDocument]; + + return favoriteNode; } /** - * Initiates the core of the MySQL connection process on a background thread. + * Strips any invalid characters form the supplied string. Invalid is defined as any characters that should + * not be allowed to be enetered on the connection screen. */ -- (void)_initiateMySQLConnectionInBackground +- (NSString *)_stripInvalidCharactersFromString:(NSString *)subject { - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - mySQLConnection = [[SPMySQLConnection alloc] init]; - - // Set up shared details - [mySQLConnection setUsername:[self user]]; - - // Initialise to socket if appropriate. - if ([self type] == SPSocketConnection) { - [mySQLConnection setUseSocket:YES]; - [mySQLConnection setSocketPath:[self socket]]; - - // Otherwise, initialise to host, using tunnel if appropriate - } else { - [mySQLConnection setUseSocket:NO]; - if ([self type] == SPSSHTunnelConnection) { - [mySQLConnection setHost:@"127.0.0.1"]; - [mySQLConnection setPort:[sshTunnel localPort]]; - [mySQLConnection setProxy:sshTunnel]; - } else { - [mySQLConnection setHost:[self host]]; - if ([[self port] length]) [mySQLConnection setPort:[[self port] integerValue]]; - } - } - - // Only set the password if there is no Keychain item set. The connection will ask the delegate for passwords in the Keychain. - if (!connectionKeychainItemName && [self password]) { - [mySQLConnection setPassword:[self password]]; - } + NSString *result = [subject stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + + return [result stringByReplacingOccurrencesOfString:@"\n" withString:@""]; +} - // Enable SSL if set - if ([self useSSL]) { - [mySQLConnection setUseSSL:YES]; - if ([self sslKeyFileLocationEnabled]) { - [mySQLConnection setSslKeyFilePath:[self sslKeyFileLocation]]; - } - if ([self sslCertificateFileLocationEnabled]) { - [mySQLConnection setSslCertificatePath:[self sslCertificateFileLocation]]; +/** + * Check all fields used in the keychain names against the old values for that + * favorite, and update the keychain names to match if necessary. + * If an (optional) recognised password field is supplied, that field is assumed + * to have changed and is used to supply the new value. + */ +- (void)_updateFavoritePasswordsFromField:(NSControl *)control +{ + if (!currentFavorite) return; + + NSDictionary *oldFavorite = [currentFavorite nodeFavorite]; + NSDictionary *newFavorite = [[[self selectedFavoriteNode] representedObject] nodeFavorite]; + + NSString *passwordValue; + NSString *oldKeychainName, *newKeychainName; + NSString *oldKeychainAccount, *newKeychainAccount; + NSString *oldHostnameForPassword = ([[oldFavorite objectForKey:SPFavoriteTypeKey] integerValue] == SPSocketConnection) ? @"localhost" : [oldFavorite objectForKey:SPFavoriteHostKey]; + NSString *newHostnameForPassword = ([[newFavorite objectForKey:SPFavoriteTypeKey] integerValue] == SPSocketConnection) ? @"localhost" : [newFavorite objectForKey:SPFavoriteHostKey]; + + // SQL passwords are indexed by name, host, user and database. If any of these + // have changed, or a standard password field has, alter the keychain item to match. + if (![[oldFavorite objectForKey:SPFavoriteNameKey] isEqualToString:[newFavorite objectForKey:SPFavoriteNameKey]] || + ![oldHostnameForPassword isEqualToString:newHostnameForPassword] || + ![[oldFavorite objectForKey:SPFavoriteUserKey] isEqualToString:[newFavorite objectForKey:SPFavoriteUserKey]] || + ![[oldFavorite objectForKey:SPFavoriteDatabaseKey] isEqualToString:[newFavorite objectForKey:SPFavoriteDatabaseKey]] || + control == standardPasswordField || control == socketPasswordField || control == sshPasswordField) + { + // Determine the correct password field to read the password from, defaulting to standard + if (control == socketPasswordField) { + passwordValue = [socketPasswordField stringValue]; + } + else if (control == sshPasswordField) { + passwordValue = [sshPasswordField stringValue]; + } + else { + passwordValue = [standardPasswordField stringValue]; } - if ([self sslCACertFileLocationEnabled]) { - [mySQLConnection setSslCACertificatePath:[self sslCACertFileLocation]]; + + // Get the old keychain name and account strings + oldKeychainName = [keychain nameForFavoriteName:[oldFavorite objectForKey:SPFavoriteNameKey] id:[newFavorite objectForKey:SPFavoriteIDKey]]; + oldKeychainAccount = [keychain accountForUser:[oldFavorite objectForKey:SPFavoriteUserKey] host:oldHostnameForPassword database:[oldFavorite objectForKey:SPFavoriteDatabaseKey]]; + + // Delete the old keychain item + [keychain deletePasswordForName:oldKeychainName account:oldKeychainAccount]; + + // Set up the new keychain name and account strings + newKeychainName = [keychain nameForFavoriteName:[newFavorite objectForKey:SPFavoriteNameKey] id:[newFavorite objectForKey:SPFavoriteIDKey]]; + newKeychainAccount = [keychain accountForUser:[newFavorite objectForKey:SPFavoriteUserKey] host:newHostnameForPassword database:[newFavorite objectForKey:SPFavoriteDatabaseKey]]; + + // Add the new keychain item if the password field has a value + if ([passwordValue length]) { + [keychain addPassword:passwordValue forName:newKeychainName account:newKeychainAccount]; } + + // Synch password changes + [standardPasswordField setStringValue:passwordValue]; + [socketPasswordField setStringValue:passwordValue]; + [sshPasswordField setStringValue:passwordValue]; + + passwordValue = @""; } - - // Connection delegate must be set before actual connection attempt is made - [mySQLConnection setDelegate:tableDocument]; - -#ifndef SP_REFACTOR /* set mysql connection settings from prefs */ - // Set whether or not we should enable delegate logging according to the prefs - [mySQLConnection setDelegateQueryLogging:[prefs boolForKey:SPConsoleEnableLogging]]; -#endif - - // Set options from preferences - [mySQLConnection setTimeout:[[prefs objectForKey:SPConnectionTimeoutValue] integerValue]]; - [mySQLConnection setUseKeepAlive:[[prefs objectForKey:SPUseKeepAlive] boolValue]]; - [mySQLConnection setKeepAliveInterval:[[prefs objectForKey:SPKeepAliveInterval] floatValue]]; - - // Connect - [mySQLConnection connect]; - - if (![mySQLConnection isConnected]) { - if (sshTunnel) { - - // If an SSH tunnel is running, temporarily block to allow the tunnel to register changes in state - [[NSRunLoop currentRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.2]]; - - // If the state is connection refused, attempt the MySQL connection again with the host using the hostfield value. - if ([sshTunnel state] == SPMySQLProxyForwardingFailed) { - if ([sshTunnel localPortFallback]) { - [mySQLConnection setPort:[sshTunnel localPortFallback]]; - [mySQLConnection connect]; - if (![mySQLConnection isConnected]) { - [[NSRunLoop currentRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.2]]; - } - } - } - } - - if (![mySQLConnection isConnected]) { - NSString *errorMessage = @""; - if (sshTunnel && [sshTunnel state] == SPMySQLProxyForwardingFailed) { - errorMessage = [NSString stringWithFormat:NSLocalizedString(@"Unable to connect to host %@ because the port connection via SSH was refused.\n\nPlease ensure that your MySQL host is set up to allow TCP/IP connections (no --skip-networking) and is configured to allow connections from the host you are tunnelling via.\n\nYou may also want to check the port is correct and that you have the necessary privileges.\n\nChecking the error detail will show the SSH debug log which may provide more details.\n\nMySQL said: %@", @"message of panel when SSH port forwarding failed"), [self host], [mySQLConnection lastErrorMessage]]; - [[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"SSH port forwarding failed", @"title when ssh tunnel port forwarding failed") errorMessage:errorMessage detail:[sshTunnel debugMessages]]; - } else if ([mySQLConnection lastErrorID] == 1045) { // "Access denied" error - errorMessage = [NSString stringWithFormat:NSLocalizedString(@"Unable to connect to host %@ because access was denied.\n\nDouble-check your username and password and ensure that access from your current location is permitted.\n\nMySQL said: %@", @"message of panel when connection to host failed due to access denied error"), [self host], [mySQLConnection lastErrorMessage]]; - [[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"Access denied!", @"connection failed due to access denied title") errorMessage:errorMessage detail:nil]; - } else if ([self type] == SPSocketConnection && (![self socket] || ![[self socket] length]) && ![SPMySQLConnection findSocketPath]) { - errorMessage = [NSString stringWithFormat:NSLocalizedString(@"The socket file could not be found in any common location. Please supply the correct socket location.\n\nMySQL said: %@", @"message of panel when connection to socket failed because optional socket could not be found"), [mySQLConnection lastErrorMessage]]; - [[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"Socket not found!", @"socket not found title") errorMessage:errorMessage detail:nil]; - } else if ([self type] == SPSocketConnection) { - errorMessage = [NSString stringWithFormat:NSLocalizedString(@"Unable to connect via the socket, or the request timed out.\n\nDouble-check that the socket path is correct and that you have the necessary privileges, and that the server is running.\n\nMySQL said: %@", @"message of panel when connection to host failed"), [mySQLConnection lastErrorMessage]]; - [[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"Socket connection failed!", @"socket connection failed title") errorMessage:errorMessage detail:nil]; - } else { - errorMessage = [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 %ld seconds).\n\nMySQL said: %@", @"message of panel when connection to host failed"), [self host], (long)[[prefs objectForKey:SPConnectionTimeoutValue] integerValue], [mySQLConnection lastErrorMessage]]; - [[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"Connection failed!", @"connection failed title") errorMessage:errorMessage detail:nil]; - } - - // Tidy up - isConnecting = NO; - - if (sshTunnel) [sshTunnel release], sshTunnel = nil; - - [mySQLConnection release], mySQLConnection = nil; -#ifndef SP_REFACTOR /* ui manipulation */ - [self _restoreConnectionInterface]; -#endif - [pool release]; - - return; + + // If SSH account/password details have changed, update the keychain to match + if (![[oldFavorite objectForKey:SPFavoriteNameKey] isEqualToString:[newFavorite objectForKey:SPFavoriteNameKey]] || + ![[oldFavorite objectForKey:SPFavoriteSSHHostKey] isEqualToString:[newFavorite objectForKey:SPFavoriteSSHHostKey]] || + ![[oldFavorite objectForKey:SPFavoriteSSHUserKey] isEqualToString:[newFavorite objectForKey:SPFavoriteSSHUserKey]] || + control == sshSSHPasswordField) + { + // Get the old keychain name and account strings + oldKeychainName = [keychain nameForSSHForFavoriteName:[oldFavorite objectForKey:SPFavoriteNameKey] id:[newFavorite objectForKey:SPFavoriteIDKey]]; + oldKeychainAccount = [keychain accountForSSHUser:[oldFavorite objectForKey:SPFavoriteSSHUserKey] sshHost:[oldFavorite objectForKey:SPFavoriteSSHHostKey]]; + + // Delete the old keychain item + [keychain deletePasswordForName:oldKeychainName account:oldKeychainAccount]; + + // Set up the new keychain name and account strings + newKeychainName = [keychain nameForSSHForFavoriteName:[newFavorite objectForKey:SPFavoriteNameKey] id:[newFavorite objectForKey:SPFavoriteIDKey]]; + newKeychainAccount = [keychain accountForSSHUser:[newFavorite objectForKey:SPFavoriteSSHUserKey] sshHost:[newFavorite objectForKey:SPFavoriteSSHHostKey]]; + + // Add the new keychain item if the password field has a value + if ([[sshPasswordField stringValue] length]) { + [keychain addPassword:[sshSSHPasswordField stringValue] forName:newKeychainName account:newKeychainAccount]; } } - - if ([self database] && ![[self database] isEqualToString:@""]) { - if (![mySQLConnection selectDatabase:[self database]]) { - [[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"Could not select database", @"message when database selection failed") errorMessage:[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"), [self database], [mySQLConnection lastErrorMessage]] detail:nil]; - - // Tidy up - isConnecting = NO; - - if (sshTunnel) [sshTunnel release], sshTunnel = nil; - - [mySQLConnection release], mySQLConnection = nil; -#ifndef SP_REFACTOR /* ui manipulation */ - [self _restoreConnectionInterface]; -#endif - [pool release]; - - return; - } + + // Update the current favorite + if (currentFavorite) [currentFavorite release], currentFavorite = nil; + + if ([[favoritesOutlineView selectedRowIndexes] count]) { + currentFavorite = [[[self selectedFavoriteNode] representedObject] copy]; } - - // Connection established - [self performSelectorOnMainThread:@selector(_mySQLConnectionEstablished) withObject:nil waitUntilDone:NO]; - - [pool release]; } -@end - -#ifndef SP_REFACTOR /* SPFlippedView */ - #pragma mark - -#pragma mark NSView subclass - flipped view for simpler drawing -/** - * Add an implementation of a flipped view to simplify drawing. - */ -@implementation SPFlippedView: NSView - -- (BOOL)isFlipped +- (void)dealloc { - return YES; + [keychain release]; + [prefs release]; + + [folderImage release], folderImage = nil; + + for (id retainedObject in nibObjectsToRelease) [retainedObject release]; + + [nibObjectsToRelease release]; + + if (mySQLConnection) [mySQLConnection release]; + if (sshTunnel) [sshTunnel setConnectionStateChangeSelector:nil delegate:nil], [sshTunnel disconnect], [sshTunnel release]; + if (connectionKeychainItemName) [connectionKeychainItemName release]; + if (connectionKeychainItemAccount) [connectionKeychainItemAccount release]; + if (connectionSSHKeychainItemName) [connectionSSHKeychainItemName release]; + if (connectionSSHKeychainItemAccount) [connectionSSHKeychainItemAccount release]; + if (currentFavorite) [currentFavorite release], currentFavorite = nil; + if (favoritesRoot) [favoritesRoot release], favoritesRoot = nil; + + [super dealloc]; } @end -#endif diff --git a/Source/SPConnectionControllerDataSource.h b/Source/SPConnectionControllerDataSource.h new file mode 100644 index 00000000..3f3aa7c0 --- /dev/null +++ b/Source/SPConnectionControllerDataSource.h @@ -0,0 +1,37 @@ +// +// $Id$ +// +// SPConnectionControllerDataSource.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on February 20, 2011 +// Copyright (c) 2011 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPConnectionController.h" + +/** + * @category SPConnectionControllerDelegate SPConnectionControllerDelegate.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * Connection controller data source category. + */ +@interface SPConnectionController (SPConnectionControllerDataSource) + +@end diff --git a/Source/SPConnectionControllerDataSource.m b/Source/SPConnectionControllerDataSource.m new file mode 100644 index 00000000..cb9c71bf --- /dev/null +++ b/Source/SPConnectionControllerDataSource.m @@ -0,0 +1,103 @@ +// +// $Id$ +// +// SPConnectionControllerDataSource.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on February 20, 2011 +// Copyright (c) 2011 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPConnectionControllerDataSource.h" +#import "SPFavoritesController.h" +#import "SPFavoriteNode.h" +#import "SPGroupNode.h" +#import "SPTreeNode.h" + +@interface SPConnectionController () + +- (void)_reloadFavoritesViewData; +- (void)_updateFavoritePasswordsFromField:(NSControl *)control; + +@end + +@implementation SPConnectionController (SPConnectionControllerDataSource) + +- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item +{ + SPTreeNode *node = (item == nil ? favoritesRoot : (SPTreeNode *)item); + + return [[node childNodes] count]; +} + +- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item +{ + SPTreeNode *node = (item == nil ? favoritesRoot : (SPTreeNode *)item); + + return NSArrayObjectAtIndex([node childNodes], index); +} + +- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item +{ + return [(SPTreeNode *)item isGroup]; +} + +- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item +{ + SPTreeNode *node = (SPTreeNode *)item; + + return (![node isGroup]) ? [[[node representedObject] nodeFavorite] objectForKey:SPFavoriteNameKey] : [[node representedObject] nodeName]; +} + +- (void)outlineView:(NSOutlineView *)outlineView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn byItem:(id)item +{ + NSString *newName = [object stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + + if ([newName length]) { + + // Get the node that was renamed + SPTreeNode *node = [self selectedFavoriteNode]; + + if (![node isGroup]) { + // Updating the name triggers a KVO update + [self setName:newName]; + + // Update associated Keychain items + [self _updateFavoritePasswordsFromField:nil]; + } + else { + [[node representedObject] setNodeName:newName]; + + [favoritesController saveFavorites]; + + [self _reloadFavoritesViewData]; + } + } +} + +- (id)outlineView:(NSOutlineView *)outlineView itemForPersistentObject:(id)object +{ + return [NSKeyedUnarchiver unarchiveObjectWithData:object]; +} + +- (id)outlineView:(NSOutlineView *)outlineView persistentObjectForItem:(id)item +{ + return [NSKeyedArchiver archivedDataWithRootObject:item]; +} + +@end diff --git a/Source/SPConnectionControllerDelegate.h b/Source/SPConnectionControllerDelegate.h index 03f13950..23d29b9b 100644 --- a/Source/SPConnectionControllerDelegate.h +++ b/Source/SPConnectionControllerDelegate.h @@ -24,14 +24,16 @@ // More info at <http://code.google.com/p/sequel-pro/> #import "SPConnectionController.h" +#import "SPFavoritesExportProtocol.h" +#import "SPFavoritesImportProtocol.h" /** * @category SPConnectionControllerDelegate SPConnectionControllerDelegate.h * * @author Stuart Connolly http://stuconnolly.com/ * - * Connection controller delegate/data source category. + * Connection controller delegate category. */ -@interface SPConnectionController (SPConnectionControllerDelegate) +@interface SPConnectionController (SPConnectionControllerDelegate) <SPFavoritesImportProtocol, SPFavoritesExportProtocol> @end diff --git a/Source/SPConnectionControllerDelegate.m b/Source/SPConnectionControllerDelegate.m index 85ddedab..352836e3 100644 --- a/Source/SPConnectionControllerDelegate.m +++ b/Source/SPConnectionControllerDelegate.m @@ -24,89 +24,32 @@ // More info at <http://code.google.com/p/sequel-pro/> #import "SPConnectionControllerDelegate.h" +#import "SPFavoritesController.h" #import "SPTableTextFieldCell.h" +#import "SPPreferenceController.h" +#import "SPGeneralPreferencePane.h" +#import "SPAppController.h" #import "SPFavoriteNode.h" +#import "SPGroupNode.h" +#import "SPTreeNode.h" -@implementation SPConnectionController (SPConnectionControllerDelegate) +#define CELL(cell) (SPTableTextFieldCell *)cell + +static NSString *SPDatabaseImage = @"database-small"; + +@interface SPConnectionController () + +- (void)_checkHost; +- (void)_sortFavorites; +- (void)_favoriteTypeDidChange; +- (void)_reloadFavoritesViewData; +- (void)_updateFavoritePasswordsFromField:(NSControl *)control; + +- (NSString *)_stripInvalidCharactersFromString:(NSString *)subject; -/*#pragma mark - - #pragma mark TableView drag & drop delegate methods - - - (BOOL)tableView:(NSTableView *)aTableView writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard *)pboard - { - NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject:rowIndexes]; - [pboard declareTypes:[NSArray arrayWithObject:favoritesPBoardType] owner:self]; - [pboard setData:archivedData forType:favoritesPBoardType]; - return YES; - } - - - (NSDragOperation)tableView:(NSTableView *)aTableView validateDrop:(id <NSDraggingInfo>)info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)operation - { - if (row == 0) return NSDragOperationNone; - if ([info draggingSource] == aTableView) - { - [aTableView setDropRow:row dropOperation:NSTableViewDropAbove]; - return NSDragOperationMove; - } - return NSDragOperationNone; - } - - - (BOOL)tableView:(NSTableView *)aTableView acceptDrop:(id <NSDraggingInfo>)info row:(NSInteger)row dropOperation:(NSTableViewDropOperation)operation - { - BOOL acceptedDrop = NO; - - if ((row == 0) || ([info draggingSource] != aTableView)) return acceptedDrop; - - // Disable all automatic sorting - currentSortItem = -1; - reverseFavoritesSort = NO; - - [prefs setInteger:currentSortItem forKey:SPFavoritesSortedBy]; - [prefs setBool:NO forKey:SPFavoritesSortedInReverse]; - - // Remove sort descriptors - [favorites sortUsingDescriptors:[NSArray array]]; - - // Uncheck sort by menu items - for (NSMenuItem *menuItem in [[favoritesSortByMenuItem submenu] itemArray]) - { - [menuItem setState:NSOffState]; - } - - NSPasteboard* pboard = [info draggingPasteboard]; - NSData* rowData = [pboard dataForType:favoritesPBoardType]; - NSIndexSet* rowIndexes = [NSKeyedUnarchiver unarchiveObjectWithData:rowData]; - NSInteger dragRow = [rowIndexes firstIndex]; - NSInteger defaultConnectionRow = [prefs integerForKey:SPLastFavoriteIndex]; - if (defaultConnectionRow == dragRow) - { - [prefs setInteger:row forKey:SPLastFavoriteIndex]; - } - NSMutableDictionary *draggedFavorite = [favorites objectAtIndex:dragRow]; - [favorites removeObjectAtIndex:dragRow]; - if (row > dragRow) - { - row--; - } - [favorites insertObject:draggedFavorite atIndex:row]; - [aTableView reloadData]; - - // reset the prefs with the new order - NSMutableArray *reorderedFavorites = [[NSMutableArray alloc] initWithArray:favorites]; - [reorderedFavorites removeObjectAtIndex:0]; - [prefs setObject:reorderedFavorites forKey:SPFavorites]; - - [[[[NSApp delegate] preferenceController] generalPreferencePane] updateDefaultFavoritePopup]; - - [reorderedFavorites release]; - - [self updateFavorites]; - [aTableView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO]; - - acceptedDrop = YES; - - return acceptedDrop; - }*/ +@end + +@implementation SPConnectionController (SPConnectionControllerDelegate) #pragma mark - #pragma mark SplitView delegate methods @@ -118,92 +61,502 @@ /** * When the split view is resized, trigger a resize in the hidden table - * width as well, to keep the connection view and connected view in synch. + * width as well, to keep the connection view and connected view in sync. */ -- (void)splitViewDidResizeSubviews:(NSNotification *)aNotification +- (void)splitViewDidResizeSubviews:(NSNotification *)notification { [databaseConnectionView setPosition:[[[connectionSplitView subviews] objectAtIndex:0] frame].size.width ofDividerAtIndex:0]; } #pragma mark - -#pragma mark Outline view datasource methods +#pragma mark Outline view delegate methods -- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item -{ - SPFavoriteNode *node = (item == nil ? favoritesRoot : (SPFavoriteNode *)item); +- (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item +{ + return ([[(SPTreeNode *)item parentNode] parentNode] == nil); +} + +- (void)outlineViewSelectionDidChange:(NSNotification *)notification +{ + NSInteger selected = [favoritesOutlineView numberOfSelectedRows]; - return [[node nodeChildren] count]; + if (selected == 1) { + + SPTreeNode *node = [self selectedFavoriteNode]; + + if (![node isGroup]) { + [self updateFavoriteSelection:self]; + + [addToFavoritesButton setEnabled:NO]; + + favoriteNameFieldWasTouched = YES; + + [connectionResizeContainer setHidden:NO]; + [connectionInstructionsTextField setStringValue:NSLocalizedString(@"Enter connection details below, or choose a favorite", @"enter connection details label")]; + } + else { + [connectionResizeContainer setHidden:YES]; + [connectionInstructionsTextField setStringValue:NSLocalizedString(@"Please choose a favorite", @"please choose a favorite connection view label")]; + } + } + else if (selected > 1) { + [connectionResizeContainer setHidden:YES]; + [connectionInstructionsTextField setStringValue:NSLocalizedString(@"Please choose a favorite", @"please choose a favorite connection view label")]; + } } -- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)anIndex ofItem:(id)item +- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item { - SPFavoriteNode *node = (item == nil ? favoritesRoot : (SPFavoriteNode *)item); + SPTreeNode *node = (SPTreeNode *)item; + + [CELL(cell) setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; - return NSArrayObjectAtIndex([node nodeChildren], anIndex); + [CELL(cell) setTextColor:([favoritesOutlineView isEnabled]) ? [NSColor blackColor] : [NSColor grayColor]]; + + if (![[node parentNode] parentNode]) { + [CELL(cell) setImage:nil]; + } + else { + [CELL(cell) setImage:(![node isGroup]) ? [NSImage imageNamed:SPDatabaseImage] : folderImage]; + } } -- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item -{ - return [(SPFavoriteNode *)item nodeIsGroup]; +- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item +{ + return ([[item parentNode] parentNode]) ? 17 : 22; } -- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item +- (NSString *)outlineView:(NSOutlineView *)outlineView toolTipForCell:(NSCell *)cell rect:(NSRectPointer)rect tableColumn:(NSTableColumn *)tableColumn item:(id)item mouseLocation:(NSPoint)mouseLocation { - SPFavoriteNode *node = (SPFavoriteNode *)item; + NSString *toolTip = nil; + + SPTreeNode *node = (SPTreeNode *)item; + + if (![node isGroup]) { + + NSString *favoriteName = [[[node representedObject] nodeFavorite] objectForKey:SPFavoriteNameKey]; + NSString *favoriteHostname = [[[node representedObject] nodeFavorite] objectForKey:SPFavoriteHostKey]; + + toolTip = ([favoriteHostname length]) ? [NSString stringWithFormat:@"%@ (%@)", favoriteName, favoriteHostname] : favoriteName; + } + // Only display a tooltip for group nodes that are a child of the root node + else if ([[node parentNode] parentNode]) { + NSUInteger favCount = [[node childNodes] count]; + + toolTip = [NSString stringWithFormat:@"%@ - %d %@", [[node representedObject] nodeName], favCount, (favCount == 1) ? NSLocalizedString(@"favorite", @"favorite singular label") : NSLocalizedString(@"favorites", @"favorites plural label")]; + } - return ([node nodeIsGroup]) ? [node nodeName] : [[node nodeFavorite] objectForKey:SPFavoriteNameKey]; + return toolTip; +} + +- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item +{ + return ([[item parentNode] parentNode] != nil); } #pragma mark - -#pragma mark Outline view delegate methods +#pragma mark Outline view drag & drop -- (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item +- (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard +{ + // If the user is in the process of changing a node's name, trigger a save and prevent dragging. + if (isEditing) { + [favoritesController saveFavorites]; + + [self _reloadFavoritesViewData]; + + isEditing = NO; + + return NO; + } + + [pboard declareTypes:[NSArray arrayWithObject:SPFavoritesPasteboardDragType] owner:self]; + + BOOL result = [pboard setData:[NSData data] forType:SPFavoritesPasteboardDragType]; + + draggedNodes = items; + + return result; +} + +- (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index { - return [(SPFavoriteNode *)item nodeIsGroup]; + NSDragOperation result = NSDragOperationNone; + + // Prevent dropping favorites on other favorites (non-groups) + if ((index == NSOutlineViewDropOnItemIndex) && (![item isGroup])) return result; + + if ([info draggingSource] == outlineView) { + [outlineView setDropItem:item dropChildIndex:index]; + + result = NSDragOperationMove; + } + + return result; } -- (void)outlineViewSelectionDidChange:(NSNotification *)notification +- (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index { - if ([favoritesTable numberOfSelectedRows] == 1) { - [self updateFavoriteSelection:self]; + BOOL acceptedDrop = NO; + + if ((!item) || ([info draggingSource] != outlineView)) return acceptedDrop; + + SPTreeNode *node = item ? item : [[[[favoritesRoot childNodes] objectAtIndex:0] childNodes] objectAtIndex:0]; - [addToFavoritesButton setEnabled:NO]; - } + // Disable all automatic sorting + currentSortItem = -1; + reverseFavoritesSort = NO; + + [prefs setInteger:currentSortItem forKey:SPFavoritesSortedBy]; + [prefs setBool:NO forKey:SPFavoritesSortedInReverse]; + + // Uncheck sort by menu items + for (NSMenuItem *menuItem in [[favoritesSortByMenuItem submenu] itemArray]) + { + [menuItem setState:NSOffState]; + } + + NSArray *nodes = draggedNodes; + + if (![nodes count]) return acceptedDrop; + + if ([node isGroup]) { + if (index == NSOutlineViewDropOnItemIndex) { + index = 0; + } + } else { - [addToFavoritesButton setEnabled:YES]; + if (index == NSOutlineViewDropOnItemIndex) { + index = 0; + } } + + if (![[node representedObject] nodeName]) { + node = [[favoritesRoot childNodes] objectAtIndex:0]; + } + + NSMutableArray *childNodeArray = [node mutableChildNodes]; + + for (SPTreeNode *treeNode in nodes) + { + // Remove the node from its old location + NSInteger oldIndex = [childNodeArray indexOfObject:treeNode]; + NSInteger newIndex = index; + + if (oldIndex != NSNotFound) { + + [childNodeArray removeObjectAtIndex:oldIndex]; + + if (index > oldIndex) { + newIndex--; + } + } + else { + [[[treeNode parentNode] mutableChildNodes] removeObject:treeNode]; + } + + [childNodeArray insertObject:treeNode atIndex:newIndex]; + + newIndex++; + } + + [favoritesController saveFavorites]; + + [self _reloadFavoritesViewData]; + + [[[[NSApp delegate] preferenceController] generalPreferencePane] updateDefaultFavoritePopup]; + + acceptedDrop = YES; + + return acceptedDrop; } -- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item +#pragma mark - +#pragma mark Textfield delegate methods + +/** + * Trap and control the 'name' field of the selected favorite. If the user pressed + * 'Add Favorite' the 'name' field is set to 'New Favorite'. If the user did not + * change the 'name' field or delete that field it will be set to user@host automatically. + */ +- (void)controlTextDidChange:(NSNotification *)notification { - [(SPTableTextFieldCell *)cell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + id field = [notification object]; - if ([favoritesTable isEnabled]) { - [(SPTableTextFieldCell *)cell setTextColor:[NSColor blackColor]]; + if (((field == standardNameField) || (field == socketNameField) || (field == sshNameField)) && [self selectedFavoriteNode]) { + + [field setStringValue:[self _stripInvalidCharactersFromString:[field stringValue]]]; + + favoriteNameFieldWasTouched = YES; + + BOOL nameFieldIsEmpty = [[field stringValue] isEqualToString:@""]; + + switch (previousType) + { + case SPTCPIPConnection: + + nameFieldIsEmpty = (nameFieldIsEmpty || [[standardNameField stringValue] isEqualToString:@""]); + + if (nameFieldIsEmpty || (!favoriteNameFieldWasTouched && (field == standardUserField || field == standardSQLHostField))) { + [standardNameField setStringValue:[NSString stringWithFormat:@"%@@%@", [standardUserField stringValue], [standardSQLHostField stringValue]]]; + + // Trigger KVO update + [self setName:[standardNameField stringValue]]; + + // If name field is empty enable user@host update + if (nameFieldIsEmpty) favoriteNameFieldWasTouched = NO; + } + + break; + case SPSocketConnection: + + nameFieldIsEmpty = (nameFieldIsEmpty || [[socketNameField stringValue] isEqualToString:@""]); + + if (nameFieldIsEmpty || (!favoriteNameFieldWasTouched && field == socketUserField)) { + [socketNameField setStringValue:[NSString stringWithFormat:@"%@@localhost", [socketUserField stringValue]]]; + + // Trigger KVO update + [self setName:[socketNameField stringValue]]; + + // If name field is empty enable user@host update + if (nameFieldIsEmpty) favoriteNameFieldWasTouched = NO; + } + + break; + case SPSSHTunnelConnection: + + nameFieldIsEmpty = (nameFieldIsEmpty || [[sshNameField stringValue] isEqualToString:@""]); + + if (nameFieldIsEmpty || (!favoriteNameFieldWasTouched && (field == sshUserField || field == sshSQLHostField))) { + [sshNameField setStringValue:[NSString stringWithFormat:@"%@@%@", [sshUserField stringValue], [sshSQLHostField stringValue]]]; + + // Trigger KVO update + [self setName:[sshNameField stringValue]]; + + // If name field is empty enable user@host update + if (nameFieldIsEmpty) favoriteNameFieldWasTouched = NO; + } + + break; + } } - else { - [(SPTableTextFieldCell *)cell setTextColor:[NSColor grayColor]]; +} + +/** + * When a host field finishes editing, ensure that it hasn't been set to "localhost" + * to ensure that socket connections don't inadvertently occur. + */ +- (void)controlTextDidEndEditing:(NSNotification *)notification +{ + if ([notification object] == standardSQLHostField || [notification object] == sshSQLHostField) { + [self _checkHost]; + } +} + +/** + * 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 +{ + // Request a password refresh to keep keychain references in sync with favorites, but only if a favorite + // is selected, meaning we're editing an existing one, not a new one. + if (((id)control != (id)favoritesOutlineView) && ([self selectedFavoriteNode])) { + [self _updateFavoritePasswordsFromField:control]; } - [(SPTableTextFieldCell *)cell setImage:([(SPFavoriteNode *)item nodeIsGroup]) ? nil : [NSImage imageNamed:@"database-small"]]; + // Proceed with editing + return YES; } -- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item +#pragma mark - +#pragma mark Tab bar delegate methods + +/** + * Trigger a resize action whenever the tab view changes. The connection + * detail forms are held within container views, which are of a fixed width; + * the tabview and buttons are contained within a resizable view which + * is set to dimensions based on the container views, allowing the view + * to be sized according to the detail type. + */ +- (void)tabView:(NSTabView *)tabView didSelectTabViewItem:(NSTabViewItem *)tabViewItem +{ + NSInteger selectedTabView = [tabView indexOfTabViewItem:tabViewItem]; + + // Deselect any selected favorite for manual changes + if (!automaticFavoriteSelection) [favoritesOutlineView deselectAll:self]; + automaticFavoriteSelection = NO; + + if (selectedTabView == previousType) return; + + [self resizeTabViewToConnectionType:selectedTabView animating:YES]; + + // Update the host as appropriate + if ((selectedTabView != SPSocketConnection) && [[self host] isEqualToString:@"localhost"]) { + [self setHost:@""]; + } + + previousType = selectedTabView; + + // Enable the add to favorites button + [addToFavoritesButton setEnabled:YES]; + + [self _favoriteTypeDidChange]; +} + +#pragma mark - +#pragma mark Scroll view notifications + +/** + * As the scrollview resizes, keep the details centered within it if + * the detail frame is larger than the scrollview size; otherwise, pin + * the detail frame to the top of the scrollview. + */ +- (void)scrollViewFrameChanged:(NSNotification *)aNotification { - return ([item nodeIsGroup]) ? 22 : 17; + NSRect scrollViewFrame = [connectionDetailsScrollView frame]; + NSRect scrollDocumentFrame = [[connectionDetailsScrollView documentView] frame]; + NSRect connectionDetailsFrame = [connectionResizeContainer frame]; + + // Scroll view is smaller than contents - keep positioned at top. + if (scrollViewFrame.size.height < connectionDetailsFrame.size.height + 10) { + if (connectionDetailsFrame.origin.y != 0) { + connectionDetailsFrame.origin.y = 0; + [connectionResizeContainer setFrame:connectionDetailsFrame]; + scrollDocumentFrame.size.height = connectionDetailsFrame.size.height + 10; + [[connectionDetailsScrollView documentView] setFrame:scrollDocumentFrame]; + } + } + // Otherwise, center + else { + connectionDetailsFrame.origin.y = (scrollViewFrame.size.height - connectionDetailsFrame.size.height)/3; + [connectionResizeContainer setFrame:connectionDetailsFrame]; + scrollDocumentFrame.size.height = scrollViewFrame.size.height; + [[connectionDetailsScrollView documentView] setFrame:scrollDocumentFrame]; + } } -- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item +#pragma mark - +#pragma mark Menu Validation + +/** + * Menu item validation. + */ +- (BOOL)validateMenuItem:(NSMenuItem *)menuItem { - return (![item nodeIsGroup]); + SEL action = [menuItem action]; + + SPTreeNode *node = [self selectedFavoriteNode]; + + if ((action == @selector(sortFavorites:)) || (action == @selector(reverseSortFavorites:))) { + + // Loop all the items in the sort by menu only checking the currently selected one + for (NSMenuItem *item in [[menuItem menu] itemArray]) + { + [item setState:([[menuItem menu] indexOfItem:item] == currentSortItem) ? NSOnState : NSOffState]; + } + + // Check or uncheck the reverse sort item + if (action == @selector(reverseSortFavorites:)) { + [menuItem setState:reverseFavoritesSort]; + } + } + + // Remove the selected favorite + if (action == @selector(removeNode:)) { + return ([favoritesOutlineView numberOfSelectedRows] == 1); + } + + // Duplicate and make the selected favorite the default + if (action == @selector(duplicateFavorite:)) { + return (([favoritesOutlineView numberOfSelectedRows] == 1) && (![node isGroup])); + } + + // Make selected favorite the default + if (action == @selector(makeSelectedFavoriteDefault:)) { + NSInteger favoriteID = [[[self selectedFavorite] objectForKey:SPFavoriteIDKey] integerValue]; + + return (([favoritesOutlineView numberOfSelectedRows] == 1) && (![node isGroup]) && (favoriteID != [prefs integerForKey:SPDefaultFavorite])); + } + + // Rename selected favorite/group + if (action == @selector(renameFavorite:)) { + return ([favoritesOutlineView numberOfSelectedRows] == 1); + } + + // Favorites export + if (action == @selector(exportFavorites:)) { + + NSInteger rows = [favoritesOutlineView numberOfSelectedRows]; + + if (rows > 1) { + [menuItem setTitle:NSLocalizedString(@"Export Selected...", @"export selected favorites menu item")]; + } + else if (rows == 1) { + return (![[self selectedFavoriteNode] isGroup]); + } + } + + return YES; } +#pragma mark - +#pragma mark Favorites import/export delegate methods + +/** + * Called by the favorites exporter when the export completes. + */ +- (void)favoritesExportCompletedWithError:(NSError *)error +{ + if (error) { + NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"Favorites export error", @"favorites export error message") + defaultButton:NSLocalizedString(@"OK", @"OK") + alternateButton:nil + otherButton:nil + informativeTextWithFormat:[NSString stringWithFormat:NSLocalizedString(@"The following error occurred during the export process:\n\n%@", @"favorites export error informative message"), [error localizedDescription]]]; + + [alert beginSheetModalForWindow:[dbDocument parentWindow] + modalDelegate:self + didEndSelector:NULL + contextInfo:NULL]; + } +} /** - * Prevent editing of outline view rows + * Called by the favorites importer when the imported data is available. */ -- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item +- (void)favoritesImportData:(NSArray *)data { - return NO; + // Add each of the imported favorites to the root node + for (NSMutableDictionary *favorite in data) + { + [favoritesController addFavoriteNodeWithData:favorite asChildOfNode:nil]; + } + + if (currentSortItem > SPFavoritesSortUnsorted) { + [self _sortFavorites]; + } + + [self _reloadFavoritesViewData]; +} + +/** + * Called by the favorites importer when the import completes. + */ +- (void)favoritesImportCompletedWithError:(NSError *)error +{ + if (error) { + NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"Favorites import error", @"favorites import error message") + defaultButton:NSLocalizedString(@"OK", @"OK") + alternateButton:nil + otherButton:nil + informativeTextWithFormat:[NSString stringWithFormat:NSLocalizedString(@"The following error occurred during the import process:\n\n%@", @"favorites import error informative message"), [error localizedDescription]]]; + + [alert beginSheetModalForWindow:[dbDocument parentWindow] + modalDelegate:self + didEndSelector:NULL + contextInfo:NULL]; + } } + @end diff --git a/Source/SPConnectionControllerInitializer.h b/Source/SPConnectionControllerInitializer.h new file mode 100644 index 00000000..7a0a0e1e --- /dev/null +++ b/Source/SPConnectionControllerInitializer.h @@ -0,0 +1,44 @@ +// +// $Id$ +// +// SPConnectionControllerInitializer.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on January 22, 2012 +// Copyright (c) 2012 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPConnectionController.h" + +/** + * @category SPConnectionControllerInitializer SPConnectionControllerInitializer.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * Connection controller initialization category. + */ +@interface SPConnectionController (SPConnectionControllerInitializer) + +- (id)initWithDocument:(SPDatabaseDocument *)document; + +- (void)loadNib; +- (void)registerForNotifications; +- (void)setUpFavoritesOutlineView; +- (void)setUpSelectedConnectionFavorite; + +@end diff --git a/Source/SPConnectionControllerInitializer.m b/Source/SPConnectionControllerInitializer.m new file mode 100644 index 00000000..34f9ca40 --- /dev/null +++ b/Source/SPConnectionControllerInitializer.m @@ -0,0 +1,271 @@ +// +// $Id$ +// +// SPConnectionControllerInitializer.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on January 22, 2012 +// Copyright (c) 2012 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPConnectionControllerInitializer.h" +#import "SPKeychain.h" +#import "SPFavoritesController.h" +#import "SPFavoriteNode.h" +#import "SPDatabaseViewController.h" + +static NSString *SPConnectionViewNibName = @"ConnectionView"; + +@interface SPConnectionController () + +- (void)_reloadFavoritesViewData; +- (void)_selectNode:(SPTreeNode *)node; + +- (SPTreeNode *)_favoriteNodeForFavoriteID:(NSInteger)favoriteID; + +@end + +@implementation SPConnectionController (SPConnectionControllerInitializer) + +/** + * Initialise the connection controller, linking it to the parent document and setting up the parent window. + */ +- (id)initWithDocument:(SPDatabaseDocument *)document +{ + if ((self = [super init])) { + + // Weak reference + dbDocument = document; + + databaseConnectionSuperview = [dbDocument databaseView]; + databaseConnectionView = [dbDocument valueForKey:@"contentViewSplitter"]; + + // Keychain references + connectionKeychainItemName = nil; + connectionKeychainItemAccount = nil; + connectionSSHKeychainItemName = nil; + connectionSSHKeychainItemAccount = nil; + + isEditing = NO; + isConnecting = NO; + sshTunnel = nil; + mySQLConnection = nil; + cancellingConnection = NO; + mySQLConnectionCancelled = NO; + favoriteNameFieldWasTouched = YES; + + [self loadNib]; + [self registerForNotifications]; + + // Hide the main view and position and display the connection view + [databaseConnectionView setHidden:YES]; + [connectionView setFrame:[databaseConnectionView frame]]; + [databaseConnectionSuperview addSubview:connectionView]; + [connectionSplitView setPosition:[[dbDocument valueForKey:@"dbTablesTableView"] frame].size.width ofDividerAtIndex:0]; + [connectionSplitView setDelegate:self]; + + // Generic folder image for use in the outline view's groups + folderImage = [[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kGenericFolderIcon)] retain]; + + [folderImage setSize:NSMakeSize(16, 16)]; + + // Set up a keychain instance and preferences reference, and create the initial favorites list + keychain = [[SPKeychain alloc] init]; + prefs = [[NSUserDefaults standardUserDefaults] retain]; + + // Create a reference to the favorites controller, forcing the data to be loaded from disk + // and the tree to be constructed. + favoritesController = [SPFavoritesController sharedFavoritesController]; + + // Tree reference + favoritesRoot = [favoritesController favoritesTree]; + + // Update the UI + [self _reloadFavoritesViewData]; + [self setUpFavoritesOutlineView]; + [self setUpSelectedConnectionFavorite]; + + // Set sort items + currentSortItem = [prefs integerForKey:SPFavoritesSortedBy]; + reverseFavoritesSort = [prefs boolForKey:SPFavoritesSortedInReverse]; + } + + return self; +} + +/** + * Loads the connection controllers UI nib. + */ +- (void)loadNib +{ + // Load the connection nib, keeping references to the top-level objects for later release + nibObjectsToRelease = [[NSMutableArray alloc] init]; + + NSArray *connectionViewTopLevelObjects = nil; + NSNib *nibLoader = [[NSNib alloc] initWithNibNamed:SPConnectionViewNibName bundle:[NSBundle mainBundle]]; + + [nibLoader instantiateNibWithOwner:self topLevelObjects:&connectionViewTopLevelObjects]; + [nibObjectsToRelease addObjectsFromArray:connectionViewTopLevelObjects]; + [nibLoader release]; +} + +/** + * Registers for various notifications. + */ +- (void)registerForNotifications +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(scrollViewFrameChanged:) + name:NSViewFrameDidChangeNotification + object:nil]; + + // Registered to be notified of changes to connection information + [self addObserver:self + forKeyPath:SPFavoriteNameKey + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; + + [self addObserver:self + forKeyPath:SPFavoriteHostKey + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; + + [self addObserver:self + forKeyPath:SPFavoriteUserKey + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; + + [self addObserver:self + forKeyPath:SPFavoriteDatabaseKey + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; + + [self addObserver:self + forKeyPath:SPFavoriteSocketKey + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; + + [self addObserver:self + forKeyPath:SPFavoritePortKey + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; + + [self addObserver:self + forKeyPath:SPFavoriteUseSSLKey + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; + + [self addObserver:self + forKeyPath:SPFavoriteSSHHostKey + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; + + [self addObserver:self + forKeyPath:SPFavoriteSSHUserKey + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; + + [self addObserver:self + forKeyPath:SPFavoriteSSHPortKey + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; + + [self addObserver:self + forKeyPath:SPFavoriteSSHKeyLocationEnabledKey + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; + + [self addObserver:self + forKeyPath:SPFavoriteSSHKeyLocationKey + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; + + [self addObserver:self + forKeyPath:SPFavoriteSSLKeyFileLocationEnabledKey + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; + + [self addObserver:self + forKeyPath:SPFavoriteSSLKeyFileLocationKey + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; + + [self addObserver:self + forKeyPath:SPFavoriteSSLCertificateFileLocationEnabledKey + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; + + [self addObserver:self + forKeyPath:SPFavoriteSSLCertificateFileLocationKey + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; + + [self addObserver:self + forKeyPath:SPFavoriteSSLCACertFileLocationEnabledKey + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; + + [self addObserver:self + forKeyPath:SPFavoriteSSLCACertFileLocationKey + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; +} + +/** + * Performs any set up necessary for the favorities outline view. + */ +- (void)setUpFavoritesOutlineView +{ + // Register double click action for the favorites outline view (double click favorite to connect) + [favoritesOutlineView setTarget:self]; + [favoritesOutlineView setDoubleAction:@selector(nodeDoubleClicked:)]; + + // Register drag types for the favorites outline view + [favoritesOutlineView registerForDraggedTypes:[NSArray arrayWithObject:SPFavoritesPasteboardDragType]]; + [favoritesOutlineView setDraggingSourceOperationMask:NSDragOperationMove forLocal:YES]; + + // Preserve expanded group nodes + [favoritesOutlineView setAutosaveExpandedItems:YES]; +} + +/** + * Sets up the selected connection favorite according to the user's preferences. + */ +- (void)setUpSelectedConnectionFavorite +{ + SPTreeNode *favorite = [self _favoriteNodeForFavoriteID:[prefs integerForKey:[prefs boolForKey:SPSelectLastFavoriteUsed] ? SPLastFavoriteID : SPDefaultFavorite]]; + + if (favorite) { + + NSNumber *typeNumber = [[[favorite representedObject] nodeFavorite] objectForKey:SPFavoriteTypeKey]; + + previousType = typeNumber ? [typeNumber integerValue] : SPTCPIPConnection; + + [self _selectNode:favorite]; + [self resizeTabViewToConnectionType:[[[[favorite representedObject] nodeFavorite] objectForKey:SPFavoriteTypeKey] integerValue] animating:NO]; + + [favoritesOutlineView scrollRowToVisible:[favoritesOutlineView selectedRow]]; + } + else { + previousType = SPTCPIPConnection; + + [self resizeTabViewToConnectionType:SPTCPIPConnection animating:NO]; + } +} + +@end diff --git a/Source/SPConnectionHandler.h b/Source/SPConnectionHandler.h new file mode 100644 index 00000000..bbff61dc --- /dev/null +++ b/Source/SPConnectionHandler.h @@ -0,0 +1,49 @@ +// +// $Id$ +// +// SPConnectionHandler.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on November 15, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPConnectionController.h" + +/** + * @category SPConnectionHandler SPConnectionHandler.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * Connection handler category. Handles all connection related non-interface processes. + */ +@interface SPConnectionController (SPConnectionHandler) + +- (void)initiateMySQLConnection; +- (void)initiateMySQLConnectionInBackground; +- (void)initiateSSHTunnelConnection; + +- (void)mySQLConnectionEstablished; +- (void)sshTunnelCallback:(SPSSHTunnel *)theTunnel; + +- (void)cancelConnection; +- (void)addConnectionToDocument; + +- (void)failConnectionWithTitle:(NSString *)theTitle errorMessage:(NSString *)theErrorMessage detail:(NSString *)errorDetail; + +@end diff --git a/Source/SPConnectionHandler.m b/Source/SPConnectionHandler.m new file mode 100644 index 00000000..bd03a8e0 --- /dev/null +++ b/Source/SPConnectionHandler.m @@ -0,0 +1,462 @@ +// +// $Id$ +// +// SPConnectionHandler.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on November 15, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPConnectionHandler.h" +#import "SPDatabaseDocument.h" +#import "SPAlertSheets.h" +#import "SPSSHTunnel.h" +#import "SPKeychain.h" +#import "RegexKitLite.h" + +#import <SPMySQL/SPMySQL.h> + +static NSString *SPLocalhostAddress = @"127.0.0.1"; + +@interface SPConnectionController () + +- (void)_restoreConnectionInterface; + +@end + +@implementation SPConnectionController (SPConnectionHandler) + +/* + * Set up the MySQL connection, either through a successful tunnel or directly in the background. + */ +- (void)initiateMySQLConnection +{ + [progressIndicatorText setStringValue:(sshTunnel) ? NSLocalizedString(@"MySQL connecting...", @"MySQL connecting very short status message") : NSLocalizedString(@"Connecting...", @"Generic connecting very short status message")]; + [progressIndicatorText display]; + + [connectButton setTitle:NSLocalizedString(@"Cancel", @"cancel button")]; + [connectButton setAction:@selector(cancelMySQLConnection:)]; + [connectButton setEnabled:YES]; + [connectButton display]; + + [NSThread detachNewThreadSelector:@selector(initiateMySQLConnectionInBackground) toTarget:self withObject:nil]; +} + +/** + * Initiates the core of the MySQL connection process on a background thread. + */ +- (void)initiateMySQLConnectionInBackground +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + mySQLConnection = [[SPMySQLConnection alloc] init]; + + // Set up shared details + [mySQLConnection setUsername:[self user]]; + + // Initialise to socket if appropriate. + if ([self type] == SPSocketConnection) { + [mySQLConnection setUseSocket:YES]; + [mySQLConnection setSocketPath:[self socket]]; + + // Otherwise, initialise to host, using tunnel if appropriate + } + else { + [mySQLConnection setUseSocket:NO]; + + if ([self type] == SPSSHTunnelConnection) { + [mySQLConnection setHost:@"127.0.0.1"]; + + [mySQLConnection setPort:[sshTunnel localPort]]; + [mySQLConnection setProxy:sshTunnel]; + } + else { + [mySQLConnection setHost:[self host]]; + + if ([[self port] length]) [mySQLConnection setPort:[[self port] integerValue]]; + } + } + + // Only set the password if there is no Keychain item set. The connection will ask the delegate for passwords in the Keychain. + if (!connectionKeychainItemName && [self password]) { + [mySQLConnection setPassword:[self password]]; + } + + // Enable SSL if set + if ([self useSSL]) { + [mySQLConnection setUseSSL:YES]; + + if ([self sslKeyFileLocationEnabled]) { + [mySQLConnection setSslKeyFilePath:[self sslKeyFileLocation]]; + } + + if ([self sslCertificateFileLocationEnabled]) { + [mySQLConnection setSslCertificatePath:[self sslCertificateFileLocation]]; + } + + if ([self sslCACertFileLocationEnabled]) { + [mySQLConnection setSslCACertificatePath:[self sslCACertFileLocation]]; + } + } + + // Connection delegate must be set before actual connection attempt is made + [mySQLConnection setDelegate:dbDocument]; + + // Set whether or not we should enable delegate logging according to the prefs + [mySQLConnection setDelegateQueryLogging:[prefs boolForKey:SPConsoleEnableLogging]]; + + // Set options from preferences + [mySQLConnection setTimeout:[[prefs objectForKey:SPConnectionTimeoutValue] integerValue]]; + [mySQLConnection setUseKeepAlive:[[prefs objectForKey:SPUseKeepAlive] boolValue]]; + [mySQLConnection setKeepAliveInterval:[[prefs objectForKey:SPKeepAliveInterval] floatValue]]; + + // Connect + [mySQLConnection connect]; + + if (![mySQLConnection isConnected]) { + if (sshTunnel) { + + // If an SSH tunnel is running, temporarily block to allow the tunnel to register changes in state + [[NSRunLoop currentRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.2]]; + + // If the state is connection refused, attempt the MySQL connection again with the host using the hostfield value. + if ([sshTunnel state] == SPMySQLProxyForwardingFailed) { + if ([sshTunnel localPortFallback]) { + [mySQLConnection setPort:[sshTunnel localPortFallback]]; + [mySQLConnection connect]; + + if (![mySQLConnection isConnected]) { + [[NSRunLoop currentRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.2]]; + } + } + } + } + + if (![mySQLConnection isConnected]) { + NSString *errorMessage = @""; + if (sshTunnel && [sshTunnel state] == SPMySQLProxyForwardingFailed) { + errorMessage = [NSString stringWithFormat:NSLocalizedString(@"Unable to connect to host %@ because the port connection via SSH was refused.\n\nPlease ensure that your MySQL host is set up to allow TCP/IP connections (no --skip-networking) and is configured to allow connections from the host you are tunnelling via.\n\nYou may also want to check the port is correct and that you have the necessary privileges.\n\nChecking the error detail will show the SSH debug log which may provide more details.\n\nMySQL said: %@", @"message of panel when SSH port forwarding failed"), [self host], [mySQLConnection lastErrorMessage]]; + [[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"SSH port forwarding failed", @"title when ssh tunnel port forwarding failed") errorMessage:errorMessage detail:[sshTunnel debugMessages]]; + } + else if ([mySQLConnection lastErrorID] == 1045) { // "Access denied" error + errorMessage = [NSString stringWithFormat:NSLocalizedString(@"Unable to connect to host %@ because access was denied.\n\nDouble-check your username and password and ensure that access from your current location is permitted.\n\nMySQL said: %@", @"message of panel when connection to host failed due to access denied error"), [self host], [mySQLConnection lastErrorMessage]]; + [[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"Access denied!", @"connection failed due to access denied title") errorMessage:errorMessage detail:nil]; + } + else if ([self type] == SPSocketConnection && (![self socket] || ![[self socket] length]) && ![mySQLConnection socketPath]) { + errorMessage = [NSString stringWithFormat:NSLocalizedString(@"The socket file could not be found in any common location. Please supply the correct socket location.\n\nMySQL said: %@", @"message of panel when connection to socket failed because optional socket could not be found"), [mySQLConnection lastErrorMessage]]; + [[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"Socket not found!", @"socket not found title") errorMessage:errorMessage detail:nil]; + } + else if ([self type] == SPSocketConnection) { + errorMessage = [NSString stringWithFormat:NSLocalizedString(@"Unable to connect via the socket, or the request timed out.\n\nDouble-check that the socket path is correct and that you have the necessary privileges, and that the server is running.\n\nMySQL said: %@", @"message of panel when connection to host failed"), [mySQLConnection lastErrorMessage]]; + [[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"Socket connection failed!", @"socket connection failed title") errorMessage:errorMessage detail:nil]; + } + else { + errorMessage = [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 %ld seconds).\n\nMySQL said: %@", @"message of panel when connection to host failed"), [self host], (long)[[prefs objectForKey:SPConnectionTimeoutValue] integerValue], [mySQLConnection lastErrorMessage]]; + [[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"Connection failed!", @"connection failed title") errorMessage:errorMessage detail:nil]; + } + + // Tidy up + isConnecting = NO; + + if (sshTunnel) [sshTunnel release], sshTunnel = nil; + + [mySQLConnection release], mySQLConnection = nil; + [self _restoreConnectionInterface]; + [pool release]; + + return; + } + } + + if ([self database] && ![[self database] isEqualToString:@""]) { + if (![mySQLConnection selectDatabase:[self database]]) { + [[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"Could not select database", @"message when database selection failed") errorMessage:[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"), [self database], [mySQLConnection lastErrorMessage]] detail:nil]; + + // Tidy up + isConnecting = NO; + + if (sshTunnel) [sshTunnel release], sshTunnel = nil; + + [mySQLConnection release], mySQLConnection = nil; + [self _restoreConnectionInterface]; + [pool release]; + + return; + } + } + + // Connection established + [self performSelectorOnMainThread:@selector(mySQLConnectionEstablished) withObject:nil waitUntilDone:NO]; + + [pool release]; +} + +/* + * Initiate the SSH connection process. + * This should only be called as part of initiateConnection:, and will indirectly + * call initiateMySQLConnection if it's successful. + */ +- (void)initiateSSHTunnelConnection +{ + [progressIndicatorText setStringValue:NSLocalizedString(@"SSH connecting...", @"SSH connecting very short status message")]; + [progressIndicatorText display]; + + // Trim whitespace and newlines from the SSH host field before attempting to connect + [self setSshHost:[[self sshHost] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]]; + + // Set up the tunnel details + sshTunnel = [[SPSSHTunnel alloc] initToHost:[self sshHost] port:[[self sshPort] integerValue] login:[self sshUser] tunnellingToPort:([[self port] length]?[[self port] integerValue]:3306) onHost:[self host]]; + [sshTunnel setParentWindow:[dbDocument parentWindow]]; + + // Add keychain or plaintext password as appropriate - note the checks in initiateConnection. + if (connectionSSHKeychainItemName) { + [sshTunnel setPasswordKeychainName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]; + } else if (sshPassword) { + [sshTunnel setPassword:[self sshPassword]]; + } + + // Set the public key path if appropriate + if (sshKeyLocationEnabled && sshKeyLocation) { + [sshTunnel setKeyFilePath:sshKeyLocation]; + } + + // Set the callback function on the tunnel + [sshTunnel setConnectionStateChangeSelector:@selector(sshTunnelCallback:) delegate:self]; + + // Ask the tunnel to connect. This will call the callback below on success or failure, passing + // itself as an argument - retain count should be one at this point. + [sshTunnel connect]; +} + +/** + * Called on the main thread once the MySQL connection is established on the background thread. Either the + * connection was cancelled or it was successful. + */ +- (void)mySQLConnectionEstablished +{ + isConnecting = NO; + + // If the user hit cancel during the connection attempt, kill the connection once + // established and reset the UI. + if (mySQLConnectionCancelled) { + if ([mySQLConnection isConnected]) { + [mySQLConnection disconnect]; + [mySQLConnection release], mySQLConnection = nil; + } + + // Kill the SSH connection if present + [self cancelConnection]; + + [self _restoreConnectionInterface]; + + return; + } + + [progressIndicatorText setStringValue:NSLocalizedString(@"Connected", @"connection established message")]; + [progressIndicatorText display]; + + // Stop the current tab's progress indicator + [dbDocument setIsProcessing:NO]; + + // Successful connection! + [connectButton setEnabled:NO]; + [connectButton display]; + [progressIndicator stopAnimation:self]; + [progressIndicatorText setHidden:YES]; + [addToFavoritesButton setHidden:NO]; + + // If SSL was enabled, check it was established correctly + if (useSSL && ([self type] == SPTCPIPConnection || [self type] == SPSocketConnection)) { + if (![mySQLConnection isConnectedViaSSL]) { + SPBeginAlertSheet(NSLocalizedString(@"SSL connection not established", @"SSL requested but not used title"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [dbDocument parentWindow], nil, nil, nil, NSLocalizedString(@"You requested that the connection should be established using SSL, but MySQL made the connection without SSL.\n\nThis may be because the server does not support SSL connections, or has SSL disabled; or insufficient details were supplied to establish an SSL connection.\n\nThis connection is not encrypted.", @"SSL connection requested but not established error detail")); + } + else { + [dbDocument setStatusIconToImageWithName:@"titlebarlock"]; + } + } + + // Re-enable favorites table view + [favoritesOutlineView setEnabled:YES]; + [(NSView *)favoritesOutlineView display]; + + // Release the tunnel if set - will now be retained by the connection + if (sshTunnel) [sshTunnel release], sshTunnel = nil; + + // Pass the connection to the document and clean up the interface + [self addConnectionToDocument]; +} + +/* + * A callback function for the SSH Tunnel setup process - will be called on a connection + * state change, allowing connection to fail or proceed as appropriate. If successful, + * will call initiateMySQLConnection. + */ +- (void)sshTunnelCallback:(SPSSHTunnel *)theTunnel +{ + if (cancellingConnection) return; + + NSInteger newState = [theTunnel state]; + + // If the user cancelled the password prompt dialog + if ([theTunnel passwordPromptCancelled]) { + [self _restoreConnectionInterface]; + + return; + } + + if (newState == SPMySQLProxyIdle) { + [dbDocument setTitlebarStatus:NSLocalizedString(@"SSH Disconnected", @"SSH disconnected titlebar marker")]; + + [self failConnectionWithTitle:NSLocalizedString(@"SSH connection failed!", @"SSH connection failed title") errorMessage:[theTunnel lastError] detail:[sshTunnel debugMessages]]; + + [self _restoreConnectionInterface]; + } + else if (newState == SPMySQLProxyConnected) { + [dbDocument setTitlebarStatus:NSLocalizedString(@"SSH Connected", @"SSH connected titlebar marker")]; + + [self initiateMySQLConnection]; + } + else { + [dbDocument setTitlebarStatus:NSLocalizedString(@"SSH Connecting…", @"SSH connecting titlebar marker")]; + } +} + +/* + * Cancel connection. + * Currently only cleans up the SSH connection (MySQL connection isn't threaded) + */ +- (void)cancelConnection +{ + if (!sshTunnel) return; + + cancellingConnection = YES; + + [sshTunnel disconnect]; + [sshTunnel release]; + + sshTunnel = nil; +} + +/** + * Add the connection to the parent document and restore the + * interface, allowing the application to run as normal. + */ +- (void)addConnectionToDocument +{ + // Hide the connection view and restore the main view + [connectionView removeFromSuperviewWithoutNeedingDisplay]; + [databaseConnectionView setHidden:NO]; + + // Restore the toolbar icons + NSArray *toolbarItems = [[[dbDocument parentWindow] toolbar] items]; + + for (NSUInteger i = 0; i < [toolbarItems count]; i++) [[toolbarItems objectAtIndex:i] setEnabled:YES]; + + if (connectionKeychainID) [dbDocument setKeychainID:connectionKeychainID]; + + // Pass the connection to the table document, allowing it to set + // up the other classes and the rest of the interface. + [dbDocument setConnection:mySQLConnection]; +} + +/* + * Ends a connection attempt by stopping the connection animation and + * displaying a specified error message. + */ +- (void)failConnectionWithTitle:(NSString *)theTitle errorMessage:(NSString *)theErrorMessage detail:(NSString *)errorDetail +{ + BOOL isSSHTunnelBindError = NO; + + // Clean up the interface + [progressIndicator stopAnimation:self]; + [progressIndicator display]; + [progressIndicatorText setHidden:YES]; + [progressIndicatorText display]; + [addToFavoritesButton setHidden:NO]; + [addToFavoritesButton display]; + [connectButton setEnabled:YES]; + [dbDocument clearStatusIcon]; + + // Release as appropriate + if (sshTunnel) { + [sshTunnel disconnect], [sshTunnel release], sshTunnel = nil; + + // If the SSH tunnel connection failed because the port it was trying to bind to was already in use take note + // of it so we can give the user the option of connecting via standard connection and use the existing tunnel. + if ([theErrorMessage rangeOfString:@"bind"].location != NSNotFound) { + isSSHTunnelBindError = YES; + } + } + + if (errorDetail) [errorDetailText setString:errorDetail]; + + // Inform the delegate that the connection attempt failed + if (delegate && [delegate respondsToSelector:@selector(connectionControllerConnectAttemptFailed:)]) { + [delegate connectionControllerConnectAttemptFailed:self]; + } + + // Only display the connection error message if there is a window visible and the connection attempt + // wasn't cancelled even though it failed. + if ([[dbDocument parentWindow] isVisible] && (!mySQLConnectionCancelled)) { + SPBeginAlertSheet(theTitle, NSLocalizedString(@"OK", @"OK button"), (errorDetail) ? NSLocalizedString(@"Show Detail", @"Show detail button") : nil, (isSSHTunnelBindError) ? NSLocalizedString(@"Use Standard Connection", @"use standard connection button") : nil, [dbDocument parentWindow], self, @selector(connectionFailureSheetDidEnd:returnCode:contextInfo:), @"connect", theErrorMessage); + } +} + +/** + * Alert sheet callback method - invoked when an error sheet is closed. + */ +- (void)connectionFailureSheetDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo +{ + // Restore the passwords from keychain for editing if appropriate + if (connectionKeychainItemName) { + [self setPassword:[keychain getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount]]; + } + + if (connectionSSHKeychainItemName) { + [self setSshPassword:[keychain getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]]; + } + + if (returnCode == NSAlertAlternateReturn) { + [errorDetailText setFont:[NSFont userFontOfSize:12]]; + [errorDetailText setAlignment:NSLeftTextAlignment]; + [errorDetailWindow makeKeyAndOrderFront:self]; + } + // Currently only SSH port bind errors offer a 3rd option in the error dialog, but if this ever changes + // this will definitely need to be updated. + else if (returnCode == NSAlertOtherReturn) { + + // Extract the local port number that SSH attempted to bind to from the debug output + NSString *tunnelPort = [[[errorDetailText string] componentsMatchedByRegex:@"LOCALHOST:([0-9]+)" capture:1L] lastObject]; + + // Change the connection type to standard TCP/IP + [self setType:SPTCPIPConnection]; + + // Change connection details + [self setPort:tunnelPort]; + [self setHost:SPLocalhostAddress]; + + // Change to standard TCP/IP connection view + [self resizeTabViewToConnectionType:SPTCPIPConnection animating:YES]; + + // Initiate the connection after a half second delay to give the connection view a chance to resize + [self performSelector:@selector(initiateConnection:) withObject:self afterDelay:0.5]; + } +} + +@end diff --git a/Source/SPConstants.h b/Source/SPConstants.h index ce53ceb3..c2775716 100644 --- a/Source/SPConstants.h +++ b/Source/SPConstants.h @@ -228,10 +228,6 @@ typedef enum // Narrow down completion max rows extern const NSUInteger SPNarrowDownCompletionMaxRows; -// Kill mode constants -extern NSString *SPKillProcessQueryMode; -extern NSString *SPKillProcessConnectionMode; - // Default monospaced font name extern NSString *SPDefaultMonospacedFontName; @@ -270,7 +266,7 @@ extern NSString *SPDataSupportFolder; // General Prefpane extern NSString *SPDefaultFavorite; extern NSString *SPSelectLastFavoriteUsed; -extern NSString *SPLastFavoriteIndex; +extern NSString *SPLastFavoriteID; extern NSString *SPAutoConnectToDefault; extern NSString *SPDefaultViewMode; extern NSString *SPLastViewMode; @@ -459,6 +455,9 @@ extern NSString *SPFavoriteSSLCertificateFileLocationKey; extern NSString *SPFavoriteSSLCACertFileLocationEnabledKey; extern NSString *SPFavoriteSSLCACertFileLocationKey; +// Favorites import/export +extern NSString *SPFavoritesDataRootKey; + // Bundle Files and Bundle Editor extern NSString *SPBundleScopeQueryEditor; extern NSString *SPBundleScopeDataTable; diff --git a/Source/SPConstants.m b/Source/SPConstants.m index ae9845fa..79de29a8 100644 --- a/Source/SPConstants.m +++ b/Source/SPConstants.m @@ -28,10 +28,6 @@ // Narrow down completion max rows const NSUInteger SPNarrowDownCompletionMaxRows = 15; -// Kill mode constants -NSString *SPKillProcessQueryMode = @"SPKillProcessQueryMode"; -NSString *SPKillProcessConnectionMode = @"SPKillProcessConnectionMode"; - // Default monospaced font name NSString *SPDefaultMonospacedFontName = @"Monaco"; @@ -69,7 +65,7 @@ NSString *SPDataSupportFolder = @"Data"; // General Prefpane NSString *SPDefaultFavorite = @"DefaultFavorite"; NSString *SPSelectLastFavoriteUsed = @"SelectLastFavoriteUsed"; -NSString *SPLastFavoriteIndex = @"LastFavoriteIndex"; +NSString *SPLastFavoriteID = @"SPLastFavoriteID"; NSString *SPAutoConnectToDefault = @"AutoConnectToDefault"; NSString *SPDefaultViewMode = @"DefaultViewMode"; NSString *SPLastViewMode = @"LastViewMode"; @@ -280,6 +276,9 @@ NSString *SPFavoriteSSLCertificateFileLocationKey = @"sslCertificateFileL NSString *SPFavoriteSSLCACertFileLocationEnabledKey = @"sslCACertFileLocationEnabled"; NSString *SPFavoriteSSLCACertFileLocationKey = @"sslCACertFileLocation"; +// Favorites import/export +NSString *SPFavoritesDataRootKey = @"SPConnectionFavorites"; + // Bundle Files and Bundle Editor NSString *SPBundleScopeQueryEditor = @"editor"; NSString *SPBundleScopeDataTable = @"datatable"; diff --git a/Source/SPDatabaseDocument.m b/Source/SPDatabaseDocument.m index a0545fbc..53dfb908 100644 --- a/Source/SPDatabaseDocument.m +++ b/Source/SPDatabaseDocument.m @@ -34,8 +34,8 @@ enum { #import "SPDatabaseDocument.h" #import "SPConnectionController.h" - -#import <SPMySQL/SPMySQL.h> +#import "SPConnectionHandler.h" +#import "SPConnectionControllerInitializer.h" #import "SPTablesList.h" #import "SPTableStructure.h" @@ -95,6 +95,8 @@ enum { #import "SPDatabaseRename.h" #endif +#import <SPMySQL/SPMySQL.h> + // Constants #ifndef SP_REFACTOR static NSString *SPCreateSyntx = @"SPCreateSyntax"; @@ -4103,7 +4105,7 @@ static NSString *SPRenameDatabaseAction = @"SPRenameDatabase"; // If the window is being set for the first time - connection controller is visible - update focus if (!parentWindow && !mySQLConnection) { #ifndef SP_REFACTOR - [aWindow makeFirstResponder:[connectionController valueForKey:@"favoritesTable"]]; + [aWindow makeFirstResponder:(NSResponder *)[connectionController favoritesOutlineView]]; #endif [connectionController performSelector:@selector(updateFavoriteSelection:) withObject:self afterDelay:0.0]; } @@ -4390,12 +4392,12 @@ static NSString *SPRenameDatabaseAction = @"SPRenameDatabase"; [self updateWindowTitle:self]; // Deselect all favorites on the connection controller - [[connectionController valueForKeyPath:@"favoritesTable"] deselectAll:connectionController]; + [[connectionController favoritesOutlineView] deselectAll:connectionController]; // Suppress the possibility to choose an other connection from the favorites // if a connection should initialized by SPF file. Otherwise it could happen // that the SPF file runs out of sync. - [[connectionController valueForKeyPath:@"favoritesTable"] setEnabled:NO]; + [[connectionController favoritesOutlineView] setEnabled:NO]; // Ensure the connection controller is set to a blank slate [connectionController setName:@""]; diff --git a/Source/SPFavoriteNode.h b/Source/SPFavoriteNode.h index d3b25854..ecd8aa28 100644 --- a/Source/SPFavoriteNode.h +++ b/Source/SPFavoriteNode.h @@ -28,38 +28,20 @@ * * @author Stuart Connolly http://stuconnolly.com/ * - * This class is designed to be a simple wrapper around a connection favorite to allow us to easily represent - * them in a tree structure for use in an outline view. If the node is a group item (i.e. a folder) then it - * should have a name as well as zero or more child nodes. Similarly, actual connection favorite nodes, don't - * have a name and should have no children. + * Tree node the represents a connection favorite. */ -@interface SPFavoriteNode : NSObject -{ - BOOL nodeIsGroup; - NSString *nodeName; - - NSDictionary *nodeFavorite; - NSMutableArray *nodeChildren; +@interface SPFavoriteNode : NSObject <NSCopying, NSCoding> +{ + NSMutableDictionary *nodeFavorite; } /** - * @property nodeIsGroup Indicates whether this node is a group item - */ -@property (readwrite, assign) BOOL nodeIsGroup; - -/** - * @property nodeName The node's name if it's a group item - */ -@property (readwrite, retain) NSString *nodeName; - -/** * @property nodeFavorite The actual favorite dictionary */ -@property (readwrite, retain) NSDictionary *nodeFavorite; +@property (readwrite, retain) NSMutableDictionary *nodeFavorite; -/** - * @property nodeChildren This node's children - */ -@property (readwrite, retain) NSMutableArray *nodeChildren; +- (id)initWithDictionary:(NSMutableDictionary *)dictionary; + ++ (SPFavoriteNode *)favoriteNodeWithDictionary:(NSMutableDictionary *)dictionary; @end diff --git a/Source/SPFavoriteNode.m b/Source/SPFavoriteNode.m index f8eb1745..2d124756 100644 --- a/Source/SPFavoriteNode.m +++ b/Source/SPFavoriteNode.m @@ -23,32 +23,82 @@ // // More info at <http://code.google.com/p/sequel-pro/> +#import "SPTreeNode.h" #import "SPFavoriteNode.h" +// Constants +static NSString *SPFavoriteNodeKey = @"SPFavoriteNode"; + @implementation SPFavoriteNode -@synthesize nodeIsGroup; -@synthesize nodeName; @synthesize nodeFavorite; -@synthesize nodeChildren; + +#pragma mark - +#pragma mark Initialisation - (id)init { if ((self = [super init])) { - [self setNodeIsGroup:NO]; - [self setNodeName:nil]; [self setNodeFavorite:nil]; - [self setNodeChildren:[NSMutableArray array]]; } return self; } +- (id)initWithDictionary:(NSMutableDictionary *)dictionary +{ + if ((self = [self init])) { + [self setNodeFavorite:dictionary]; + } + + return self; +} + ++ (SPFavoriteNode *)favoriteNodeWithDictionary:(NSMutableDictionary *)dictionary +{ + return [[[self alloc] initWithDictionary:dictionary] autorelease]; +} + +#pragma mark - +#pragma mark Copying protocol methods + +- (id)copyWithZone:(NSZone *)zone +{ + SPFavoriteNode *node = [[[self class] allocWithZone:zone] init]; + + [node setNodeFavorite:[[self nodeFavorite] copyWithZone:zone]]; + + return node; +} + +#pragma mark - +#pragma mark Coding protocol methods + +- (id)initWithCoder:(NSCoder *)coder +{ + [self setNodeFavorite:[coder decodeObjectForKey:SPFavoriteNodeKey]]; + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeObject:[self nodeFavorite] forKey:SPFavoriteNodeKey]; +} + +#pragma mark - +#pragma mark Other + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p ('%@')>", [self className], self, [[self nodeFavorite] objectForKey:SPFavoriteNameKey]]; +} + +#pragma mark - + - (void)dealloc { - if (nodeName) [nodeName release], nodeName = nil; if (nodeFavorite) [nodeFavorite release], nodeFavorite = nil; - if (nodeChildren) [nodeChildren release], nodeChildren = nil; [super dealloc]; } diff --git a/Source/SPFavoritesController.h b/Source/SPFavoritesController.h index 1b9f0aca..e15ab385 100644 --- a/Source/SPFavoritesController.h +++ b/Source/SPFavoritesController.h @@ -25,6 +25,8 @@ #import "SPSingleton.h" +@class SPTreeNode; + /** * @class SPFavoritesController SPFavoritesController.h * @@ -35,17 +37,31 @@ */ @interface SPFavoritesController : SPSingleton { - NSMutableDictionary *favorites; + SPTreeNode *favoritesTree; + NSMutableDictionary *favoritesData; + + pthread_mutex_t writeLock; + pthread_mutex_t favoritesLock; } /** - * @property favorites Favorites data dictionary + * @property favoritesTree The current favorites tree + */ +@property (readonly) SPTreeNode *favoritesTree; + +/** + * @property favoritesData Favorites data dictionary */ -@property (readonly) NSMutableDictionary *favorites; +@property (readonly) NSMutableDictionary *favoritesData; + (SPFavoritesController *)sharedFavoritesController; - (void)saveFavorites; - (void)reloadFavoritesWithSave:(BOOL)save; +- (SPTreeNode *)addGroupNodeWithName:(NSString *)name asChildOfNode:(SPTreeNode *)parent; +- (SPTreeNode *)addFavoriteNodeWithData:(NSMutableDictionary *)data asChildOfNode:(SPTreeNode *)parent; + +- (void)removeFavoriteNode:(SPTreeNode *)node; + @end diff --git a/Source/SPFavoritesController.m b/Source/SPFavoritesController.m index 222dd119..c53bbae3 100644 --- a/Source/SPFavoritesController.m +++ b/Source/SPFavoritesController.m @@ -24,18 +24,28 @@ // More info at <http://code.google.com/p/sequel-pro/> #import "SPFavoritesController.h" +#import "SPFavoriteNode.h" +#import "SPTreeNode.h" +#import "SPGroupNode.h" +#import "pthread.h" static SPFavoritesController *sharedFavoritesController = nil; -@interface SPFavoritesController (PrivateAPI) +@interface SPFavoritesController () - (void)_loadFavorites; +- (void)_constructFavoritesTree; +- (void)_saveFavoritesDataInBackground:(NSDictionary *)data; +- (void)_addNode:(SPTreeNode *)node asChildOfNode:(SPTreeNode *)parent; + +- (SPTreeNode *)_constructBranchForNodeData:(NSDictionary *)nodeData; @end @implementation SPFavoritesController -@synthesize favorites; +@synthesize favoritesTree; +@synthesize favoritesData; #pragma mark - #pragma mark Initialisation @@ -52,19 +62,25 @@ static SPFavoritesController *sharedFavoritesController = nil; { if ((self = [super init])) { - favorites = nil; + favoritesTree = nil; + favoritesData = nil; + + pthread_mutex_init(&writeLock, NULL); + pthread_mutex_init(&favoritesLock, NULL); [self _loadFavorites]; + [self _constructFavoritesTree]; } return self; } #pragma mark - -#pragma mark Public API /** * Returns the shared favorites controller. + * + * @return The shared controller instance. */ + (SPFavoritesController *)sharedFavoritesController { @@ -77,6 +93,9 @@ static SPFavoritesController *sharedFavoritesController = nil; return sharedFavoritesController; } +#pragma mark - +#pragma mark Favorites data handling + /** * Saves the current favorites dictionary in memory to disk. Note that the current favorites data file is moved * rather than overwritten in the event that we can't write the new file, the original can simply be restored. @@ -84,129 +103,333 @@ static SPFavoritesController *sharedFavoritesController = nil; */ - (void)saveFavorites { - NSError *error = nil; - NSString *errorString = nil; + pthread_mutex_lock(&favoritesLock); + + [NSThread detachNewThreadSelector:@selector(_saveFavoritesDataInBackground:) + toTarget:self + withObject:[[[favoritesTree childNodes] objectAtIndex:0] dictionaryRepresentation]]; + + pthread_mutex_unlock(&favoritesLock); +} + +/** + * Reloads the favorites data from disk with the option to save before doing so. + * + * @param save Indicates whether the current favorites data in memory should be saved to disk before being + * reloaded. Specifying NO effectively discards any changes since the last save operation. + */ +- (void)reloadFavoritesWithSave:(BOOL)save +{ + if (save) [self saveFavorites]; + + if (favoritesData) { + [self _loadFavorites]; + [self _constructFavoritesTree]; + } +} + +#pragma mark - +#pragma mark Favorites interaction + +/** + * Adds a new group node with the supplied name to the children of the supplied parent node. + * + * @param name The name of the new group + * @param parent + * + * @return The node instance that was created and added + */ +- (SPTreeNode *)addGroupNodeWithName:(NSString *)name asChildOfNode:(SPTreeNode *)parent +{ + SPTreeNode *node = [SPTreeNode treeNodeWithRepresentedObject:[SPGroupNode groupNodeWithName:name]]; + + [node setIsGroup:YES]; + + [self _addNode:node asChildOfNode:parent]; + + return node; +} + +/** + * Adds a new favorite node with the supplied data to the children of the supplied parent node. + * + * @param data The data for the new favorite + * @param + * + * @return The node instance that was created and added + */ +- (SPTreeNode *)addFavoriteNodeWithData:(NSMutableDictionary *)data asChildOfNode:(SPTreeNode *)parent +{ + SPTreeNode *node = [SPTreeNode treeNodeWithRepresentedObject:[SPFavoriteNode favoriteNodeWithDictionary:data]]; + + [self _addNode:node asChildOfNode:parent]; + + return node; +} + +/** + * Removes the supplied favorite node by asking the root node to remove it from it's children (i.e. the + * entire tree is searched. + * + * @param The node to be removed + */ +- (void)removeFavoriteNode:(SPTreeNode *)node +{ + [favoritesTree removeObjectFromChildren:node]; + + // Save data to disk + [self saveFavorites]; +} + +#pragma mark - +#pragma mark Private API + +/** + * Attempts to load the users connection favorites from ~/Library/Application Support/Sequel Pro/Data/Favorites.plist + * If the 'Data' directory doesn't already exist it will be created, as well as an empty favorites plist. + */ +- (void)_loadFavorites +{ + pthread_mutex_lock(&favoritesLock); + NSError *error = nil; NSFileManager *fileManager = [NSFileManager defaultManager]; + if (favoritesData) [favoritesData release], favoritesData = nil; + NSString *dataPath = [fileManager applicationSupportDirectoryForSubDirectory:SPDataSupportFolder error:&error]; if (error) { NSLog(@"Error retrieving data directory path: %@", [error localizedDescription]); + + pthread_mutex_unlock(&favoritesLock); + return; } NSString *favoritesFile = [dataPath stringByAppendingPathComponent:SPFavoritesDataFile]; - NSString *favoritesBackupFile = [dataPath stringByAppendingPathComponent:[@"~" stringByAppendingString:SPFavoritesDataFile]]; - // If the favorites data file already exists, attempt to move it to keep as a backup + // If the favorites data file already exists use it, otherwise create an empty one if ([fileManager fileExistsAtPath:favoritesFile]) { - [fileManager moveItemAtPath:favoritesFile toPath:favoritesBackupFile error:&error]; + favoritesData = [[NSDictionary alloc] initWithContentsOfFile:favoritesFile]; } - - if (error) { - NSLog(@"Unable to backup (move) existing favorites data file during save. Deleting instead: %@", [error localizedDescription]); + else { + NSMutableDictionary *newFavorites = [NSMutableDictionary dictionaryWithObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:NSLocalizedString(@"Favorites", @"favorites label"), SPFavoritesGroupNameKey, [NSArray array], SPFavoriteChildrenKey, nil] forKey:SPFavoritesRootKey]; error = nil; + NSString *errorString = nil; - // We can't move it so try and delete it - if (![fileManager removeItemAtPath:favoritesFile error:&error] && error) { - NSLog(@"Unable to delete existing favorites data file during save. Something is wrong, permissions perhaps: %@", [error localizedDescription]); - return; - } - } - else { - NSData *plistData = [NSPropertyListSerialization dataFromPropertyList:favorites + NSData *plistData = [NSPropertyListSerialization dataFromPropertyList:newFavorites format:NSPropertyListXMLFormat_v1_0 errorDescription:&errorString]; - if (plistData) { [plistData writeToFile:favoritesFile options:NSAtomicWrite error:&error]; if (error) { - NSLog(@"Error writing favorites data. Restoring backup if available: %@", [error localizedDescription]); - - // Restore the original data file - [fileManager moveItemAtPath:favoritesBackupFile toPath:favoritesFile error:NULL]; - } - else { - // Remove the original backup - [fileManager removeItemAtPath:favoritesBackupFile error:NULL]; + NSLog(@"Error writing default favorites data: %@", [error localizedDescription]); } } else if (errorString) { - NSLog(@"Error converting favorites data to plist format: %@", errorString); + NSLog(@"Error converting default favorites data to plist format: %@", errorString); [errorString release]; + + pthread_mutex_unlock(&favoritesLock); + + return; } + + favoritesData = newFavorites; } + + pthread_mutex_unlock(&favoritesLock); } /** - * Reloads the favorites data from disk with the option to save before doing so. - * - * @param save Indicates whether the current favorites data in memory should be saved to disk before being - * reloaded. Specifying NO effectively discards any changes since the last save operation. + * Constructs the favorites tree by initialising an instance of SPFavoriteNode for every favorite and group. */ -- (void)reloadFavoritesWithSave:(BOOL)save +- (void)_constructFavoritesTree { - if (save) [self saveFavorites]; + pthread_mutex_lock(&favoritesLock); - if (favorites) [self _loadFavorites]; + if (!favoritesData) { + pthread_mutex_unlock(&favoritesLock); + return; + } + + NSDictionary *root = [favoritesData objectForKey:SPFavoritesRootKey]; + + SPGroupNode *rootGroupNode = [[SPGroupNode alloc] init]; + SPGroupNode *favoritesGroupNode = [[SPGroupNode alloc] initWithName:[[root objectForKey:SPFavoritesGroupNameKey] uppercaseString]]; + + SPTreeNode *rootNode = [[SPTreeNode alloc] initWithRepresentedObject:rootGroupNode]; + SPTreeNode *favoritesNode = [[SPTreeNode alloc] initWithRepresentedObject:favoritesGroupNode]; + + [rootNode setIsGroup:YES]; + [favoritesNode setIsGroup:YES]; + + for (NSDictionary *favorite in [root objectForKey:SPFavoriteChildrenKey]) + { + SPTreeNode *node = [self _constructBranchForNodeData:favorite]; + + [[favoritesNode mutableChildNodes] addObject:node]; + + [node release]; + } + + [[rootNode mutableChildNodes] addObject:favoritesNode]; + + [rootGroupNode release]; + [favoritesGroupNode release]; + [favoritesNode release]; + + favoritesTree = rootNode; + + pthread_mutex_unlock(&favoritesLock); } -#pragma mark - -#pragma mark Private API +/** + * Constructs the tree branch for the supplied favorites data. Note that depending on the contents of the + * branch (i.e. does it contain any groups and their depth) this method will recursively call itself. + * + * @param nodeData The favorites data dictionary + * + * @return The root node of the branch + */ +- (SPTreeNode *)_constructBranchForNodeData:(NSDictionary *)nodeData +{ + id node = nil; + SPTreeNode *treeNode = nil; + + if ([nodeData objectForKey:SPFavoritesGroupNameKey] && [nodeData objectForKey:SPFavoriteChildrenKey]) { + + node = [[SPGroupNode alloc] initWithName:[nodeData objectForKey:SPFavoritesGroupNameKey]]; + + treeNode = [[SPTreeNode alloc] initWithRepresentedObject:node]; + + [treeNode setIsGroup:YES]; + + for (NSDictionary *favorite in [nodeData objectForKey:SPFavoriteChildrenKey]) + { + SPTreeNode *innerNode = [self _constructBranchForNodeData:favorite]; + + [[treeNode mutableChildNodes] addObject:innerNode]; + + [innerNode release]; + } + } + else { + node = [[SPFavoriteNode alloc] initWithDictionary:nodeData]; + + treeNode = [[SPTreeNode alloc] initWithRepresentedObject:node]; + } + + return treeNode; +} /** - * Attempts to load the users connection favorites from ~/Library/Application Support/Sequel Pro/Data/Favorites.plist - * If the 'Data' directory doesn't already exist it will be created, as well as an empty favorites plist. + * Saves the supplied favorites data to disk on a background thread. + * + * @param data The raw plist data (serialized NSDictionary) to be saved */ -- (void)_loadFavorites +- (void)_saveFavoritesDataInBackground:(NSDictionary *)data { + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + pthread_mutex_lock(&writeLock); + + if (!favoritesTree) { + pthread_mutex_unlock(&writeLock); + return; + } + NSError *error = nil; - NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *errorString = nil; - if (favorites) [favorites release], favorites = nil; + NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *dataPath = [fileManager applicationSupportDirectoryForSubDirectory:SPDataSupportFolder error:&error]; if (error) { NSLog(@"Error retrieving data directory path: %@", [error localizedDescription]); + + pthread_mutex_unlock(&writeLock); return; } NSString *favoritesFile = [dataPath stringByAppendingPathComponent:SPFavoritesDataFile]; + NSString *favoritesBackupFile = [dataPath stringByAppendingPathComponent:[@"~" stringByAppendingString:SPFavoritesDataFile]]; - // If the favorites data file already exists use it, otherwise create an empty one + // If the favorites data file already exists, attempt to move it to keep as a backup if ([fileManager fileExistsAtPath:favoritesFile]) { - favorites = [[NSDictionary alloc] initWithContentsOfFile:favoritesFile]; + [fileManager moveItemAtPath:favoritesFile toPath:favoritesBackupFile error:&error]; } - else { - NSMutableDictionary *newFavorites = [NSMutableDictionary dictionaryWithObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:NSLocalizedString(@"Favorites", @"favorites label"), SPFavoritesGroupNameKey, [NSArray array], SPFavoriteChildrenKey, nil] forKey:SPFavoritesRootKey]; + + if (error) { + NSLog(@"Unable to backup (move) existing favorites data file during save. Deleting instead: %@", [error localizedDescription]); error = nil; - NSString *errorString = nil; - NSData *plistData = [NSPropertyListSerialization dataFromPropertyList:newFavorites + // We can't move it so try and delete it + if (![fileManager removeItemAtPath:favoritesFile error:&error] && error) { + NSLog(@"Unable to delete existing favorites data file during save. Something is wrong, permissions perhaps: %@", [error localizedDescription]); + + pthread_mutex_unlock(&writeLock); + return; + } + } + else { + NSDictionary *dictionary = [NSDictionary dictionaryWithObject:data forKey:SPFavoritesRootKey]; + + // Convert the current favorites tree to a dictionary representation to create the plist data + NSData *plistData = [NSPropertyListSerialization dataFromPropertyList:dictionary format:NSPropertyListXMLFormat_v1_0 errorDescription:&errorString]; + if (plistData) { [plistData writeToFile:favoritesFile options:NSAtomicWrite error:&error]; if (error) { - NSLog(@"Error writing default favorites data: %@", [error localizedDescription]); + NSLog(@"Error writing favorites data. Restoring backup if available: %@", [error localizedDescription]); + + // Restore the original data file + [fileManager moveItemAtPath:favoritesBackupFile toPath:favoritesFile error:NULL]; + } + else { + // Remove the original backup + [fileManager removeItemAtPath:favoritesBackupFile error:NULL]; } } else if (errorString) { - NSLog(@"Error converting default favorites data to plist format: %@", errorString); + NSLog(@"Error converting favorites data to plist format: %@", errorString); [errorString release]; - return; + + [fileManager removeItemAtPath:favoritesBackupFile error:NULL]; } - - favorites = newFavorites; } + + pthread_mutex_unlock(&writeLock); + + [pool release]; +} + +/** + * Adds the supplied node to the children of the supplied parent and saves the tree to disk. + * + * @param node The node to be added + * @param asChild + */ +- (void)_addNode:(SPTreeNode *)node asChildOfNode:(SPTreeNode *)parent +{ + if (parent) { + [[parent mutableChildNodes] addObject:node]; + } + else { + [[[[favoritesTree mutableChildNodes] objectAtIndex:0] mutableChildNodes] addObject:node]; + } + + [self saveFavorites]; } #pragma mark - @@ -216,7 +439,11 @@ static SPFavoritesController *sharedFavoritesController = nil; */ - (void)dealloc { - if (favorites) [favorites release], favorites = nil; + if (favoritesTree) [favoritesTree release], favoritesTree = nil; + if (favoritesData) [favoritesData release], favoritesData = nil; + + pthread_mutex_destroy(&writeLock); + pthread_mutex_destroy(&favoritesLock); [super dealloc]; } diff --git a/Source/SPFavoritesExportProtocol.h b/Source/SPFavoritesExportProtocol.h new file mode 100644 index 00000000..ebd7bab5 --- /dev/null +++ b/Source/SPFavoritesExportProtocol.h @@ -0,0 +1,42 @@ +// +// $Id$ +// +// SPFavoritesExportProtocol.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on June 11, 2011 +// Copyright (c) 2011 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +/** + * @protocol SPFavoritesExportProtocol SPFavoritesExportProtocol.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * Favorites exporter delegate protocol. + */ +@protocol SPFavoritesExportProtocol + +/** + * Invoked when the favorites export proccess completes. + * + * @param error An error instance. Anything other than nil indicates an error occurred. + */ +- (void)favoritesExportCompletedWithError:(NSError *)error; + +@end diff --git a/Source/SPFavoritesExporter.h b/Source/SPFavoritesExporter.h new file mode 100644 index 00000000..2ffb2a21 --- /dev/null +++ b/Source/SPFavoritesExporter.h @@ -0,0 +1,50 @@ +// +// $Id$ +// +// SPFavoritesExporter.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on May 14, 2011 +// Copyright (c) 2011 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPFavoritesExportProtocol.h" + +@interface SPFavoritesExporter : NSObject +{ + NSObject <SPFavoritesExportProtocol> *delegate; + + NSString *exportPath; + NSArray *exportFavorites; +} + +@property (readwrite, assign) NSObject <SPFavoritesExportProtocol> *delegate; + +/** + * @property exportPath The file path to export to + */ +@property (readwrite, retain) NSString *exportPath; + +/** + * @property exportFavorites The array of favorites to be exported + */ +@property (readwrite, retain) NSArray *exportFavorites; + +- (void)writeFavorites:(NSArray *)favorites toFile:(NSString *)path; + +@end diff --git a/Source/SPFavoritesExporter.m b/Source/SPFavoritesExporter.m new file mode 100644 index 00000000..66590c30 --- /dev/null +++ b/Source/SPFavoritesExporter.m @@ -0,0 +1,111 @@ +// +// $Id$ +// +// SPFavoritesExporter.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on May 14, 2011 +// Copyright (c) 2011 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPFavoritesExporter.h" +#import "SPTreeNode.h" + +@interface SPFavoritesExporter () + +- (void)_writeFavoritesInBackground; +- (void)_informDelegateOfExportCompletion:(NSError *)error; + +@end + +@implementation SPFavoritesExporter + +@synthesize delegate; +@synthesize exportPath; +@synthesize exportFavorites; + +/*** + * Write the supplied array of favorites to the file at the supplied path. + * + * @param favorites The array of favorites to be written + * @param path The file system path that the file is to be written to + */ +- (void)writeFavorites:(NSArray *)favorites toFile:(NSString *)path +{ + [self setExportFavorites:favorites]; + [self setExportPath:path]; + + [NSThread detachNewThreadSelector:@selector(_writeFavoritesInBackground) toTarget:self withObject:nil]; +} + +/** + * Writes the favorites array to disk in plist format on separate thread. + */ +- (void)_writeFavoritesInBackground +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + NSError *error = nil; + NSString *errorString = nil; + + NSMutableArray *favorites = [[NSMutableArray alloc] init]; + + // Get a dictionary representation of all favorites + for (SPTreeNode *node in [self exportFavorites]) + { + [favorites addObject:[node dictionaryRepresentation]]; + } + + NSDictionary *dictionary = [NSDictionary dictionaryWithObject:favorites forKey:SPFavoritesDataRootKey]; + + [favorites release]; + + // Convert the current favorites tree to a dictionary representation to create the plist data + NSData *plistData = [NSPropertyListSerialization dataFromPropertyList:dictionary + format:NSPropertyListXMLFormat_v1_0 + errorDescription:&errorString]; + + if (plistData) { + [plistData writeToFile:[self exportPath] options:NSAtomicWrite error:&error]; + + if (error) { + NSLog(@"Error writing favorites data: %@", [error localizedDescription]); + } + } + else if (errorString) { + NSLog(@"Error converting favorites data to plist format: %@", errorString); + + [errorString release]; + } + + [self _informDelegateOfExportCompletion:error]; + + [pool release]; +} + +/** + * Informs the delegate that the export process has completed. + */ +- (void)_informDelegateOfExportCompletion:(NSError *)error +{ + if ([self delegate] && [[self delegate] respondsToSelector:@selector(favoritesExportCompletedWithError:)]) { + [[self delegate] performSelectorOnMainThread:@selector(favoritesExportCompletedWithError:) withObject:error waitUntilDone:NO]; + } +} + +@end diff --git a/Source/SPFavoritesImportProtocol.h b/Source/SPFavoritesImportProtocol.h new file mode 100644 index 00000000..bee4d7e8 --- /dev/null +++ b/Source/SPFavoritesImportProtocol.h @@ -0,0 +1,49 @@ +// +// $Id$ +// +// SPFavoritesImportProtocol.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on August 1, 2011 +// Copyright (c) 2011 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +/** + * @protocol SPFavoritesExportProtocol SPFavoritesExportProtocol.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * Favorites importer delegate protocol. + */ +@protocol SPFavoritesImportProtocol + +/** + * Invoked when the favorites import process successfully imports the favorites data. + * + * @param data The imported data as a dictionary. + */ +- (void)favoritesImportData:(NSArray *)data; + +/** + * Invoked when the favorites import proccess completes. + * + * @param error An error instance. Anything other than nil indicates an error occurred. + */ +- (void)favoritesImportCompletedWithError:(NSError *)error; + +@end diff --git a/Source/SPFavoritesImporter.h b/Source/SPFavoritesImporter.h new file mode 100644 index 00000000..ec1603cc --- /dev/null +++ b/Source/SPFavoritesImporter.h @@ -0,0 +1,44 @@ +// +// $Id$ +// +// SPFavoritesImporter.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on May 14, 2011 +// Copyright (c) 2011 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPFavoritesImportProtocol.h" + +@interface SPFavoritesImporter : NSObject +{ + NSObject <SPFavoritesImportProtocol> *delegate; + + NSString *importPath; +} + +@property (readwrite, assign) NSObject <SPFavoritesImportProtocol> *delegate; + +/** + * @property exportPath The file path to import from + */ +@property (readwrite, retain) NSString *importPath; + +- (void)importFavoritesFromFileAtPath:(NSString *)path; + +@end diff --git a/Source/SPFavoritesImporter.m b/Source/SPFavoritesImporter.m new file mode 100644 index 00000000..5ec0c433 --- /dev/null +++ b/Source/SPFavoritesImporter.m @@ -0,0 +1,123 @@ +// +// $Id$ +// +// SPFavoritesImporter.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on May 14, 2011 +// Copyright (c) 2011 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPFavoritesImporter.h" + +@interface SPFavoritesImporter () + +- (void)_importFavoritesInBackground; +- (void)_informDelegateOfImportCompletion:(NSError *)error; +- (void)_informDelegateOfImportDataAvailable:(NSArray *)data; +- (void)_informDelegateOfErrorCode:(NSUInteger)code description:(NSString *)description; + +@end + +@implementation SPFavoritesImporter + +@synthesize delegate; +@synthesize importPath; + +/** + * Imports the favorites from the file at the supplied path. + * + * @param path The path of the file to import + */ +- (void)importFavoritesFromFileAtPath:(NSString *)path +{ + [self setImportPath:path]; + + [NSThread detachNewThreadSelector:@selector(_importFavoritesInBackground) toTarget:self withObject:nil]; +} + +#pragma mark - +#pragma mark Private API + +/** + * Starts the import process on a separate thread. + */ +- (void)_importFavoritesInBackground +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + NSDictionary *importData; + NSFileManager *fileManager = [NSFileManager defaultManager]; + + if ([fileManager fileExistsAtPath:[self importPath]]) { + importData = [[NSDictionary alloc] initWithContentsOfFile:[self importPath]]; + + NSArray *favorites = [importData valueForKey:SPFavoritesDataRootKey]; + + if (favorites) { + [self _informDelegateOfImportDataAvailable:favorites]; + } + else { + [self _informDelegateOfErrorCode:NSFileReadUnknownError + description:NSLocalizedString(@"Error reading import file.", @"error reading import file")]; + } + } + else { + [self _informDelegateOfErrorCode:NSFileReadNoSuchFileError + description:NSLocalizedString(@"Import file does not exist.", @"import file does not exist message")]; + } + + [pool release]; +} + +/** + * Informs the delegate that the import process has completed. + */ +- (void)_informDelegateOfImportCompletion:(NSError *)error +{ + if ([self delegate] && [[self delegate] respondsToSelector:@selector(favoritesExportCompletedWithError:)]) { + [[self delegate] performSelectorOnMainThread:@selector(favoritesExportCompletedWithError:) withObject:error waitUntilDone:NO]; + } +} + +/** + * Informs the delegate that the imported data is available. + */ +- (void)_informDelegateOfImportDataAvailable:(NSArray *)data +{ + if ([self delegate] && [[self delegate] respondsToSelector:@selector(favoritesImportData:)]) { + [[self delegate] performSelectorOnMainThread:@selector(favoritesImportData:) withObject:data waitUntilDone:NO]; + } +} + +/** + * Informs the delegate that an error occurred during the import. + * + * @param code The error code + * @param description A short description of the error + */ +- (void)_informDelegateOfErrorCode:(NSUInteger)code description:(NSString *)description +{ + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:code + userInfo:[NSDictionary dictionaryWithObject:description forKey:NSLocalizedDescriptionKey]]; + + [self _informDelegateOfImportCompletion:error]; +} + +@end diff --git a/Source/SPFavoritesOutlineView.m b/Source/SPFavoritesOutlineView.m index 6930c281..4a47b3db 100644 --- a/Source/SPFavoritesOutlineView.m +++ b/Source/SPFavoritesOutlineView.m @@ -52,6 +52,19 @@ return [self menu]; } +- (void)keyDown:(NSEvent *)event +{ + // Enter or Return initiates a connection to the selected favorite, which is the same as double-clicking + // one, so call the same selector. + if (([self numberOfSelectedRows] == 1) && (([event keyCode] == 36) || ([event keyCode] == 76))) { + [[self delegate] performSelector:[self doubleAction]]; + + return; + } + + [super keyDown:event]; +} + /** * To prevent right-clicking in a column's 'group' heading, ask the delegate if we support selecting it * as this normally doesn't apply to left-clicks. If we do support selecting this row, simply pass on the event. diff --git a/Source/SPFavoritesPreferencePane.h b/Source/SPFavoritesPreferencePane.h deleted file mode 100644 index 4bcd2cd3..00000000 --- a/Source/SPFavoritesPreferencePane.h +++ /dev/null @@ -1,101 +0,0 @@ -// -// $Id$ -// -// SPFavoritesPreferencePane.h -// sequel-pro -// -// Created by Stuart Connolly (stuconnolly.com) on October 31, 2010 -// Copyright (c) 2010 Stuart Connolly. All rights reserved. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -// -// More info at <http://code.google.com/p/sequel-pro/> - -#import "SPPreferencePane.h" - -@class SPKeychain, BWAnchoredButtonBar; - -/** - * @class SPFavoritesPreferencePane SPFavoritesPreferencePane.h - * - * @author Stuart Connolly http://stuconnolly.com/ - * - * Favorites preference pane controller. - */ -@interface SPFavoritesPreferencePane : SPPreferencePane <SPPreferencePaneProtocol> -{ - IBOutlet NSTableView *favoritesTableView; - IBOutlet NSArrayController *favoritesController; - - IBOutlet NSTabView *favoritesTabView; - - IBOutlet NSSecureTextField *standardPasswordField; - IBOutlet NSSecureTextField *socketPasswordField; - IBOutlet NSSecureTextField *sshSQLPasswordField; - IBOutlet NSSecureTextField *sshPasswordField; - - IBOutlet NSTextField *favoriteNameTextField; - IBOutlet NSTextField *favoriteUserTextField; - IBOutlet NSTextField *favoriteHostTextField; - IBOutlet NSTextField *favoriteUserTextFieldSocket; - IBOutlet NSTextField *favoriteUserTextFieldSSH; - IBOutlet NSTextField *favoriteHostTextFieldSSH; - - IBOutlet NSButton *sshSSHKeyButton; - IBOutlet NSButton *standardSSLKeyFileButton; - IBOutlet NSButton *standardSSLCertificateButton; - IBOutlet NSButton *standardSSLCACertButton; - IBOutlet NSButton *socketSSLKeyFileButton; - IBOutlet NSButton *socketSSLCertificateButton; - IBOutlet NSButton *socketSSLCACertButton; - - IBOutlet NSView *sshKeyLocationHelp; - IBOutlet NSView *sslKeyFileLocationHelp; - IBOutlet NSView *sslCertificateLocationHelp; - IBOutlet NSView *sslCACertLocationHelp; - - IBOutlet NSTextFieldCell *tableCell; - - IBOutlet NSMenuItem *favoritesSortByMenuItem; - - IBOutlet BWAnchoredButtonBar *splitViewButtonBar; - - SPKeychain *keychain; - - NSOpenPanel *keySelectionPanel; - - NSInteger favoriteType; - NSDictionary *currentFavorite; - BOOL favoriteNameFieldWasTouched; - - // Sorting - BOOL reverseFavoritesSort; - SPFavoritesSortItem previousSortItem, currentSortItem; -} - -- (IBAction)addFavorite:(id)sender; -- (IBAction)removeFavorite:(id)sender; -- (IBAction)duplicateFavorite:(id)sender; -- (IBAction)makeSelectedFavoriteDefault:(id)sender; -- (IBAction)sortFavorites:(id)sender; -- (IBAction)reverseFavoritesSortOrder:(id)sender; -- (IBAction)chooseKeyLocation:(id)sender; -- (IBAction)favoriteTypeDidChange:(id)sender; -- (IBAction)chooseKeyLocation:(id)sender; -- (IBAction)updateKeyLocationFileVisibility:(id)sender; - -- (void)selectFavorites:(NSArray *)favorites; - -@end diff --git a/Source/SPFavoritesPreferencePane.m b/Source/SPFavoritesPreferencePane.m deleted file mode 100644 index 159e8687..00000000 --- a/Source/SPFavoritesPreferencePane.m +++ /dev/null @@ -1,928 +0,0 @@ -// -// $Id$ -// -// SPFavoritesPreferencePane.m -// sequel-pro -// -// Created by Stuart Connolly (stuconnolly.com) on October 31, 2010 -// Copyright (c) 2010 Stuart Connolly. All rights reserved. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -// -// More info at <http://code.google.com/p/sequel-pro/> - -#import "SPFavoritesPreferencePane.h" -#import "SPFavoriteTextFieldCell.h" -#import "SPPreferenceController.h" -#import "SPKeychain.h" -#import <BWToolkitFramework/BWToolkitFramework.h> -#import "SPGeneralPreferencePane.h" - -@interface SPFavoritesPreferencePane (PrivateAPI) - -- (void)_sortFavorites; -- (void)_updateFavoritePasswordsFromField:(NSControl *)passwordControl; - -@end - -@interface NSSavePanel (NSSavePanel_unpublishedUntilSnowLeopardAPI) -- (void)setShowsHiddenFiles:(BOOL)flag; -@end - -@implementation SPFavoritesPreferencePane - -#pragma mark - -#pragma mark Intialisation - -/** - * Init. - */ -- (id)init -{ - if ((self = [super init])) { - - keychain = [[SPKeychain alloc] init]; - - favoriteType = 0; - reverseFavoritesSort = NO; - favoriteNameFieldWasTouched = YES; - - previousSortItem = SPFavoritesSortNameItem; - } - - return self; -} - -/** - * Initialise the UI, specifically the favourites table view and sort the favourites if required. - */ -- (void)awakeFromNib -{ - // Set sort items - currentSortItem = [prefs integerForKey:SPFavoritesSortedBy]; - reverseFavoritesSort = [prefs boolForKey:SPFavoritesSortedInReverse]; - - // Replace column's NSTextFieldCell with custom SWProfileTextFieldCell - [[[favoritesTableView tableColumns] objectAtIndex:0] setDataCell:tableCell]; - - [favoritesTableView registerForDraggedTypes:[NSArray arrayWithObject:SPFavoritesPasteboardDragType]]; - - [favoritesTableView selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO]; - [favoritesTableView reloadData]; - - [tableCell setImage:[NSImage imageNamed:@"database"]]; - - // Set the button bar delegate - [splitViewButtonBar setSplitViewDelegate:self]; - - // Hide the tabs on the favorites tab view - left visible in IB for easy use - [favoritesTabView setTabViewType:NSNoTabsNoBorder]; - - // Sort favorites if a sort type has been selected - if (currentSortItem != SPFavoritesSortUnsorted) [self _sortFavorites]; -} - -#pragma mark - -#pragma mark IBAction methods - -/** - * Adds a new connection favorite. - */ -- (IBAction)addFavorite:(id)sender -{ - NSNumber *favoriteid = [NSNumber numberWithInteger:[[NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]] hash]]; - - // Create default favorite - NSMutableDictionary *favorite = [NSMutableDictionary dictionaryWithObjects:[NSArray arrayWithObjects:NSLocalizedString(@"New Favorite", @"new favorite name"), [NSNumber numberWithInteger:0], @"", @"", @"", @"", [NSNumber numberWithInt:NSOffState], [NSNumber numberWithInt:NSOffState], [NSNumber numberWithInt:NSOffState], [NSNumber numberWithInt:NSOffState], @"", @"", @"", [NSNumber numberWithInt:NSOffState], @"", @"", favoriteid, nil] - forKeys:[NSArray arrayWithObjects:@"name", @"type", @"host", @"socket", @"user", @"port", @"useSSL", @"sslKeyFileLocationEnabled", @"sslCertificateFileLocationEnabled", @"sslCACertFileLocationEnabled", @"database", @"sshHost", @"sshUser", @"sshKeyLocationEnabled", @"sshKeyLocation", @"sshPort", @"id", nil]]; - - [favoritesController addObject:favorite]; - [favoritesController setSelectedObjects:[NSArray arrayWithObject:favorite]]; - - [favoritesTableView reloadData]; - [favoritesTableView scrollRowToVisible:[favoritesTableView selectedRow]]; - - [[(SPPreferenceController *)[[[self view] window] delegate] generalPreferencePane] updateDefaultFavoritePopup]; - - favoriteNameFieldWasTouched = NO; - - [[[self view] window] makeFirstResponder:favoriteHostTextField]; -} - -/** - * Removes the selected connection favorite. - */ -- (IBAction)removeFavorite:(id)sender -{ - if ([favoritesTableView numberOfSelectedRows] == 1) { - NSAlert *alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:NSLocalizedString(@"Delete favorite '%@'?", @"delete database message"), [favoritesController valueForKeyPath:@"selection.name"]] - defaultButton:NSLocalizedString(@"Delete", @"delete button") - alternateButton:NSLocalizedString(@"Cancel", @"cancel button") - otherButton:nil - informativeTextWithFormat:[NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to delete the favorite '%@'? This operation cannot be undone.", @"delete database informative message"), [favoritesController valueForKeyPath:@"selection.name"]]]; - - NSArray *buttons = [alert buttons]; - - // Change the alert's cancel button to have the key equivalent of return - [[buttons objectAtIndex:0] setKeyEquivalent:@"d"]; - [[buttons objectAtIndex:0] setKeyEquivalentModifierMask:NSCommandKeyMask]; - [[buttons objectAtIndex:1] setKeyEquivalent:@"\r"]; - - [alert setAlertStyle:NSCriticalAlertStyle]; - - [alert beginSheetModalForWindow:[[self view] window] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:@"removeFavorite"]; - } -} - -/** - * Duplicates the selected connection favorite. - */ -- (IBAction)duplicateFavorite:(id)sender -{ - if ([favoritesTableView numberOfSelectedRows] == 1) { - - NSMutableDictionary *favorite = [NSMutableDictionary dictionaryWithDictionary:[[favoritesController arrangedObjects] objectAtIndex:[favoritesTableView selectedRow]]]; - NSNumber *favoriteid = [NSNumber numberWithInteger:[[NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]] hash]]; - NSInteger duplicatedFavoriteType = [[favorite objectForKey:@"type"] integerValue]; - - // Select the keychain passwords for duplication - NSString *keychainName = [keychain nameForFavoriteName:[favorite objectForKey:@"name"] id:[favorite objectForKey:@"id"]]; - NSString *keychainAccount = [keychain accountForUser:[favorite objectForKey:@"user"] host:((duplicatedFavoriteType == SPSocketConnection)?@"localhost":[favorite objectForKey:@"host"]) database:[favorite objectForKey:@"database"]]; - NSString *password = [keychain getPasswordForName:keychainName account:keychainAccount]; - NSString *keychainSSHName = [keychain nameForSSHForFavoriteName:[favorite objectForKey:@"name"] id:[favorite objectForKey:@"id"]]; - NSString *keychainSSHAccount = [keychain accountForSSHUser:[favorite objectForKey:@"sshUser"] sshHost:[favorite objectForKey:@"sshHost"]]; - NSString *sshPassword = [keychain getPasswordForName:keychainSSHName account:keychainSSHAccount]; - - // Update the unique ID - [favorite setObject:favoriteid forKey:@"id"]; - - // Alter the name for clarity - [favorite setObject:[NSString stringWithFormat:NSLocalizedString(@"%@ Copy", @"Initial favourite name after duplicating a previous favourite"), [favorite objectForKey:@"name"]] forKey:@"name"]; - - // Create new keychain items if appropriate - if (password && [password length]) { - keychainName = [keychain nameForFavoriteName:[favorite objectForKey:@"name"] id:[favorite objectForKey:@"id"]]; - [keychain addPassword:password forName:keychainName account:keychainAccount]; - } - - if (sshPassword && [sshPassword length]) { - keychainSSHName = [keychain nameForSSHForFavoriteName:[favorite objectForKey:@"name"] id:[favorite objectForKey:@"id"]]; - [keychain addPassword:sshPassword forName:keychainSSHName account:keychainSSHAccount]; - } - - password = nil, sshPassword = nil; - - [favoritesController addObject:favorite]; - [favoritesController setSelectedObjects:[NSArray arrayWithObject:favorite]]; - - [favoritesTableView reloadData]; - [favoritesTableView scrollRowToVisible:[favoritesTableView selectedRow]]; - - [[(SPPreferenceController *)[[[self view] window] delegate] generalPreferencePane] updateDefaultFavoritePopup]; - - [[[self view] window] makeFirstResponder:favoriteNameTextField]; - } -} - -/** - * Sorts the favorites table view based on the selected sort by item - */ -- (IBAction)sortFavorites:(id)sender -{ - previousSortItem = currentSortItem; - currentSortItem = [[sender menu] indexOfItem:sender]; - - [prefs setInteger:currentSortItem forKey:SPFavoritesSortedBy]; - - // Perform sorting - [self _sortFavorites]; - - if ((NSInteger)previousSortItem > -1) [[[sender menu] itemAtIndex:previousSortItem] setState:NSOffState]; - - [[[sender menu] itemAtIndex:currentSortItem] setState:NSOnState]; -} - -/** - * Reverses the favorites table view sorting based on the selected criteria - */ -- (IBAction)reverseFavoritesSortOrder:(id)sender -{ - reverseFavoritesSort = (![sender state]); - - [prefs setBool:reverseFavoritesSort forKey:SPFavoritesSortedInReverse]; - - // Perform re-sorting - [self _sortFavorites]; - - [sender setState:reverseFavoritesSort]; -} - -/** - * Makes the selected favorite the default. - */ -- (IBAction)makeSelectedFavoriteDefault:(id)sender -{ - // Minus 2 from index to account for the 'Last Used' and separator items - [prefs setInteger:[favoritesTableView selectedRow] forKey:SPDefaultFavorite]; - - [favoritesTableView reloadData]; - - [[(SPPreferenceController *)[[[self view] window] delegate] generalPreferencePane] updateDefaultFavoritePopup]; -} - -/** - * Update the favorite host when the type changes. - */ -- (IBAction)favoriteTypeDidChange:(id)sender -{ - // If not socket and host is localhost, clear. - if (([sender indexOfSelectedItem] != 1) && [[favoritesController valueForKeyPath:@"selection.host"] isEqualToString:@"localhost"]) - { - [favoritesController setValue:@"" forKeyPath:@"selection.host"]; - } - - favoriteType = [sender indexOfSelectedItem]; - - // Update the name for a new added favorite if not touched by the user - if(!favoriteNameFieldWasTouched) { - [favoriteNameTextField setStringValue:[NSString stringWithFormat:@"%@@%@", - ([favoritesController valueForKeyPath:@"selection.user"]) ? [favoritesController valueForKeyPath:@"selection.user"] : @"", - (([sender indexOfSelectedItem] == 1) ? @"localhost" : - (([favoritesController valueForKeyPath:@"selection.host"]) ? [favoritesController valueForKeyPath:@"selection.host"] : @"")) - ]]; - - [favoritesController setValue:[favoriteNameTextField stringValue] forKeyPath:@"selection.name"]; - } - - // Request a password refresh to keep keychain references in synch with the favorites - [self _updateFavoritePasswordsFromField:nil]; -} - -/** - * Opens the SSH/SSL key selection window, ready to select a key file. - */ -- (IBAction)chooseKeyLocation:(id)sender -{ - NSString *directoryPath = nil; - NSString *filePath = nil; - NSArray *permittedFileTypes = nil; - keySelectionPanel = [NSOpenPanel openPanel]; - [keySelectionPanel setShowsHiddenFiles:[prefs boolForKey:SPHiddenKeyFileVisibilityKey]]; - - // Switch details by sender. - // First, SSH keys: - if (sender == sshSSHKeyButton) { - - // If the custom key location is currently disabled - after the button - // action - leave it disabled and return without showing the sheet. - if (![favoritesController valueForKeyPath:@"selection.sshKeyLocationEnabled"]) { - return; - } - - // Otherwise open a panel at the last or default location - NSString *sshKeyLocation = [favoritesController valueForKeyPath:@"selection.sshKeyLocation"]; - if (sshKeyLocation && [sshKeyLocation length]) { - filePath = [sshKeyLocation lastPathComponent]; - directoryPath = [sshKeyLocation stringByDeletingLastPathComponent]; - } - - permittedFileTypes = [NSArray arrayWithObjects:@"pem", @"", nil]; - - [keySelectionPanel setAccessoryView:sshKeyLocationHelp]; - - // SSL key file location: - } - else if (sender == standardSSLKeyFileButton || sender == socketSSLKeyFileButton) { - if ([sender state] == NSOffState) { - [favoritesController setValue:nil forKeyPath:@"selection.sslKeyFileLocation"]; - return; - } - - permittedFileTypes = [NSArray arrayWithObjects:@"pem", @"key", @"", nil]; - [keySelectionPanel setAccessoryView:sslKeyFileLocationHelp]; - - // SSL certificate file location: - } - else if (sender == standardSSLCertificateButton || sender == socketSSLCertificateButton) { - if ([sender state] == NSOffState) { - [favoritesController setValue:nil forKeyPath:@"selection.sslCertificateFileLocation"]; - return; - } - - permittedFileTypes = [NSArray arrayWithObjects:@"pem", @"cert", @"crt", @"", nil]; - [keySelectionPanel setAccessoryView:sslCertificateLocationHelp]; - - // SSL CA certificate file location: - } - else if (sender == standardSSLCACertButton || sender == socketSSLCACertButton) { - if ([sender state] == NSOffState) { - [favoritesController setValue:nil forKeyPath:@"selection.sslCACertFileLocation"]; - return; - } - - permittedFileTypes = [NSArray arrayWithObjects:@"pem", @"cert", @"crt", @"", nil]; - [keySelectionPanel setAccessoryView:sslCACertLocationHelp]; - } - - [keySelectionPanel beginSheetForDirectory:directoryPath - file:filePath - types:permittedFileTypes - modalForWindow:[[self view] window] - modalDelegate:self - didEndSelector:@selector(chooseKeyLocationSheetDidEnd:returnCode:contextInfo:) - contextInfo:sender]; -} - -/** - * Toggle hidden file visiblity in response to accessory view changes - */ -- (IBAction)updateKeyLocationFileVisibility:(id)sender -{ - [keySelectionPanel setShowsHiddenFiles:[prefs boolForKey:SPHiddenKeyFileVisibilityKey]]; -} - -#pragma mark - -#pragma mark Public API - -/** - * Selects the specified favorite(s) in the favorites list. - */ -- (void)selectFavorites:(NSArray *)favorites -{ - [favoritesController setSelectedObjects:favorites]; - [favoritesTableView scrollRowToVisible:[favoritesController selectionIndex]]; -} - -#pragma mark - -#pragma mark TableView datasource methods - -- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView -{ - return [[favoritesController arrangedObjects] count]; -} - -- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex -{ - return [[[favoritesController arrangedObjects] objectAtIndex:rowIndex] objectForKey:[tableColumn identifier]]; -} - -#pragma mark - -#pragma mark TableView drag & drop delegate methods - -- (BOOL)tableView:(NSTableView *)tableView writeRowsWithIndexes:(NSIndexSet *)rows toPasteboard:(NSPasteboard*)pboard -{ - if ([rows count] == 1) { - [pboard declareTypes:[NSArray arrayWithObject:SPFavoritesPasteboardDragType] owner:nil]; - [pboard setString:[[NSNumber numberWithInteger:[rows firstIndex]] stringValue] forType:SPFavoritesPasteboardDragType]; - - return YES; - } - else { - return NO; - } -} - -- (NSDragOperation)tableView:(NSTableView *)tableView validateDrop:(id <NSDraggingInfo>)info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)operation -{ - NSInteger originalRow; - NSArray *pboardTypes = [[info draggingPasteboard] types]; - - if (([pboardTypes count] > 1) && (row != -1)) { - if (([pboardTypes containsObject:SPFavoritesPasteboardDragType]) && (operation == NSTableViewDropAbove)) { - originalRow = [[[info draggingPasteboard] stringForType:SPFavoritesPasteboardDragType] integerValue]; - - if ((row != originalRow) && (row != (originalRow + 1))) { - return NSDragOperationMove; - } - } - } - - return NSDragOperationNone; -} - -- (BOOL)tableView:(NSTableView *)tableView acceptDrop:(id <NSDraggingInfo>)info row:(NSInteger)row dropOperation:(NSTableViewDropOperation)operation -{ - NSInteger originalRow; - NSInteger destinationRow; - NSInteger lastFavoriteIndexCached; - NSMutableDictionary *draggedRow; - - // Disable all automatic sorting - currentSortItem = -1; - reverseFavoritesSort = NO; - - [prefs setInteger:currentSortItem forKey:SPFavoritesSortedBy]; - [prefs setBool:NO forKey:SPFavoritesSortedInReverse]; - - // Remove sort descriptors - [favoritesController setSortDescriptors:[NSArray array]]; - - // Uncheck sort by menu items - for (NSMenuItem *menuItem in [[favoritesSortByMenuItem submenu] itemArray]) - { - [menuItem setState:NSOffState]; - } - - originalRow = [[[info draggingPasteboard] stringForType:SPFavoritesPasteboardDragType] integerValue]; - destinationRow = row; - - if (destinationRow > originalRow) { - destinationRow--; - } - - draggedRow = [NSMutableDictionary dictionaryWithDictionary:[[favoritesController arrangedObjects] objectAtIndex:originalRow]]; - - // Before deleting this favorite, we need to save the current index. - // because removeObjectAtArrangedObjectIndex will set prefs LastFavoriteIndex to 0 - lastFavoriteIndexCached = [prefs integerForKey:SPLastFavoriteIndex]; - - [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 (lastFavoriteIndexCached == originalRow) { - [prefs setInteger:destinationRow forKey:SPLastFavoriteIndex]; - } - - // Update default favorite to take on new value - if ([prefs integerForKey:SPDefaultFavorite] == originalRow) { - [prefs setInteger:destinationRow forKey:SPDefaultFavorite]; - } - - [[(SPPreferenceController *)[[[self view] window] delegate] generalPreferencePane] updateDefaultFavoritePopup]; - - return YES; -} - -#pragma mark - -#pragma mark TableView delegate methods - -- (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)index -{ - if ([cell isKindOfClass:[SPFavoriteTextFieldCell class]]) { - [cell setFavoriteName:[[[favoritesController arrangedObjects] objectAtIndex:index] objectForKey:@"name"]]; - - if ([[[[favoritesController arrangedObjects] objectAtIndex:index] objectForKey:@"type"] integerValue] == SPSocketConnection) { - [cell setFavoriteHost:@"localhost"]; - } - else { - [cell setFavoriteHost:[[[favoritesController arrangedObjects] objectAtIndex:index] objectForKey:@"host"]]; - } - } -} - -- (void)tableViewSelectionDidChange:(NSNotification *)notification -{ - if ([[favoritesTableView selectedRowIndexes] count] > 0) { - [favoritesController setSelectionIndexes:[favoritesTableView selectedRowIndexes]]; - } - - // If no selection is present, blank the password fields (which can't use bindings) - if ([[favoritesTableView selectedRowIndexes] count] == 0) { - [standardPasswordField setStringValue:@""]; - [socketPasswordField setStringValue:@""]; - [sshSQLPasswordField setStringValue:@""]; - [sshPasswordField setStringValue:@""]; - - return; - } - - // Keep a copy of the favorite as it currently stands - if (currentFavorite) [currentFavorite release]; - - currentFavorite = [[[favoritesController selectedObjects] objectAtIndex:0] copy]; - - // Retrieve and set the password. - NSString *keychainName = [keychain nameForFavoriteName:[currentFavorite objectForKey:@"name"] id:[currentFavorite objectForKey:@"id"]]; - NSString *keychainAccount = [keychain accountForUser:[currentFavorite objectForKey:@"user"] host:(([[currentFavorite objectForKey:@"type"] integerValue] == SPSocketConnection)?@"localhost":[currentFavorite objectForKey:@"host"]) database:[currentFavorite objectForKey:@"database"]]; - NSString *passwordValue = [keychain getPasswordForName:keychainName account:keychainAccount]; - - [standardPasswordField setStringValue:passwordValue?passwordValue:@""]; - [socketPasswordField setStringValue:passwordValue?passwordValue:@""]; - [sshSQLPasswordField setStringValue:passwordValue?passwordValue:@""]; - - // Retrieve the SSH keychain password if appropriate. - NSString *keychainSSHName = [keychain nameForSSHForFavoriteName:[currentFavorite objectForKey:@"name"] id:[currentFavorite objectForKey:@"id"]]; - NSString *keychainSSHAccount = [keychain accountForSSHUser:[currentFavorite objectForKey:@"sshUser"] sshHost:[currentFavorite objectForKey:@"sshHost"]]; - NSString *sshPasswordValue = [keychain getPasswordForName:keychainSSHName account:keychainSSHAccount]; - [sshPasswordField setStringValue:sshPasswordValue?sshPasswordValue:@""]; - - favoriteNameFieldWasTouched = YES; -} - -#pragma mark - -#pragma mark TextField delegate methods and type change action - -/** - * 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 -{ - // Request a password refresh to keep keychain references in synch with favorites - [self _updateFavoritePasswordsFromField:control]; - - // Proceed with editing - return YES; -} - -/** - * Trap and control the 'name' field of the selected favorite. If the user pressed - * 'Add Favorite' the 'name' field is set to "New Favorite". If the user do not - * change the 'name' field or delete that field it will be set to user@host automatically. - */ -- (void)controlTextDidChange:(NSNotification *)notification -{ - id field = [notification object]; - - BOOL nameFieldIsEmpty = ([[favoritesController valueForKeyPath:@"selection.name"] isEqualToString:@""] || - [[favoriteNameTextField stringValue] isEqualToString:@""]); - - switch (favoriteType) - { - case 0: - if (nameFieldIsEmpty || (!favoriteNameFieldWasTouched && (field == favoriteUserTextField || field == favoriteHostTextField))) { - [favoriteNameTextField setStringValue:[NSString stringWithFormat:@"%@@%@", [favoriteUserTextField stringValue], [favoriteHostTextField stringValue]]]; - [favoritesController setValue:[favoriteNameTextField stringValue] forKeyPath:@"selection.name"]; - [prefs synchronize]; - - // if name field is empty enable user@host update - if (nameFieldIsEmpty) favoriteNameFieldWasTouched = NO; - } - break; - case 1: - if (nameFieldIsEmpty || (!favoriteNameFieldWasTouched && field == favoriteUserTextFieldSocket)) { - [favoriteNameTextField setStringValue:[NSString stringWithFormat:@"%@@localhost", [favoriteUserTextFieldSocket stringValue]]]; - [favoritesController setValue:[favoriteNameTextField stringValue] forKeyPath:@"selection.name"]; - [prefs synchronize]; - - // if name field is empty enable user@host update - if (nameFieldIsEmpty) favoriteNameFieldWasTouched = NO; - } - break; - case 2: - if (nameFieldIsEmpty || (!favoriteNameFieldWasTouched && (field == favoriteUserTextFieldSSH || field == favoriteHostTextFieldSSH))) { - [favoriteNameTextField setStringValue:[NSString stringWithFormat:@"%@@%@", [favoriteUserTextFieldSSH stringValue], [favoriteHostTextFieldSSH stringValue]]]; - [favoritesController setValue:[favoriteNameTextField stringValue] forKeyPath:@"selection.name"]; - [prefs synchronize]; - - // if name field is empty enable user@host update - if (nameFieldIsEmpty) favoriteNameFieldWasTouched = NO; - } - break; - default: - break; - } - - if (field == favoriteNameTextField) favoriteNameFieldWasTouched = YES; -} - -#pragma mark - -#pragma mark SplitView delegate methods - -- (CGFloat)splitView:(NSSplitView *)sender constrainMaxCoordinate:(CGFloat)proposedMax ofSubviewAt:(NSInteger)offset -{ - return (proposedMax - 220); -} - -- (CGFloat)splitView:(NSSplitView *)sender constrainMinCoordinate:(CGFloat)proposedMin ofSubviewAt:(NSInteger)offset -{ - return (proposedMin + 94); -} - -#pragma mark - -#pragma mark Other - -/** - * Menu item validation; - */ -- (BOOL)validateMenuItem:(NSMenuItem *)menuItem -{ - SEL action = [menuItem action]; - - if ((action == @selector(removeFavorite:)) || (action == @selector(duplicateFavorite:))) { - return ([favoritesTableView numberOfSelectedRows] > 0); - } - - if (action == @selector(makeSelectedFavoriteDefault:)) { - return ([favoritesTableView numberOfSelectedRows] == 1); - } - - if ((action == @selector(sortFavorites:)) || (action == @selector(reverseFavoritesSortOrder:))) { - - // Loop all the items in the sort by menu only checking the currently selected one - for (NSMenuItem *item in [[menuItem menu] itemArray]) - { - [item setState:([[menuItem menu] indexOfItem:item] == currentSortItem)]; - } - - // Check or uncheck the reverse sort item - if (action == @selector(reverseFavoritesSortOrder:)) { - [menuItem setState:reverseFavoritesSort]; - } - - return [[[[[self view] window] toolbar] selectedItemIdentifier] isEqualToString:SPPreferenceToolbarFavorites]; - } - - return YES; -} - -/** - * Called after closing the SSH/SSL key selection sheet. - */ -- (void)chooseKeyLocationSheetDidEnd:(NSOpenPanel *)openPanel returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo -{ - NSString *abbreviatedFileName = [[[openPanel URL] path] stringByAbbreviatingWithTildeInPath]; - - // SSH key file selection - if (contextInfo == sshSSHKeyButton) { - if (returnCode == NSCancelButton) { - [favoritesController setValue:[NSNumber numberWithInt:NSOffState] forKeyPath:@"selection.sshKeyLocationEnabled"]; - return; - } - - [favoritesController setValue:abbreviatedFileName forKeyPath:@"selection.sshKeyLocation"]; - - // SSL key file selection - } - else if (contextInfo == standardSSLKeyFileButton || contextInfo == socketSSLKeyFileButton) { - if (returnCode == NSCancelButton) { - [favoritesController setValue:[NSNumber numberWithInt:NSOffState] forKeyPath:@"selection.sslKeyFileLocationEnabled"]; - [favoritesController setValue:nil forKeyPath:@"selection.sslKeyFileLocation"]; - return; - } - - [favoritesController setValue:abbreviatedFileName forKeyPath:@"selection.sslKeyFileLocation"]; - - // SSL certificate file selection - } - else if (contextInfo == standardSSLCertificateButton || contextInfo == socketSSLCertificateButton) { - if (returnCode == NSCancelButton) { - [favoritesController setValue:[NSNumber numberWithInt:NSOffState] forKeyPath:@"selection.sslCertificateFileLocationEnabled"]; - [favoritesController setValue:nil forKeyPath:@"selection.sslCertificateFileLocation"]; - return; - } - - [favoritesController setValue:abbreviatedFileName forKeyPath:@"selection.sslCertificateFileLocation"]; - - // SSL CA certificate file selection - } - else if (contextInfo == standardSSLCACertButton || contextInfo == socketSSLCACertButton) { - if (returnCode == NSCancelButton) { - [favoritesController setValue:[NSNumber numberWithInt:NSOffState] forKeyPath:@"selection.sslCACertFileLocationEnabled"]; - [favoritesController setValue:nil forKeyPath:@"selection.sslCACertFileLocation"]; - return; - } - - [favoritesController setValue:abbreviatedFileName forKeyPath:@"selection.sslCACertFileLocation"]; - } -} - -- (void)sheetDidEnd:(id)sheet returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo -{ - // Order out current sheet to suppress overlapping of sheets - if ([sheet respondsToSelector:@selector(orderOut:)]) { - [sheet orderOut:nil]; - } - else if ([sheet respondsToSelector:@selector(window)]) { - [[sheet window] orderOut:nil]; - } - - // Remove the current favorite - if ([contextInfo isEqualToString:@"removeFavorite"]) { - if (returnCode == NSAlertDefaultReturn) { - - // 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"]; - NSString *sshUser = [favoritesController valueForKeyPath:@"selection.sshUser"]; - NSString *sshHost = [favoritesController valueForKeyPath:@"selection.sshHost"]; - NSString *favoriteid = [favoritesController valueForKeyPath:@"selection.id"]; - NSInteger type = [[favoritesController valueForKeyPath:@"selection.type"] integerValue]; - - // Remove passwords from the Keychain - [keychain deletePasswordForName:[keychain nameForFavoriteName:name id:favoriteid] - account:[keychain accountForUser:user host:((type == SPSocketConnection)?@"localhost":host) database:database]]; - [keychain deletePasswordForName:[keychain nameForSSHForFavoriteName:name id:favoriteid] - account:[keychain accountForSSHUser:sshUser sshHost:sshHost]]; - - // Reset last used favorite - if ([favoritesTableView selectedRow] == [prefs integerForKey:SPLastFavoriteIndex]) { - [prefs setInteger:0 forKey:SPLastFavoriteIndex]; - } - - // Reset default favorite - if ([favoritesTableView selectedRow] == [prefs integerForKey:SPDefaultFavorite]) { - [prefs setInteger:[prefs integerForKey:SPLastFavoriteIndex] forKey:SPDefaultFavorite]; - } - - [favoritesController removeObjectAtArrangedObjectIndex:[favoritesTableView selectedRow]]; - - [favoritesTableView reloadData]; - - [[(SPPreferenceController *)[[[self view] window] delegate] generalPreferencePane] updateDefaultFavoritePopup]; - } - } -} - -#pragma mark - -#pragma mark Preference pane protocol methods - -- (NSView *)preferencePaneView -{ - return [self view]; -} - -- (NSImage *)preferencePaneIcon -{ - return [NSImage imageNamed:@"toolbar-preferences-favorites"]; -} - -- (NSString *)preferencePaneName -{ - return NSLocalizedString(@"Favorites", @"favorites label"); -} - -- (NSString *)preferencePaneIdentifier -{ - return SPPreferenceToolbarFavorites; -} - -- (NSString *)preferencePaneToolTip -{ - return NSLocalizedString(@"Favorite Preferences", @"favorites preference pane tooltip"); -} - -- (BOOL)preferencePaneAllowsResizing -{ - return YES; -} - -#pragma mark - -#pragma mark Private API - -/** - * Sorts the connection favorites based on the selected criteria. - */ -- (void)_sortFavorites -{ - NSString *sortKey = SPFavoriteNameKey; - - switch (currentSortItem) - { - case SPFavoritesSortNameItem: - sortKey = SPFavoriteNameKey; - break; - case SPFavoritesSortHostItem: - sortKey = SPFavoriteHostKey; - break; - case SPFavoritesSortTypeItem: - sortKey = SPFavoriteTypeKey; - break; - default: - return; - } - - NSSortDescriptor *sortDescriptor = nil; - - if (currentSortItem == SPFavoritesSortTypeItem) { - sortDescriptor = [[[NSSortDescriptor alloc] initWithKey:sortKey ascending:(!reverseFavoritesSort)] autorelease]; - } - else { - sortDescriptor = [[[NSSortDescriptor alloc] initWithKey:sortKey ascending:(!reverseFavoritesSort) selector:@selector(caseInsensitiveCompare:)] autorelease]; - } - - [favoritesController setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]]; - - [favoritesTableView reloadData]; - - [[(SPPreferenceController *)[[[self view] window] delegate] generalPreferencePane] updateDefaultFavoritePopup]; -} - -/** - * Check all fields used in the keychain names against the old values for that - * favorite, and update the keychain names to match if necessary. - * If an (optional) recognised password field is supplied, that field is assumed - * to have changed and is used to supply the new value. - */ -- (void)_updateFavoritePasswordsFromField:(NSControl *)passwordControl -{ - if (!currentFavorite) return; - - NSString *passwordValue; - NSString *oldKeychainName, *newKeychainName; - NSString *oldKeychainAccount, *newKeychainAccount; - NSString *oldHostnameForPassword = ([[currentFavorite objectForKey:@"type"] integerValue] == SPSocketConnection) ? @"localhost" : [currentFavorite objectForKey:@"host"]; - NSString *newHostnameForPassword = ([[favoritesController valueForKeyPath:@"selection.type"] integerValue] == SPSocketConnection) ? @"localhost" : [favoritesController valueForKeyPath:@"selection.host"]; - - // SQL passwords are indexed by name, host, user and database. If any of these - // have changed, or a standard password field has, alter the keychain item to match. - if (![[currentFavorite objectForKey:@"name"] isEqualToString:[favoritesController valueForKeyPath:@"selection.name"]] - || ![oldHostnameForPassword isEqualToString:newHostnameForPassword] - || ![[currentFavorite objectForKey:@"user"] isEqualToString:[favoritesController valueForKeyPath:@"selection.user"]] - || ![[currentFavorite objectForKey:@"database"] isEqualToString:[favoritesController valueForKeyPath:@"selection.database"]] - || passwordControl == standardPasswordField || passwordControl == socketPasswordField || passwordControl == sshSQLPasswordField) - { - - // Determine the correct password field to read the password from, defaulting to standard - if (passwordControl == socketPasswordField) { - passwordValue = [socketPasswordField stringValue]; - } - else if (passwordControl == sshSQLPasswordField) { - passwordValue = [sshSQLPasswordField stringValue]; - } - else { - passwordValue = [standardPasswordField stringValue]; - } - - // Get the old keychain name and account strings - oldKeychainName = [keychain nameForFavoriteName:[currentFavorite objectForKey:@"name"] id:[favoritesController valueForKeyPath:@"selection.id"]]; - oldKeychainAccount = [keychain accountForUser:[currentFavorite objectForKey:@"user"] host:oldHostnameForPassword database:[currentFavorite objectForKey:@"database"]]; - - // If there's no new password, remove the old item from the keychain - if (![passwordValue length]) { - [keychain deletePasswordForName:oldKeychainName account:oldKeychainAccount]; - - // Otherwise, set up the new keychain name and account strings and create or edit the item - } else { - newKeychainName = [keychain nameForFavoriteName:[favoritesController valueForKeyPath:@"selection.name"] id:[favoritesController valueForKeyPath:@"selection.id"]]; - newKeychainAccount = [keychain accountForUser:[favoritesController valueForKeyPath:@"selection.user"] host:newHostnameForPassword database:[favoritesController valueForKeyPath:@"selection.database"]]; - if ([keychain passwordExistsForName:oldKeychainName account:oldKeychainAccount]) { - [keychain updateItemWithName:oldKeychainName account:oldKeychainAccount toName:newKeychainName account:newKeychainAccount password:passwordValue]; - } else { - [keychain addPassword:passwordValue forName:newKeychainName account:newKeychainAccount]; - } - - } - - // Synch password changes - [standardPasswordField setStringValue:passwordValue]; - [socketPasswordField setStringValue:passwordValue]; - [sshSQLPasswordField setStringValue:passwordValue]; - - passwordValue = @""; - } - - // If SSH account/password details have changed, update the keychain to match - if (![[currentFavorite objectForKey:@"name"] isEqualToString:[favoritesController valueForKeyPath:@"selection.name"]] - || ![[currentFavorite objectForKey:@"sshHost"] isEqualToString:[favoritesController valueForKeyPath:@"selection.sshHost"]] - || ![[currentFavorite objectForKey:@"sshUser"] isEqualToString:[favoritesController valueForKeyPath:@"selection.sshUser"]] - || passwordControl == sshPasswordField) { - - // Get the old keychain name and account strings - oldKeychainName = [keychain nameForSSHForFavoriteName:[currentFavorite objectForKey:@"name"] id:[favoritesController valueForKeyPath:@"selection.id"]]; - oldKeychainAccount = [keychain accountForSSHUser:[currentFavorite objectForKey:@"sshUser"] sshHost:[currentFavorite objectForKey:@"sshHost"]]; - - // If there's no new password, delete the keychain item - if (![[sshPasswordField stringValue] length]) { - [keychain deletePasswordForName:oldKeychainName account:oldKeychainAccount]; - - - // Otherwise, set up the new keychain name and account strings and create or update the keychain item - } else { - newKeychainName = [keychain nameForSSHForFavoriteName:[favoritesController valueForKeyPath:@"selection.name"] id:[favoritesController valueForKeyPath:@"selection.id"]]; - newKeychainAccount = [keychain accountForSSHUser:[favoritesController valueForKeyPath:@"selection.sshUser"] sshHost:[favoritesController valueForKeyPath:@"selection.sshHost"]]; - if ([keychain passwordExistsForName:oldKeychainName account:oldKeychainAccount]) { - [keychain updateItemWithName:oldKeychainName account:oldKeychainAccount toName:newKeychainName account:newKeychainAccount password:[sshPasswordField stringValue]]; - } else { - [keychain addPassword:[sshPasswordField stringValue] forName:newKeychainName account:newKeychainAccount]; - } - } - } - - // Update the current favorite - if (currentFavorite) [currentFavorite release], currentFavorite = nil; - - if ([[favoritesTableView selectedRowIndexes] count] > 0) - currentFavorite = [[[favoritesController selectedObjects] objectAtIndex:0] copy]; -} - -#pragma mark - - -- (void)dealloc -{ - [keychain release], keychain = nil; - - if (currentFavorite) [currentFavorite release], currentFavorite = nil; - - [super dealloc]; -} - -@end diff --git a/Source/SPFlippedView.h b/Source/SPFlippedView.h new file mode 100644 index 00000000..0270daf8 --- /dev/null +++ b/Source/SPFlippedView.h @@ -0,0 +1,28 @@ +// +// $Id$ +// +// SPFlippedView.h +// sequel-pro +// +// Created by Rowan Beentje on 28/06/2009. +// Copyright 2009 Arboreal. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +@interface SPFlippedView : NSView + +@end diff --git a/Source/SPFlippedView.m b/Source/SPFlippedView.m new file mode 100644 index 00000000..b5334488 --- /dev/null +++ b/Source/SPFlippedView.m @@ -0,0 +1,38 @@ +// +// $Id$ +// +// SPFlippedView.m +// sequel-pro +// +// Created by Rowan Beentje on 28/06/2009. +// Copyright 2009 Arboreal. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPFlippedView.h" + +@implementation SPFlippedView + +/** + * Is flipped to simplify drawing. + */ +- (BOOL)isFlipped +{ + return YES; +} + +@end diff --git a/Source/SPGroupNode.h b/Source/SPGroupNode.h new file mode 100644 index 00000000..8c4f2e84 --- /dev/null +++ b/Source/SPGroupNode.h @@ -0,0 +1,47 @@ +// +// $Id$ +// +// SPGroupNode.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on November 21, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +/** + * @class SPGroupNode SPGroupNode.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * Tree node that represents a group. + */ +@interface SPGroupNode : NSObject <NSCopying, NSCoding> +{ + NSString *nodeName; +} + +/** + * @property nodeName The group node's name + */ +@property (readwrite, retain) NSString *nodeName; + +- (id)initWithName:(NSString *)name; + ++ (SPGroupNode *)groupNodeWithName:(NSString *)name; + +@end diff --git a/Source/SPGroupNode.m b/Source/SPGroupNode.m new file mode 100644 index 00000000..7bf1124b --- /dev/null +++ b/Source/SPGroupNode.m @@ -0,0 +1,103 @@ +// +// $Id$ +// +// SPGroupNode.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on November 21, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPGroupNode.h" + +// Constants +static NSString *SPGroupNodeNameKey = @"SPGroupNodeName"; + +@implementation SPGroupNode + +@synthesize nodeName; + +#pragma mark - +#pragma mark Initialisation + +- (id)init +{ + if ((self = [super init])) { + [self setNodeName:nil]; + } + + return self; +} + +- (id)initWithName:(NSString *)name +{ + if ((self = [self init])) { + [self setNodeName:name]; + } + + return self; +} + ++ (SPGroupNode *)groupNodeWithName:(NSString *)name +{ + return [[[self alloc] initWithName:name] autorelease]; +} + +#pragma mark - +#pragma mark Copying protocol methods + +- (id)copyWithZone:(NSZone *)zone +{ + SPGroupNode *node = [[[self class] allocWithZone:zone] init]; + + [node setNodeName:[self nodeName]]; + + return node; +} + +#pragma mark - +#pragma mark Coding protocol methods + +- (id)initWithCoder:(NSCoder *)coder +{ + [self setNodeName:[coder decodeObjectForKey:SPGroupNodeNameKey]]; + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeObject:[self nodeName] forKey:SPGroupNodeNameKey]; +} + +#pragma mark - +#pragma mark Other + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p ('%@')>", [self className], self, [self nodeName]]; +} + +#pragma mark - + +- (void)dealloc +{ + if (nodeName) [nodeName release], nodeName = nil; +} + +@end diff --git a/Source/SPMutableArrayAdditions.h b/Source/SPMutableArrayAdditions.h new file mode 100644 index 00000000..c39e931a --- /dev/null +++ b/Source/SPMutableArrayAdditions.h @@ -0,0 +1,37 @@ +// +// $Id$ +// +// SPMutableArrayAdditions.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on February 2, 2011 +// Copyright (c) 2011 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +/** + * @category SPMutableArrayAdditionsTest SPMutableArrayAdditionsTest.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * NSMutableArray additions category. + */ +@interface NSMutableArray (SPMutableArrayAdditions) + +- (void)reverse; + +@end diff --git a/Source/SPMutableArrayAdditions.m b/Source/SPMutableArrayAdditions.m new file mode 100644 index 00000000..529bf784 --- /dev/null +++ b/Source/SPMutableArrayAdditions.m @@ -0,0 +1,48 @@ +// +// $Id$ +// +// SPMutableArrayAdditions.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on February 2, 2011 +// Copyright (c) 2011 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPMutableArrayAdditions.h" +#import "SPArrayAdditions.h" + +@implementation NSMutableArray (SPMutableArrayAdditions) + +- (void)reverse +{ + NSUInteger count = [self count]; + + for (NSUInteger i = 0; i < (count / 2); i++) + { + NSUInteger j = ((count - i) - 1); + + id obj = [NSArrayObjectAtIndex(self, i) retain]; + + [self replaceObjectAtIndex:i withObject:NSArrayObjectAtIndex(self, j)]; + [self replaceObjectAtIndex:j withObject:obj]; + + [obj release]; + } +} + +@end diff --git a/Source/SPPreferenceController.h b/Source/SPPreferenceController.h index 6b0db9df..f05b63e3 100644 --- a/Source/SPPreferenceController.h +++ b/Source/SPPreferenceController.h @@ -27,7 +27,6 @@ @class SPGeneralPreferencePane, SPTablesPreferencePane, - SPFavoritesPreferencePane, SPNotificationsPreferencePane, SPEditorPreferencePane, SPAutoUpdatePreferencePane, @@ -45,7 +44,6 @@ // Preference pane controllers IBOutlet SPGeneralPreferencePane <SPPreferencePaneProtocol> *generalPreferencePane; IBOutlet SPTablesPreferencePane <SPPreferencePaneProtocol> *tablesPreferencePane; - IBOutlet SPFavoritesPreferencePane <SPPreferencePaneProtocol> *favoritesPreferencePane; IBOutlet SPNotificationsPreferencePane <SPPreferencePaneProtocol> *notificationsPreferencePane; IBOutlet SPEditorPreferencePane <SPPreferencePaneProtocol> *editorPreferencePane; IBOutlet SPAutoUpdatePreferencePane <SPPreferencePaneProtocol> *autoUpdatePreferencePane; @@ -69,7 +67,6 @@ @property (readonly) SPGeneralPreferencePane *generalPreferencePane; @property (readonly) SPTablesPreferencePane *tablesPreferencePane; -@property (readonly) SPFavoritesPreferencePane *favoritesPreferencePane; @property (readonly) SPNotificationsPreferencePane *notificationsPreferencePane; @property (readonly) SPEditorPreferencePane *editorPreferencePane; @property (readonly) SPAutoUpdatePreferencePane *autoUpdatePreferencePane; @@ -81,14 +78,11 @@ */ @property (readwrite, assign) NSUInteger fontChangeTarget; - // Toolbar item IBAction methods - (IBAction)displayPreferencePane:(id)sender; - (IBAction)displayTablePreferences:(id)sender; -- (IBAction)displayFavoritePreferences:(id)sender; - (IBAction)displayEditorPreferences:(id)sender; -// Other - (void)changeFont:(id)sender; @end diff --git a/Source/SPPreferenceController.m b/Source/SPPreferenceController.m index 0d6d46aa..01d39e25 100644 --- a/Source/SPPreferenceController.m +++ b/Source/SPPreferenceController.m @@ -42,7 +42,6 @@ @synthesize generalPreferencePane; @synthesize tablesPreferencePane; -@synthesize favoritesPreferencePane; @synthesize notificationsPreferencePane; @synthesize editorPreferencePane; @synthesize autoUpdatePreferencePane; @@ -78,7 +77,6 @@ generalPreferencePane, tablesPreferencePane, notificationsPreferencePane, - favoritesPreferencePane, editorPreferencePane, autoUpdatePreferencePane, networkPreferencePane, @@ -128,27 +126,6 @@ [self _resizeWindowForContentView:[tablesPreferencePane preferencePaneView]]; } -/** - * Displays the favorite preferences pane. - */ -- (IBAction)displayFavoritePreferences:(id)sender -{ - // To make the Favorites pane resizable give the window a minimum size and display the resize indicator. - // Notice that we still make all other panes non-resizable by removing the dsiplay of the indicator and - // resetting the minimum size to zero. - [[self window] setMinSize:NSMakeSize(500, 381)]; - [[self window] setShowsResizeIndicator:[favoritesPreferencePane preferencePaneAllowsResizing]]; - - [toolbar setSelectedItemIdentifier:[favoritesPreferencePane preferencePaneIdentifier]]; - - [self _resizeWindowForContentView:[favoritesPreferencePane preferencePaneView]]; - - // Set the default favorite popup back to preference - if ([sender isKindOfClass:[NSMenuItem class]]) { - [generalPreferencePane updateDefaultFavoritePopupSelection]; - } -} - /** * Displays the editor preferences pane. */ @@ -226,14 +203,6 @@ [tablesItem setTarget:self]; [tablesItem setAction:@selector(displayTablePreferences:)]; - // Favorite preferences - favoritesItem = [[NSToolbarItem alloc] initWithItemIdentifier:[favoritesPreferencePane preferencePaneIdentifier]]; - - [favoritesItem setLabel:[favoritesPreferencePane preferencePaneName]]; - [favoritesItem setImage:[favoritesPreferencePane preferencePaneIcon]]; - [favoritesItem setTarget:self]; - [favoritesItem setAction:@selector(displayFavoritePreferences:)]; - // Notification preferences notificationsItem = [[NSToolbarItem alloc] initWithItemIdentifier:[notificationsPreferencePane preferencePaneIdentifier]]; diff --git a/Source/SPPreferenceControllerDelegate.m b/Source/SPPreferenceControllerDelegate.m index 56bb807a..5b531066 100644 --- a/Source/SPPreferenceControllerDelegate.m +++ b/Source/SPPreferenceControllerDelegate.m @@ -65,9 +65,6 @@ else if ([itemIdentifier isEqualToString:SPPreferenceToolbarTables]) { return tablesItem; } - else if ([itemIdentifier isEqualToString:SPPreferenceToolbarFavorites]) { - return favoritesItem; - } else if ([itemIdentifier isEqualToString:SPPreferenceToolbarNotifications]) { return notificationsItem; } @@ -92,7 +89,6 @@ return [NSArray arrayWithObjects: SPPreferenceToolbarGeneral, SPPreferenceToolbarTables, - SPPreferenceToolbarFavorites, SPPreferenceToolbarNotifications, SPPreferenceToolbarEditor, SPPreferenceToolbarShortcuts, @@ -106,7 +102,6 @@ return [NSArray arrayWithObjects: SPPreferenceToolbarGeneral, SPPreferenceToolbarTables, - SPPreferenceToolbarFavorites, SPPreferenceToolbarNotifications, SPPreferenceToolbarEditor, SPPreferenceToolbarShortcuts, @@ -120,7 +115,6 @@ return [NSArray arrayWithObjects: SPPreferenceToolbarGeneral, SPPreferenceToolbarTables, - SPPreferenceToolbarFavorites, SPPreferenceToolbarNotifications, SPPreferenceToolbarEditor, SPPreferenceToolbarShortcuts, diff --git a/Source/SPPreferencesUpgrade.m b/Source/SPPreferencesUpgrade.m index 1583d5fb..10a86a5d 100644 --- a/Source/SPPreferencesUpgrade.m +++ b/Source/SPPreferencesUpgrade.m @@ -26,6 +26,9 @@ #import "SPPreferencesUpgrade.h" #import "SPKeychain.h" +static NSString *SPOldFavoritesKey = @"favorites"; +static NSString *SPOldDefaultEncodingKey = @"DefaultEncoding"; + @implementation SPPreferencesUpgrade /** @@ -124,7 +127,7 @@ void SPApplyRevisionChanges(void) @"showError", SPShowNoAffectedRowsError, @"connectionTimeout", SPConnectionTimeoutValue, @"keepAliveInterval", SPKeepAliveInterval, - @"lastFavoriteIndex", SPLastFavoriteIndex, + @"lastFavoriteIndex", SPLastFavoriteID, nil]; keyEnumerator = [keysToUpgrade keyEnumerator]; @@ -145,8 +148,8 @@ void SPApplyRevisionChanges(void) } // For versions prior to r567 (0.9.5), add a timestamp-based identifier to favorites and keychain entries - if (recordedVersionNumber < 567 && [prefs objectForKey:SPFavorites]) { - NSMutableArray *favoritesArray = [NSMutableArray arrayWithArray:[prefs objectForKey:SPFavorites]]; + if (recordedVersionNumber < 567 && [prefs objectForKey:SPOldFavoritesKey]) { + NSMutableArray *favoritesArray = [NSMutableArray arrayWithArray:[prefs objectForKey:SPOldFavoritesKey]]; NSMutableDictionary *favorite; NSString *password, *keychainName, *keychainAccount; SPKeychain *upgradeKeychain = [[SPKeychain alloc] init]; @@ -173,14 +176,14 @@ void SPApplyRevisionChanges(void) [favoritesArray replaceObjectAtIndex:i withObject:[NSDictionary dictionaryWithDictionary:favorite]]; } - [prefs setObject:[NSArray arrayWithArray:favoritesArray] forKey:SPFavorites]; + [prefs setObject:[NSArray arrayWithArray:favoritesArray] forKey:SPOldFavoritesKey]; [upgradeKeychain release]; password = nil; } // For versions prior to r981 (~0.9.6), upgrade the favourites to include a connection type for each - if (recordedVersionNumber < 981 && [prefs objectForKey:SPFavorites]) { - NSMutableArray *favoritesArray = [NSMutableArray arrayWithArray:[prefs objectForKey:SPFavorites]]; + if (recordedVersionNumber < 981 && [prefs objectForKey:SPOldFavoritesKey]) { + NSMutableArray *favoritesArray = [NSMutableArray arrayWithArray:[prefs objectForKey:SPOldFavoritesKey]]; NSMutableDictionary *favorite; // Cycle through the favorites @@ -213,7 +216,7 @@ void SPApplyRevisionChanges(void) [favoritesArray replaceObjectAtIndex:i withObject:[NSDictionary dictionaryWithDictionary:favorite]]; } - [prefs setObject:[NSArray arrayWithArray:favoritesArray] forKey:SPFavorites]; + [prefs setObject:[NSArray arrayWithArray:favoritesArray] forKey:SPOldFavoritesKey]; } // For versions prior to r1128 (~0.9.6), reset the main window toolbar items to add new items @@ -266,7 +269,7 @@ void SPApplyRevisionChanges(void) } // For versions prior to 2325 (<0.9.9), convert the old encoding pref string into the new localizable constant - if (recordedVersionNumber < 2325 && [prefs objectForKey:@"DefaultEncoding"] && [[prefs objectForKey:@"DefaultEncoding"] isKindOfClass:[NSString class]]) { + if (recordedVersionNumber < 2325 && [prefs objectForKey:SPOldDefaultEncodingKey] && [[prefs objectForKey:SPOldDefaultEncodingKey] isKindOfClass:[NSString class]]) { NSDictionary *encodingMap = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt:SPEncodingAutodetect], @"Autodetect", [NSNumber numberWithInt:SPEncodingUCS2], @"UCS-2 Unicode (ucs2)", @@ -289,7 +292,7 @@ void SPApplyRevisionChanges(void) [NSNumber numberWithInt:SPEncodingEUCKRKorean], @"EUC-KR Korean (euckr)", nil]; - NSNumber *newMappedValue = [encodingMap valueForKey:[prefs objectForKey:@"DefaultEncoding"]]; + NSNumber *newMappedValue = [encodingMap valueForKey:[prefs objectForKey:SPOldDefaultEncodingKey]]; if (newMappedValue == nil) newMappedValue = [NSNumber numberWithInt:0]; @@ -301,7 +304,7 @@ void SPApplyRevisionChanges(void) } /** - * Attempts to migrate the user's connection favorites from their preference file to the new Favaorites + * Attempts to migrate the user's connection favorites from their preference file to the new favorites * plist in the application's support 'Data' directory. */ void SPMigrateConnectionFavoritesData(void) @@ -310,6 +313,26 @@ void SPMigrateConnectionFavoritesData(void) NSFileManager *fileManager = [NSFileManager defaultManager]; NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; + NSMutableArray *favorites = [[NSMutableArray alloc] initWithArray:[prefs objectForKey:SPOldFavoritesKey]]; + + // Change the last used favorite and default favorite's indexes to be ID based + if (![prefs objectForKey:SPLastFavoriteID] && [favorites count]) { + + NSInteger lastFavoriteIndex = [prefs integerForKey:@"LastFavoriteIndex"]; + NSInteger defaultFavoriteIndex = [prefs integerForKey:SPDefaultFavorite]; + + if ((lastFavoriteIndex >= (NSInteger)0) && ((NSUInteger)lastFavoriteIndex <= [favorites count])) { + [prefs setInteger:[[[favorites objectAtIndex:lastFavoriteIndex] objectForKey:SPFavoriteIDKey] integerValue] forKey:SPLastFavoriteID]; + } + + if ((defaultFavoriteIndex >= (NSInteger)0) && ((NSUInteger)defaultFavoriteIndex <= [favorites count])) { + [prefs setInteger:[[[favorites objectAtIndex:defaultFavoriteIndex] objectForKey:SPFavoriteIDKey] integerValue] forKey:SPDefaultFavorite]; + } + + // TOOD: Favorites migration - only uncomment when we want to remove backwards compatibility + //[prefs removeObjectForKey:@"LastFavoriteIndex"]; + } + NSString *dataPath = [fileManager applicationSupportDirectoryForSubDirectory:SPDataSupportFolder error:&error]; if (error) { @@ -322,7 +345,7 @@ void SPMigrateConnectionFavoritesData(void) // Only proceed if the new favorites plist doesn't already exist if (![fileManager fileExistsAtPath:favoritesFile]) { - NSDictionary *newFavorites = [NSDictionary dictionaryWithObject:[NSDictionary dictionaryWithObjectsAndKeys:NSLocalizedString(@"Favorites", @"favorites label"), SPFavoritesGroupNameKey, [prefs objectForKey:SPFavorites], SPFavoriteChildrenKey, nil] forKey:SPFavoritesRootKey]; + NSDictionary *newFavorites = [NSDictionary dictionaryWithObject:[NSDictionary dictionaryWithObjectsAndKeys:NSLocalizedString(@"Favorites", @"favorites label"), SPFavoritesGroupNameKey, favorites, SPFavoriteChildrenKey, nil] forKey:SPFavoritesRootKey]; error = nil; NSString *errorString = nil; @@ -337,8 +360,8 @@ void SPMigrateConnectionFavoritesData(void) NSLog(@"Error migrating favorites data: %@", [error localizedDescription]); } else { - // Only uncomment when migration is complete - //[prefs removeObjectForKey:SPFavorites]; + // TOOD: Favorites migration - only uncomment when we want to remove backwards compatibility + //[prefs removeObjectForKey:SPOldFavoritesKey]; } } else if (errorString) { @@ -348,6 +371,8 @@ void SPMigrateConnectionFavoritesData(void) return; } } + + [favorites release]; } @end diff --git a/Source/SPProcessListController.m b/Source/SPProcessListController.m index f1aae941..cc1b40c4 100644 --- a/Source/SPProcessListController.m +++ b/Source/SPProcessListController.m @@ -29,7 +29,10 @@ #import "SPAppController.h" #import <SPMySQL/SPMySQL.h> -static const NSString *SPTableViewIDColumnIdentifier = @"Id"; +// Constants +static NSString *SPKillProcessQueryMode = @"SPKillProcessQueryMode"; +static NSString *SPKillProcessConnectionMode = @"SPKillProcessConnectionMode"; +static NSString *SPTableViewIDColumnIdentifier = @"Id"; @interface SPProcessListController (PrivateAPI) @@ -63,6 +66,8 @@ static const NSString *SPTableViewIDColumnIdentifier = @"Id"; autoRefreshTimer = nil; processListThreadRunning = NO; + showFullProcessList = [prefs boolForKey:SPProcessListShowFullProcessList]; + processes = [[NSMutableArray alloc] init]; prefs = [NSUserDefaults standardUserDefaults]; diff --git a/Source/SPQueryFavoriteManager.m b/Source/SPQueryFavoriteManager.m index 625f9855..be63e2a4 100644 --- a/Source/SPQueryFavoriteManager.m +++ b/Source/SPQueryFavoriteManager.m @@ -585,7 +585,7 @@ */ - (void)tableView:(NSTableView*)tableView didClickTableColumn:(NSTableColumn *)tableColumn { - // TODO: Not yet implemented + // TODO: Implement me return; } diff --git a/Source/SPSSHTunnel.m b/Source/SPSSHTunnel.m index aae71891..4839c886 100644 --- a/Source/SPSSHTunnel.m +++ b/Source/SPSSHTunnel.m @@ -356,19 +356,6 @@ // Launch and run the tunnel [task launch]; - // TODO: The below code doesn't actually appear to work. We will probably have to switch to system()/exec() for grouped children... - // Apply the process group to the child task to ensure it quits with the parent process. - // Note that if run from within Xcode, Xcode is the parent process! -/* pid_t group = setsid(); - if (group == -1) group = getpgrp(); - if(setpgid([task processIdentifier], group) == -1) { - connectionState = SPSSH_STATE_IDLE; - [task terminate]; - if (lastError) [lastError release]; - lastError = [[NSString alloc] initWithFormat:NSLocalizedString(@"The SSH Tunnel could not safely be marked as belonging to Sequel Pro, and so has been shut down for security reasons. Please try again.\n\n(Error %i)", @"SSH tunnel could not be security marked by Sequel Pro"), errno]; - if (delegate) [delegate performSelectorOnMainThread:stateChangeSelector withObject:self waitUntilDone:NO]; - }*/ - // Listen for output [task waitUntilExit]; diff --git a/Source/SPTableCopy.m b/Source/SPTableCopy.m index 47c425a7..fd9b9055 100644 --- a/Source/SPTableCopy.m +++ b/Source/SPTableCopy.m @@ -91,23 +91,27 @@ { BOOL success = YES; - //disable foreign key checks + // Disable foreign key checks [connection queryString:@"/*!32352 SET foreign_key_checks=0 */"]; - if([connection queryErrored]) + + if ([connection queryErrored]) { success = NO; + } - //copy tables - for(NSString *tableName in tablesArray) { - if(![self copyTable:tableName from:sourceDB to:targetDB withContent:copyWithContent]) + for (NSString *tableName in tablesArray) + { + if (![self copyTable:tableName from:sourceDB to:targetDB withContent:copyWithContent]) { success = NO; + } } - //enable foreign key checks + // Enable foreign key checks [connection queryString:@"/*!32352 SET foreign_key_checks=1 */"]; - if([connection queryErrored]) + + if ([connection queryErrored]) { success = NO; + } - //done return success; } @@ -119,12 +123,10 @@ [targetDB backtickQuotedString], [tableName backtickQuotedString] ]; - // Move the table + [connection queryString:moveStatement]; - if ([connection queryErrored]) return NO; - - return YES; + return ![connection queryErrored]; } @end diff --git a/Source/SPTreeNode.h b/Source/SPTreeNode.h new file mode 100644 index 00000000..7e4290b4 --- /dev/null +++ b/Source/SPTreeNode.h @@ -0,0 +1,56 @@ +// +// $Id$ +// +// SPTreeNode.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on November 23, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +/** + * @class SPTreeNode SPTreeNode.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * NSTreeNode subclass which adds some convenience methods. + */ +@interface SPTreeNode : NSTreeNode <NSCoding> +{ + BOOL isGroup; +} + +/** + * @property isGroup Indicates whether or not the node is a group. + */ +@property (readwrite, assign) BOOL isGroup; + +- (void)removeObjectFromChildren:(id)object; + +- (NSMutableArray *)descendants; +- (NSMutableArray *)childLeafs; +- (NSMutableArray *)allChildLeafs; +- (NSMutableArray *)groupChildren; + +- (SPTreeNode *)parentFromArray:(NSArray *)array; + +- (BOOL)isDescendantOfOrOneOfNodes:(NSArray *)nodes; + +- (NSDictionary *)dictionaryRepresentation; + +@end diff --git a/Source/SPTreeNode.m b/Source/SPTreeNode.m new file mode 100644 index 00000000..f787816a --- /dev/null +++ b/Source/SPTreeNode.m @@ -0,0 +1,296 @@ +// +// $Id$ +// +// SPTreeNode.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on November 23, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPTreeNode.h" +#import "SPFavoriteNode.h" +#import "SPGroupNode.h" + +// Constants +static NSString *SPTreeNodeIsGroupKey = @"SPTreeNodeIsGroup"; + +@implementation SPTreeNode + +@synthesize isGroup; + +#pragma mark - +#pragma mark Initialisation + ++ (id)treeNodeWithRepresentedObject:(id)object +{ + return [[[SPTreeNode alloc] initWithRepresentedObject:object] autorelease]; +} + +- (id)initWithRepresentedObject:(id)object +{ + if ((self = [super initWithRepresentedObject:object])) { + [self setIsGroup:NO]; + } + + return self; +} + +#pragma mark - +#pragma mark Public API + +/** + * Recursive method which searches children and children of all sub-nodes + * to remove the supplied object. + * + * @param object The object to remove + */ +- (void)removeObjectFromChildren:(id)object +{ + for (SPTreeNode *node in [self childNodes]) + { + if (node == object) { + [[self mutableChildNodes] removeObjectIdenticalTo:object]; + return; + } + + if ([node isGroup]) { + [node removeObjectFromChildren:object]; + } + } +} + +/** + * Generates an array of all descendants. + * + * @return The array of decendant nodes. + */ +- (NSMutableArray *)descendants +{ + NSMutableArray *descendants = [NSMutableArray array]; + + for (SPTreeNode *node in [self childNodes]) + { + [descendants addObject:node]; + + if ([node isGroup]) { + [descendants addObjectsFromArray:[node descendants]]; + } + } + + return descendants; +} + +/** + * Generates an array of this node's child leafs nodes. + * + * @return The array of child nodes. + */ +- (NSMutableArray *)childLeafs +{ + NSMutableArray *childLeafs = [NSMutableArray array]; + + for (SPTreeNode *node in [self childNodes]) + { + if (![node isGroup]) { + [childLeafs addObject:node]; + } + } + + return childLeafs; +} + +/** + * Generates an array of all leafs in children and children of all sub-nodes (effectively all leaf nodes below + * this node. + * + * @return The array of child nodes. + */ +- (NSMutableArray *)allChildLeafs +{ + NSMutableArray *childLeafs = [NSMutableArray array]; + + for (SPTreeNode *node in [self childNodes]) + { + if (![node isGroup]) { + [childLeafs addObject:node]; + } + else { + [childLeafs addObjectsFromArray:[node allChildLeafs]]; + } + } + + return childLeafs; +} + +/** + * Returns only the children that are group nodes. + * + * @return The array of child group nodes. + */ +- (NSMutableArray *)groupChildren +{ + NSMutableArray *groupChildren = [NSMutableArray array]; + + for (SPTreeNode *node in [self childNodes]) + { + if ([node isGroup]) { + [groupChildren addObject:node]; + } + } + + return groupChildren; +} + +/** + * Finds the receiver's parent from the supplied array of nodes. + * + * @param array The array of nodes + * + * @return The parent of this instance of nil if not found + */ +- (SPTreeNode *)parentFromArray:(NSArray *)array +{ + SPTreeNode *result = nil; + + for (SPTreeNode *node in array) + { + if (node == self) break; + + if ([[node childNodes] indexOfObjectIdenticalTo:self] != NSNotFound) { + result = node; + break; + } + + if ([node isGroup]) { + SPTreeNode *innerNode = [self parentFromArray:[node childNodes]]; + + if (innerNode) { + result = innerNode; + break; + } + } + } + + return result; +} + +/** + * Returns YES if self is contained anywhere inside the children or children of + * sub-nodes of the nodes contained inside the supplied array. + * + * @param nodes The array of nodes to search + * + * @return A BOOL indicating whether or not it's a descendent + */ +- (BOOL)isDescendantOfOrOneOfNodes:(NSArray *)nodes +{ + for (SPTreeNode *node in nodes) + { + if (node == self) return YES; + + // Check all the sub-nodes + if ([node isGroup]) { + if ([self isDescendantOfOrOneOfNodes:[node childNodes]]) { + return YES; + } + } + } + + return NO; +} + +/** + * Constructs a dictionary representation of the favorite. + * + * @return The dictionary representation. + */ +- (NSDictionary *)dictionaryRepresentation +{ + NSMutableDictionary *dictionary = nil; + + id object = [self representedObject]; + + if ([object isKindOfClass:[SPFavoriteNode class]]) { + + dictionary = [NSDictionary dictionaryWithDictionary:[object nodeFavorite]]; + } + else if ([object isKindOfClass:[SPGroupNode class]]) { + + NSMutableArray *children = [NSMutableArray array]; + + for (SPTreeNode *node in [self childNodes]) + { + NSDictionary *representation = [node dictionaryRepresentation]; + + if (representation) { + [children addObject:representation]; + } + } + + dictionary = [NSMutableDictionary dictionary]; + + NSString *name = (![self parentNode]) ? NSLocalizedString(@"Favorites", @"favorites label") : [object nodeName]; + + [dictionary setObject:name ? name : @"" forKey:SPFavoritesGroupNameKey]; + [dictionary setObject:children forKey:SPFavoriteChildrenKey]; + } + + return dictionary; +} + +#pragma mark - +#pragma mark Coding protocol methods + +- (id)initWithCoder:(NSCoder *)coder +{ + [self setIsGroup:[[coder decodeObjectForKey:SPTreeNodeIsGroupKey] boolValue]]; + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeObject:[NSNumber numberWithBool:[self isGroup]] forKey:SPTreeNodeIsGroupKey]; +} + +#pragma mark - +#pragma mark Other + +- (NSString *)description +{ + NSMutableString *description = [NSMutableString string]; + + [description appendString:[[self representedObject] description]]; + [description appendString:@"\n"]; + + NSArray *nodes = [self childNodes]; + + for (NSUInteger i = 0; i < [nodes count]; i++) + { + SPTreeNode *node = [nodes objectAtIndex:i]; + + [description appendString:([node isGroup]) ? [node description] : [[node representedObject] description]]; + + if (i < ([nodes count] - 1)) [description appendString:@"\n"]; + } + + return description; +} + +@end |