diff options
author | rowanbeentje <rowan@beent.je> | 2012-10-06 11:48:15 +0000 |
---|---|---|
committer | rowanbeentje <rowan@beent.je> | 2012-10-06 11:48:15 +0000 |
commit | ecb5c70566d1303288e4faf170bda40672a799e1 (patch) | |
tree | 2882fa5fd6f25eed9c754e810785f5834225b95f /Source | |
parent | ada181f6fe5b010a5ab56030d16b35e92e58af10 (diff) | |
download | sequelpro-ecb5c70566d1303288e4faf170bda40672a799e1.tar.gz sequelpro-ecb5c70566d1303288e4faf170bda40672a799e1.tar.bz2 sequelpro-ecb5c70566d1303288e4faf170bda40672a799e1.zip |
Change the connection screen interface, particularly relating to favourite editing on the connection screen (Issue #1339, Issue #1440):
- No longer save changes made to connection favourites as soon as the interface is updated
- Alter the interfaace if favourites are editing, offering to save the changes either to the old connection favourite or to a new favourite
- Add a "Test connection" button to verify changes before saving
- Add a "Quick Connect" entry to the top of the connection view's favourites list so a blank form is always available
- Use a custom highlight when editing favourites to show the favourite has changed but is still linked
- Reduce the margin on the left-hand side of the connection favourites list to increase the available space
- Alter favourite name generation, making it less aggressive when generating names from partial details (eg creating names of just "@") and removing the user
- Alter key icon usage to correctly update the button appearance if an SSL or SSH key is selected
Diffstat (limited to 'Source')
-rw-r--r-- | Source/SPConnectionController.h | 25 | ||||
-rw-r--r-- | Source/SPConnectionController.m | 581 | ||||
-rw-r--r-- | Source/SPConnectionControllerDataSource.m | 36 | ||||
-rw-r--r-- | Source/SPConnectionControllerDelegate.m | 220 | ||||
-rw-r--r-- | Source/SPConnectionControllerInitializer.m | 37 | ||||
-rw-r--r-- | Source/SPConnectionHandler.m | 61 | ||||
-rw-r--r-- | Source/SPFavoriteTextFieldCell.h | 16 | ||||
-rw-r--r-- | Source/SPFavoriteTextFieldCell.m | 208 | ||||
-rw-r--r-- | Source/SPFavoritesOutlineView.m | 63 |
9 files changed, 714 insertions, 533 deletions
diff --git a/Source/SPConnectionController.h b/Source/SPConnectionController.h index de517fdf..dda04343 100644 --- a/Source/SPConnectionController.h +++ b/Source/SPConnectionController.h @@ -42,7 +42,8 @@ SPSplitView #ifndef SP_REFACTOR /* class decl */ ,SPKeychain, - SPFavoriteNode + SPFavoriteNode, + SPFavoriteTextFieldCell #endif ; @@ -64,6 +65,8 @@ BOOL cancellingConnection; BOOL isConnecting; + BOOL isEditingConnection; + BOOL isTestingConnection; // Standard details NSInteger previousType; @@ -141,25 +144,31 @@ IBOutlet NSButton *socketSSLCertificateButton; IBOutlet NSButton *socketSSLCACertButton; - IBOutlet NSButton *addToFavoritesButton; IBOutlet NSButton *connectButton; + IBOutlet NSButton *testConnectButton; IBOutlet NSButton *helpButton; + IBOutlet NSButton *saveFavoriteButton; IBOutlet NSProgressIndicator *progressIndicator; IBOutlet NSTextField *progressIndicatorText; IBOutlet NSMenuItem *favoritesSortByMenuItem; IBOutlet NSView *exportPanelAccessoryView; - - BOOL isEditing; + IBOutlet NSView *editButtonsView; + + BOOL isEditingItemName; BOOL reverseFavoritesSort; BOOL initComplete; BOOL mySQLConnectionCancelled; - BOOL favoriteNameFieldWasTouched; + BOOL favoriteNameFieldWasAutogenerated; #ifndef SP_REFACTOR /* ivars */ NSArray *draggedNodes; NSImage *folderImage; SPTreeNode *favoritesRoot; + SPTreeNode *quickConnectItem; + + SPFavoriteTextFieldCell *quickConnectCell; + NSDictionary *currentFavorite; SPFavoritesController *favoritesController; SPFavoritesSortItem currentSortItem; @@ -198,6 +207,7 @@ #endif @property (readonly, assign) BOOL isConnecting; +@property (readonly, assign) BOOL isEditingConnection; // Connection processes - (IBAction)initiateConnection:(id)sender; @@ -211,18 +221,19 @@ - (IBAction)updateSSLInterface:(id)sender; - (IBAction)updateKeyLocationFileVisibility:(id)sender; - (void)updateSplitViewSize; + - (void)resizeTabViewToConnectionType:(NSUInteger)theType animating:(BOOL)animate; + - (IBAction)sortFavorites:(id)sender; - (IBAction)reverseSortFavorites:(NSMenuItem *)sender; -- (void)resizeTabViewToConnectionType:(NSUInteger)theType animating:(BOOL)animate; - // Favorites interaction - (void)updateFavoriteSelection:(id)sender; - (NSMutableDictionary *)selectedFavorite; - (SPTreeNode *)selectedFavoriteNode; - (NSArray *)selectedFavoriteNodes; +- (IBAction)saveFavorite:(id)sender; - (IBAction)addFavorite:(id)sender; - (IBAction)addFavoriteUsingCurrentDetails:(id)sender; - (IBAction)addGroup:(id)sender; diff --git a/Source/SPConnectionController.m b/Source/SPConnectionController.m index d6078f1d..a6ca5f75 100644 --- a/Source/SPConnectionController.m +++ b/Source/SPConnectionController.m @@ -31,6 +31,7 @@ // More info at <http://code.google.com/p/sequel-pro/> #import "SPConnectionController.h" +#import "SPConnectionHandler.h" #import "SPDatabaseDocument.h" #ifndef SP_REFACTOR /* headers */ @@ -67,6 +68,10 @@ static NSString *SPExportFavoritesFilename = @"SequelProFavorites.plist"; @interface SPConnectionController () +// Privately redeclare as read/write to get the synthesized setter +@property (readwrite, assign) BOOL isEditingConnection; + +- (void)_saveCurrentDetailsCreatingNewFavorite:(BOOL)createNewFavorite; - (BOOL)_checkHost; #ifndef SP_REFACTOR - (void)_sortFavorites; @@ -83,13 +88,22 @@ static NSString *SPExportFavoritesFilename = @"SequelProFavorites.plist"; - (SPTreeNode *)_favoriteNodeForFavoriteID:(NSInteger)favoriteID; - (NSString *)_stripInvalidCharactersFromString:(NSString *)subject; -- (void)_updateFavoritePasswordsFromField:(NSControl *)control; +- (NSString *)_generateNameForConnection; + +- (void)_startEditingConnection; static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, void *key); #endif @end +@interface SPConnectionController (SPConnectionControllerDelegate) + +- (void)_stopEditingConnection; + +@end + + @implementation SPConnectionController @synthesize delegate; @@ -125,6 +139,7 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, @synthesize connectionSSHKeychainItemAccount; @synthesize isConnecting; +@synthesize isEditingConnection; #pragma mark - #pragma mark Connection processes @@ -143,6 +158,9 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, if (sender == favoritesOutlineView && [favoritesOutlineView clickedRow] <= 0) return; #endif + // If triggered via the "Test Connection" button, set the state - otherwise clear it + isTestingConnection = (sender == testConnectButton); + // 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, [dbDocument parentWindow], self, nil, nil, NSLocalizedString(@"Insufficient details provided to establish a connection. Please enter at least the hostname.", @"insufficient details informative message")); @@ -212,9 +230,9 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, // Disable the favorites outline view to prevent further connections attempts [favoritesOutlineView setEnabled:NO]; - [addToFavoritesButton setHidden:YES]; [helpButton setHidden:YES]; [connectButton setEnabled:NO]; + [testConnectButton setEnabled:NO]; [progressIndicator startAnimation:self]; [progressIndicatorText setHidden:NO]; #endif @@ -226,7 +244,7 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, // have been changed or not; if not, leave the mark in place and remove the password from the field // for increased security. #ifndef SP_REFACTOR - if (connectionKeychainItemName) { + if (connectionKeychainItemName && !isTestingConnection) { if ([[keychain getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount] isEqualToString:[self password]]) { [self setPassword:[[NSString string] stringByPaddingToLength:[[self password] length] withString:@"sp" startingAtIndex:0]]; @@ -240,7 +258,7 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, } } - if (connectionSSHKeychainItemName) { + if (connectionSSHKeychainItemName && !isTestingConnection) { if ([[keychain getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount] isEqualToString:[self sshPassword]]) { [self setSshPassword:[[NSString string] stringByPaddingToLength:[[self sshPassword] length] withString:@"sp" startingAtIndex:0]]; [[sshSSHPasswordField undoManager] removeAllActionsWithTarget:sshSSHPasswordField]; @@ -302,10 +320,15 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, SPTreeNode *node = [self selectedFavoriteNode]; if (node) { + if (node == quickConnectItem) { + 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]; } + // Otherwise start editing the group node's name else { [favoritesOutlineView editColumn:0 row:[favoritesOutlineView selectedRow] withEvent:nil select:YES]; @@ -325,6 +348,11 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, keySelectionPanel = [NSOpenPanel openPanel]; [keySelectionPanel setShowsHiddenFiles:[prefs boolForKey:SPHiddenKeyFileVisibilityKey]]; + // If the button was toggled off, ensure editing is ended + if ([sender state] == NSOffState) { + [self _startEditingConnection]; + } + // Switch details by sender. // First, SSH keys: if (sender == sshSSHKeyButton) { @@ -402,6 +430,7 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, - (IBAction)updateSSLInterface:(id)sender { [self resizeTabViewToConnectionType:[self type] animating:YES]; + [self _startEditingConnection]; } /** @@ -436,7 +465,10 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, - (void)resizeTabViewToConnectionType:(NSUInteger)theType animating:(BOOL)animate { NSRect frameRect, targetResizeRect; - NSInteger additionalFormHeight = 55; + + // Use a magic number which needs to be added to the form when calculating resizes - + // including the height of the button areas below. + NSInteger additionalFormHeight = 92; frameRect = [connectionResizeContainer frame]; @@ -529,6 +561,7 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, currentFavorite = [fav copy]; [connectionResizeContainer setHidden:NO]; + [self _stopEditingConnection]; // Set up the type, also storing it in the previous type store to prevent type "changes" triggering actions NSUInteger connectionType = ([fav objectForKey:SPFavoriteTypeKey] ? [[fav objectForKey:SPFavoriteTypeKey] integerValue] : SPTCPIPConnection); @@ -639,6 +672,14 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, } /** + * Saves the current connection favorite. + */ +- (IBAction)saveFavorite:(id)sender +{ + [self _saveCurrentDetailsCreatingNewFavorite:NO]; +} + +/** * Adds a new connection favorite. */ - (IBAction)addFavorite:(id)sender @@ -687,7 +728,7 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, [[[[NSApp delegate] preferenceController] generalPreferencePane] updateDefaultFavoritePopup]; - favoriteNameFieldWasTouched = NO; + favoriteNameFieldWasAutogenerated = YES; [favoritesOutlineView editColumn:0 row:[favoritesOutlineView selectedRow] withEvent:nil select:YES]; } @@ -698,98 +739,7 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, */ - (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]) { - SPBeginAlertSheet(NSLocalizedString(@"Insufficient connection details", @"insufficient details message"), - NSLocalizedString(@"OK", @"OK button"), nil, nil, [dbDocument parentWindow], nil, nil, nil, - NSLocalizedString(@"Insufficient details provided to establish a connection. Please provide at least a host.", @"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, [dbDocument parentWindow], nil, nil, nil, - NSLocalizedString(@"Please enter the hostname for the SSH Tunnel, or disable the SSH Tunnel.", @"message of panel when ssh details are incomplete")); - return; - } - - // Ensure that a socket connection is not inadvertently used - 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 numberWithInteger:[self useSSL]] forKey:SPFavoriteUseSSLKey]; - [newFavorite setObject:[NSNumber numberWithInteger:[self sslKeyFileLocationEnabled]] forKey:SPFavoriteSSLKeyFileLocationEnabledKey]; - if ([self sslKeyFileLocation]) [newFavorite setObject:[self sslKeyFileLocation] forKey:SPFavoriteSSLKeyFileLocationKey]; - [newFavorite setObject:[NSNumber numberWithInteger:[self sslCertificateFileLocationEnabled]] forKey:SPFavoriteSSLCertificateFileLocationEnabledKey]; - if ([self sslCertificateFileLocation]) [newFavorite setObject:[self sslCertificateFileLocation] forKey:SPFavoriteSSLCertificateFileLocationKey]; - [newFavorite setObject:[NSNumber numberWithInteger:[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]; - if ([self sshPort]) [newFavorite setObject:[self sshPort] forKey:SPFavoriteSSHPortKey]; - [newFavorite setObject:[NSNumber numberWithInteger:[self sshKeyLocationEnabled]] forKey:SPFavoriteSSHKeyLocationEnabledKey]; - if ([self sshKeyLocation]) [newFavorite setObject:[self sshKeyLocation] forKey:SPFavoriteSSHKeyLocationKey]; - - // 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]]] - account:[keychain accountForUser:[self user] host:(([self type] == SPSocketConnection) ? @"localhost" : [self host]) database:[self database]]]; - } - - // 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]]]; - } - - SPTreeNode *selectedNode = [self selectedFavoriteNode]; - - SPTreeNode *node = [favoritesController addFavoriteNodeWithData:newFavorite asChildOfNode:[selectedNode isGroup] ? selectedNode : nil]; - - [self _reloadFavoritesViewData]; - [self _selectNode:node]; - - // Update the favorites popup button in the preferences - [[[[NSApp delegate] preferenceController] generalPreferencePane] updateDefaultFavoritePopup]; + [self _saveCurrentDetailsCreatingNewFavorite:YES]; } /** @@ -805,9 +755,7 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, [self _reloadFavoritesViewData]; [self _selectNode:node]; - - isEditing = YES; - + [favoritesOutlineView editColumn:0 row:[favoritesOutlineView selectedRow] withEvent:nil select:YES]; } @@ -999,26 +947,9 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, */ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - NSMutableDictionary *selectedFavorite = [self selectedFavorite]; - if (!selectedFavorite) return; - - id oldObject = [change objectForKey:NSKeyValueChangeOldKey]; - id newObject = [change objectForKey:NSKeyValueChangeNewKey]; - - if (oldObject != newObject) { - [selectedFavorite setObject:![newObject isNSNull] ? newObject : @"" forKey:keyPath]; - - // Save the new data to disk - [favoritesController saveFavorites]; - - [self _reloadFavoritesViewData]; - - if ([keyPath isEqualToString:SPFavoriteNameKey]) { - [[NSNotificationCenter defaultCenter] postNotificationName:SPConnectionFavoritesChangedNotification object:self]; - } - } } + #pragma mark - #pragma mark Sheet methods @@ -1082,6 +1013,8 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, [self setSslCACertFileLocation:abbreviatedFileName]; } + + [self _startEditingConnection]; } /** @@ -1119,15 +1052,9 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, if (returnCode == NSAlertAlternateReturn) { [self setType:SPSocketConnection]; [self setHost:@""]; -#ifndef SP_REFACTOR - [self _updateFavoritePasswordsFromField:standardSQLHostField]; -#endif } else { [self setHost:@"127.0.0.1"]; -#ifndef SP_REFACTOR - [self _updateFavoritePasswordsFromField:standardSQLHostField]; -#endif } } @@ -1135,6 +1062,245 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, #pragma mark Private API /** + * Take the current details and either save them to the currently selected + * favourite, or create a new connection favourite using them. + * If creating a new favourite, also select it and ensure the selected + * favourite is visible. + */ +- (void)_saveCurrentDetailsCreatingNewFavorite:(BOOL)createNewFavorite +{ + + // Complete any active editing + if ([[connectionView window] firstResponder]) { + [[connectionView window] endEditingFor:[[connectionView window] firstResponder]]; + } + + + // 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, [dbDocument parentWindow], nil, nil, nil, + NSLocalizedString(@"Insufficient details provided to establish a connection. Please provide at least a host.", @"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, [dbDocument parentWindow], nil, nil, nil, + NSLocalizedString(@"Please enter the hostname for the SSH Tunnel, or disable the SSH Tunnel.", @"message of panel when ssh details are incomplete")); + return; + } + + // Ensure that a socket connection is not inadvertently used + if (![self _checkHost]) return; + + + // Set up the favourite, or get the mutable dictionary for the current favourite. + NSMutableDictionary *theFavorite; + if (createNewFavorite) { + theFavorite = [NSMutableDictionary dictionary]; + [theFavorite setObject:[self _createNewFavoriteID] forKey:SPFavoriteIDKey]; + } else { + if (!currentFavorite) { + [NSException raise:NSInternalInconsistencyException format:@"Tried to save a current favourite with no currentFavorite"]; + } + theFavorite = [self selectedFavorite]; + } + + // Set the name - either taking the provided name, or generating one. + if ([[self name] length]) { + [theFavorite setObject:[self name] forKey:SPFavoriteNameKey]; + } else { + NSString *favoriteName = [self _generateNameForConnection]; + if (!favoriteName) { + favoriteName = NSLocalizedString(@"Untitled", @"Name for an untitled connection"); + } + [theFavorite setObject:favoriteName forKey:SPFavoriteNameKey]; + } + + // Set standard details for the connection + [theFavorite setObject:[NSNumber numberWithInteger:[self type]] forKey:SPFavoriteTypeKey]; + if ([self host]) { + [theFavorite setObject:[self host] forKey:SPFavoriteHostKey]; + } + if ([self socket]) { + [theFavorite setObject:[self socket] forKey:SPFavoriteSocketKey]; + } + if ([self user]) { + [theFavorite setObject:[self user] forKey:SPFavoriteUserKey]; + } + if ([self port]) { + [theFavorite setObject:[self port] forKey:SPFavoritePortKey]; + } + if ([self database]) { + [theFavorite setObject:[self database] forKey:SPFavoriteDatabaseKey]; + } + + // SSL details + if ([self useSSL]) { + [theFavorite setObject:[NSNumber numberWithInteger:[self useSSL]] forKey:SPFavoriteUseSSLKey]; + } + [theFavorite setObject:[NSNumber numberWithInteger:[self sslKeyFileLocationEnabled]] forKey:SPFavoriteSSLKeyFileLocationEnabledKey]; + if ([self sslKeyFileLocation]) { + [theFavorite setObject:[self sslKeyFileLocation] forKey:SPFavoriteSSLKeyFileLocationKey]; + } + [theFavorite setObject:[NSNumber numberWithInteger:[self sslCertificateFileLocationEnabled]] forKey:SPFavoriteSSLCertificateFileLocationEnabledKey]; + if ([self sslCertificateFileLocation]) { + [theFavorite setObject:[self sslCertificateFileLocation] forKey:SPFavoriteSSLCertificateFileLocationKey]; + } + [theFavorite setObject:[NSNumber numberWithInteger:[self sslCACertFileLocationEnabled]] forKey:SPFavoriteSSLCACertFileLocationEnabledKey]; + if ([self sslCACertFileLocation]) { + [theFavorite setObject:[self sslCACertFileLocation] forKey:SPFavoriteSSLCACertFileLocationKey]; + } + + // SSH details + if ([self sshHost]) { + [theFavorite setObject:[self sshHost] forKey:SPFavoriteSSHHostKey]; + } + if ([self sshUser]) { + [theFavorite setObject:[self sshUser] forKey:SPFavoriteSSHUserKey]; + } + if ([self sshPort]) { + [theFavorite setObject:[self sshPort] forKey:SPFavoriteSSHPortKey]; + } + [theFavorite setObject:[NSNumber numberWithInteger:[self sshKeyLocationEnabled]] forKey:SPFavoriteSSHKeyLocationEnabledKey]; + if ([self sshKeyLocation]) { + [theFavorite setObject:[self sshKeyLocation] forKey:SPFavoriteSSHKeyLocationKey]; + } + + + /** + * Password handling for the SQL connection + */ + NSString *oldKeychainName, *oldKeychainAccount, *newKeychainName, *newKeychainAccount;; + NSString *oldHostnameForPassword = ([[currentFavorite objectForKey:SPFavoriteTypeKey] integerValue] == SPSocketConnection) ? @"localhost" : [currentFavorite objectForKey:SPFavoriteHostKey]; + NSString *newHostnameForPassword = ([self type] == SPSocketConnection) ? @"localhost" : [self host]; + + // Grab the password for this connection + // Add the password to keychain as appropriate + NSString *sqlPassword = [self password]; + if (mySQLConnection && connectionKeychainItemName) { + sqlPassword = [keychain getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount]; + } + + // If creating a new favourite, always add the password to the keychain if it's set + if (createNewFavorite && [sqlPassword length]) { + [keychain addPassword:sqlPassword + forName:[keychain nameForFavoriteName:[theFavorite objectForKey:SPFavoriteNameKey] id:[theFavorite objectForKey:SPFavoriteIDKey]] + account:[keychain accountForUser:[self user] host:newHostnameForPassword database:[self database]]]; + } + + // If not creating a new favourite... + if (!createNewFavorite) { + + // Get the old keychain name and account strings + oldKeychainName = [keychain nameForFavoriteName:[currentFavorite objectForKey:SPFavoriteNameKey] id:[currentFavorite objectForKey:SPFavoriteIDKey]]; + oldKeychainAccount = [keychain accountForUser:[currentFavorite objectForKey:SPFavoriteUserKey] host:oldHostnameForPassword database:[currentFavorite objectForKey:SPFavoriteDatabaseKey]]; + + // If there's no new password, remove the old item from the keychain + if (![sqlPassword 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:[theFavorite objectForKey:SPFavoriteNameKey] id:[theFavorite objectForKey:SPFavoriteIDKey]]; + newKeychainAccount = [keychain accountForUser:[self user] host:newHostnameForPassword database:[self database]]; + if ([keychain passwordExistsForName:oldKeychainName account:oldKeychainAccount]) { + [keychain updateItemWithName:oldKeychainName account:oldKeychainAccount toName:newKeychainName account:newKeychainAccount password:sqlPassword]; + } else { + [keychain addPassword:sqlPassword forName:newKeychainName account:newKeychainAccount]; + } + } + } + sqlPassword = nil; + + + /** + * Password handling for the SSH connection + */ + NSString *theSSHPassword = [self sshPassword]; + if (mySQLConnection && connectionSSHKeychainItemName) { + theSSHPassword = [keychain getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]; + } + + // If creating a new favourite, always add the password if it's set + if (createNewFavorite && [theSSHPassword length]) { + [keychain addPassword:theSSHPassword + forName:[keychain nameForSSHForFavoriteName:[theFavorite objectForKey:SPFavoriteNameKey] id:[theFavorite objectForKey:SPFavoriteIDKey]] + account:[keychain accountForSSHUser:[self sshUser] sshHost:[self sshHost]]]; + } + + // If not creating a new favourite... + if (!createNewFavorite) { + + // Get the old keychain name and account strings + oldKeychainName = [keychain nameForSSHForFavoriteName:[currentFavorite objectForKey:SPFavoriteNameKey] id:[currentFavorite objectForKey:SPFavoriteIDKey]]; + oldKeychainAccount = [keychain accountForSSHUser:[currentFavorite objectForKey:SPFavoriteSSHUserKey] sshHost:[currentFavorite objectForKey:SPFavoriteSSHHostKey]]; + + // If there's no new password, remove the old item from the keychain + if (![theSSHPassword 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 nameForSSHForFavoriteName:[theFavorite objectForKey:SPFavoriteNameKey] id:[theFavorite objectForKey:SPFavoriteIDKey]]; + newKeychainAccount = [keychain accountForSSHUser:[self sshUser] sshHost:[self sshHost]]; + if ([keychain passwordExistsForName:oldKeychainName account:oldKeychainAccount]) { + [keychain updateItemWithName:oldKeychainName account:oldKeychainAccount toName:newKeychainName account:newKeychainAccount password:theSSHPassword]; + } else { + [keychain addPassword:theSSHPassword forName:newKeychainName account:newKeychainAccount]; + } + } + } + theSSHPassword = nil; + + + /** + * Saving the connection + */ + + // If creating the connection, add to the favourites tree. + if (createNewFavorite) { + SPTreeNode *selectedNode = [self selectedFavoriteNode]; + SPTreeNode *parentNode = nil; + + // If the current node is a group node, create the favorite as a child of it + if ([selectedNode isGroup]) { + parentNode = selectedNode; + + // Otherwise, create the new node as a sibling of the selected node if possible + } else if ([selectedNode parentNode] && [selectedNode parentNode] != favoritesRoot) { + parentNode = (SPTreeNode *)[selectedNode parentNode]; + } + + // Add the new node and select it + SPTreeNode *newNode = [favoritesController addFavoriteNodeWithData:theFavorite asChildOfNode:parentNode]; + [self _reloadFavoritesViewData]; + [self _selectNode:newNode]; + + // Update the favorites popup button in the preferences + [[[[NSApp delegate] preferenceController] generalPreferencePane] updateDefaultFavoritePopup]; + + // Otherwise, if editing the favourite, update it + } else { + [[[self selectedFavoriteNode] representedObject] setNodeFavorite:theFavorite]; + + // Save the new data to disk + [favoritesController saveFavorites]; + + [self _stopEditingConnection]; + + if (currentFavorite) [currentFavorite release], currentFavorite = nil; + currentFavorite = [theFavorite copy]; + + [self _reloadFavoritesViewData]; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:SPConnectionFavoritesChangedNotification object:self]; +} + +/** * Check the host field and ensure it isn't set to 'localhost' for non-socket connections. */ - (BOOL)_checkHost @@ -1281,16 +1447,12 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, } // Update the name for newly added favorites if not already touched by the user, by triggering a KVO update - if (![[self name] length]) { - [self setName:[NSString stringWithFormat:@"%@@%@", - ([favorite objectForKey:SPFavoriteUserKey]) ? [favorite objectForKey:SPFavoriteUserKey] : @"", - ((previousType == SPSocketConnection) ? @"localhost" : - (([favorite objectForKey:SPFavoriteHostKey]) ? [favorite valueForKeyPath:SPFavoriteHostKey] : @"")) - ]]; + if (![[self name] length] || favoriteNameFieldWasAutogenerated) { + NSString *favoriteName = [self _generateNameForConnection]; + if (favoriteName) { + [self setName:favoriteName]; + } } - - // Trigger a password change in response to host changes - [self _updateFavoritePasswordsFromField:nil]; } /** @@ -1350,13 +1512,12 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, [dbDocument setIsProcessing:NO]; // Reset the UI - [addToFavoritesButton setHidden:NO]; - [addToFavoritesButton display]; [helpButton setHidden:NO]; [helpButton display]; [connectButton setTitle:NSLocalizedString(@"Connect", @"connect button")]; [connectButton setEnabled:YES]; [connectButton display]; + [testConnectButton setEnabled:YES]; [progressIndicator stopAnimation:self]; [progressIndicator display]; [progressIndicatorText setHidden:YES]; @@ -1463,6 +1624,8 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, SPTreeNode *favoriteNode = nil; if (!favoritesRoot) return favoriteNode; + + if (!favoriteID) return quickConnectItem; for (SPTreeNode *node in [favoritesRoot allChildLeafs]) { @@ -1486,106 +1649,70 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, return [result stringByReplacingOccurrencesOfString:@"\n" withString:@""]; } -#ifndef SP_REFACTOR /** - * 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. + * Generate a name for the current connection based on any other populated details. + * Currently uses the host and database fields. + * If a name cannot be generated because there are insufficient other details, returns nil. */ -- (void)_updateFavoritePasswordsFromField:(NSControl *)control +- (NSString *)_generateNameForConnection { - if (!currentFavorite) return; - - NSDictionary *oldFavorite = currentFavorite; - 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]; - } - - // 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]]; - - // If there's no new password, remove the old item from the keychain - if (![passwordValue length]) { - [keychain deletePasswordForName:oldKeychainName account:oldKeychainAccount]; + NSString *aName; - // Otherwise, set up the new keychain name and account strings and create or edit the item - } else { - newKeychainName = [keychain nameForFavoriteName:[newFavorite objectForKey:SPFavoriteNameKey] id:[newFavorite objectForKey:SPFavoriteIDKey]]; - newKeychainAccount = [keychain accountForUser:[newFavorite objectForKey:SPFavoriteUserKey] host:newHostnameForPassword database:[newFavorite objectForKey:SPFavoriteDatabaseKey]]; - 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?passwordValue:@""]; - [socketPasswordField setStringValue:passwordValue?passwordValue:@""]; - [sshPasswordField setStringValue:passwordValue?passwordValue:@""]; - - passwordValue = @""; + if ([self type] != SPSocketConnection && ![[self host] length]) { + return nil; } - - // 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]]; - // If there's no new password, delete the keychain item - if (![[sshSSHPasswordField stringValue] length]) { - [keychain deletePasswordForName:oldKeychainName account:oldKeychainAccount]; + aName = ([self type] == SPSocketConnection) ? @"localhost" : [self host]; - // Otherwise, set up the new keychain name and account strings and create or update the keychain item - } else { - newKeychainName = [keychain nameForSSHForFavoriteName:[newFavorite objectForKey:SPFavoriteNameKey] id:[newFavorite objectForKey:SPFavoriteIDKey]]; - newKeychainAccount = [keychain accountForSSHUser:[newFavorite objectForKey:SPFavoriteSSHUserKey] sshHost:[newFavorite objectForKey:SPFavoriteSSHHostKey]]; - if ([keychain passwordExistsForName:oldKeychainName account:oldKeychainAccount]) { - [keychain updateItemWithName:oldKeychainName account:oldKeychainAccount toName:newKeychainName account:newKeychainAccount password:[sshSSHPasswordField stringValue]]; - } else { - [keychain addPassword:[sshSSHPasswordField stringValue] forName:newKeychainName account:newKeychainAccount]; - } - } + if ([[self database] length]) { + aName = [NSString stringWithFormat:@"%@/%@", aName, [self database]]; } - - // Update the current favorite - if (currentFavorite) [currentFavorite release], currentFavorite = nil; - - if ([[favoritesOutlineView selectedRowIndexes] count]) { - currentFavorite = [[[[self selectedFavoriteNode] representedObject] nodeFavorite] copy]; + + return aName; +} + + +/** + * If editing is not already active, mark editing as starting, triggering UI updates + * to match. + */ +- (void)_startEditingConnection +{ + + // If not connecting, hide the connection status text to reflect changes + if (!isConnecting) { + [progressIndicatorText setHidden:YES]; } + + if (isEditingConnection) return; + + // Fade and move the edit button area in + [editButtonsView setAlphaValue:0.0]; + [editButtonsView setHidden:NO]; + [editButtonsView setFrameOrigin:NSMakePoint([editButtonsView frame].origin.x, [editButtonsView frame].origin.y - 40)]; + [[editButtonsView animator] setFrameOrigin:NSMakePoint([editButtonsView frame].origin.x, [editButtonsView frame].origin.y + 40)]; + [[editButtonsView animator] setAlphaValue:1.0]; + + // Update the "Save" button state as appropriate + [saveFavoriteButton setEnabled:([self selectedFavorite] != nil)]; + + // Show the area to allow saving the changes + [self setIsEditingConnection:YES]; + [favoritesOutlineView setNeedsDisplayInRect:[favoritesOutlineView rectOfRow:[favoritesOutlineView selectedRow]]]; +} + +/** + * If editing is active, mark editing as complete, triggering UI updates to match. + */ +- (void)_stopEditingConnection +{ + if (!isEditingConnection) return; + + [editButtonsView setHidden:YES]; + [progressIndicatorText setHidden:YES]; + + [self setIsEditingConnection:NO]; } -#endif #pragma mark - @@ -1624,6 +1751,8 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, #ifndef SP_REFACTOR [folderImage release], folderImage = nil; + [quickConnectItem release], quickConnectItem = nil; + [quickConnectCell release], quickConnectCell = nil; #endif for (id retainedObject in nibObjectsToRelease) [retainedObject release]; diff --git a/Source/SPConnectionControllerDataSource.m b/Source/SPConnectionControllerDataSource.m index 86f6db49..d7f3d235 100644 --- a/Source/SPConnectionControllerDataSource.m +++ b/Source/SPConnectionControllerDataSource.m @@ -39,7 +39,6 @@ @interface SPConnectionController () - (void)_reloadFavoritesViewData; -- (void)_updateFavoritePasswordsFromField:(NSControl *)control; @end @@ -47,16 +46,42 @@ #ifndef SP_REFACTOR +/** + * Return the number of children for the specified item in the favourites tree. + * Note that to support the "Quick Connect" entry, the returned count is amended + * for the top level. + */ - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item { SPTreeNode *node = (item == nil ? favoritesRoot : (SPTreeNode *)item); - + + // If at the root, return the count plus one for the "Quick Connect" entry + if (!item) { + return [[node childNodes] count] + 1; + } + return [[node childNodes] count]; } +/** + * Return the branch at the specified index of a supplied tree level. + * Note that to support the "Quick Connect" entry, children of the top level + * have their offsets amended. + */ - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)childIndex ofItem:(id)item { + + // For the top level of the tree, return the "Quick Connect" child for position zero; + // amend all other positions to compensate for the faked position. + if (!item) { + if (childIndex == 0) { + return quickConnectItem; + } + childIndex--; + } + SPTreeNode *node = (item == nil ? favoritesRoot : (SPTreeNode *)item); + return NSArrayObjectAtIndex([node childNodes], childIndex); } @@ -83,11 +108,10 @@ SPTreeNode *node = [self selectedFavoriteNode]; if (![node isGroup]) { + // Updating the name triggers a KVO update - [self setName:newName]; - - // Update associated Keychain items - [self _updateFavoritePasswordsFromField:nil]; + [self setName:newName]; + [self saveFavorite:self]; } else { [[node representedObject] setNodeName:newName]; diff --git a/Source/SPConnectionControllerDelegate.m b/Source/SPConnectionControllerDelegate.m index 30b602cd..e18c43ab 100644 --- a/Source/SPConnectionControllerDelegate.m +++ b/Source/SPConnectionControllerDelegate.m @@ -43,19 +43,27 @@ #endif static NSString *SPDatabaseImage = @"database-small"; +static NSString *SPQuickConnectImage = @"quick-connect-icon.pdf"; +static NSString *SPQuickConnectImageWhite = @"quick-connect-icon-white.pdf"; @interface SPConnectionController () +// Privately redeclare as read/write to get the synthesized setter +@property (readwrite, assign) BOOL isEditingConnection; + - (void)_checkHost; - (void)_sortFavorites; - (void)_favoriteTypeDidChange; - (void)_reloadFavoritesViewData; -- (void)_updateFavoritePasswordsFromField:(NSControl *)control; - (NSString *)_stripInvalidCharactersFromString:(NSString *)subject; +- (void)_startEditingConnection; +- (void)_stopEditingConnection; - (void)_setNodeIsExpanded:(BOOL)expanded fromNotification:(NSNotification *)notification; +- (NSString *)_generateNameForConnection; + @end @implementation SPConnectionController (SPConnectionControllerDelegate) @@ -88,25 +96,27 @@ static NSString *SPDatabaseImage = @"database-small"; return ([[(SPTreeNode *)item parentNode] parentNode] == nil); } +- (void)outlineViewSelectionIsChanging:(NSNotification *)notification +{ + if (isEditingConnection) { + [self _stopEditingConnection]; + [[notification object] setNeedsDisplay:YES]; + } +} + - (void)outlineViewSelectionDidChange:(NSNotification *)notification -{ +{ NSInteger selected = [favoritesOutlineView numberOfSelectedRows]; - - if (selected == 1) { - SPTreeNode *node = [self selectedFavoriteNode]; - - [self updateFavoriteSelection:self]; + if (isEditingConnection) { + [self _stopEditingConnection]; + [[notification object] setNeedsDisplay:YES]; + } - if (![node isGroup]) { - [addToFavoritesButton setEnabled:NO]; + if (selected == 1) { + [self updateFavoriteSelection:self]; - favoriteNameFieldWasTouched = YES; - } - else { - [addToFavoritesButton setEnabled:YES]; - } - + favoriteNameFieldWasAutogenerated = NO; [connectionResizeContainer setHidden:NO]; [connectionInstructionsTextField setStringValue:NSLocalizedString(@"Enter connection details below, or choose a favorite", @"enter connection details label")]; } @@ -116,17 +126,58 @@ static NSString *SPDatabaseImage = @"database-small"; } } +- (NSCell *)outlineView:(NSOutlineView *)outlineView dataCellForTableColumn:(NSTableColumn *)tableColumn item:(id)item +{ + if (item == quickConnectItem) { + return (NSCell *)quickConnectCell; + } + + return [tableColumn dataCellForRow:[outlineView rowForItem:item]]; +} + - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item { SPTreeNode *node = (SPTreeNode *)item; - + + // Draw entries with the small system font by default [(SPTableTextFieldCell *)cell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; - [(SPTableTextFieldCell *)cell setImage:(![[node parentNode] parentNode]) ? nil : (![node isGroup]) ? [NSImage imageNamed:SPDatabaseImage] : folderImage]; + + // Set an image as appropriate; the quick connect image for that entry, no image for other + // top-level items, the folder image for group nodes, or the database image for other nodes. + if (![[node parentNode] parentNode]) { + if (node == quickConnectItem) { + if ([outlineView rowForItem:item] == [outlineView selectedRow]) { + [(SPTableTextFieldCell *)cell setImage:[NSImage imageNamed:SPQuickConnectImageWhite]]; + } else { + [(SPTableTextFieldCell *)cell setImage:[NSImage imageNamed:SPQuickConnectImage]]; + } + } else { + [(SPTableTextFieldCell *)cell setImage:nil]; + } + } else { + if ([node isGroup]) { + [(SPTableTextFieldCell *)cell setImage:folderImage]; + } else { + [(SPTableTextFieldCell *)cell setImage:[NSImage imageNamed:SPDatabaseImage]]; + } + } + + // If a favourite item is being edited, draw the text in bold to show state + if (isEditingConnection && ![node isGroup] && [outlineView rowForItem:item] == [outlineView selectedRow]) { + NSMutableAttributedString *editedCellString = [[cell attributedStringValue] mutableCopy]; + [editedCellString addAttribute:NSForegroundColorAttributeName value:[NSColor colorWithDeviceWhite:0.25f alpha:1.f] range:NSMakeRange(0, [editedCellString length])]; + [cell setAttributedStringValue:editedCellString]; + [editedCellString release]; + } } - (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item { - return ([[item parentNode] parentNode]) ? 17 : 22; + if (item == quickConnectItem) { + return 24.f; + } + + return ([[item parentNode] parentNode]) ? 17.f : 22.f; } - (NSString *)outlineView:(NSOutlineView *)outlineView toolTipForCell:(NSCell *)cell rect:(NSRectPointer)rect tableColumn:(NSTableColumn *)tableColumn item:(id)item mouseLocation:(NSPoint)mouseLocation @@ -170,8 +221,18 @@ static NSString *SPDatabaseImage = @"database-small"; } - (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item -{ - return ([[item parentNode] parentNode] != nil); +{ + + // If this is a top level item, only allow the "Quick Connect" item to be selectable + if (![[item parentNode] parentNode]) { + if (item == quickConnectItem) { + return YES; + } + return NO; + } + + // Otherwise allow all items to be selectable + return YES; } - (BOOL)outlineView:(NSOutlineView *)outlineView shouldShowOutlineCellForItem:(id)item @@ -184,6 +245,11 @@ static NSString *SPDatabaseImage = @"database-small"; return ([[item parentNode] parentNode] != nil); } +- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item +{ + return (item != quickConnectItem); +} + - (void)outlineViewItemDidCollapse:(NSNotification *)notification { [self _setNodeIsExpanded:NO fromNotification:notification]; @@ -210,12 +276,12 @@ static NSString *SPDatabaseImage = @"database-small"; } // If the user is in the process of changing a node's name, trigger a save and prevent dragging. - if (isEditing) { + if (isEditingItemName) { [favoritesController saveFavorites]; [self _reloadFavoritesViewData]; - isEditing = NO; + isEditingItemName = NO; return NO; } @@ -350,77 +416,60 @@ static NSString *SPDatabaseImage = @"database-small"; #ifndef SP_REFACTOR /** - * 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. + * React to control text changes in the connection interface */ - (void)controlTextDidChange:(NSNotification *)notification { id field = [notification object]; - + + // If a 'name' field was edited, and is now of zero length, trigger a replacement + // with a standard suggestion if (((field == standardNameField) || (field == socketNameField) || (field == sshNameField)) && [self selectedFavoriteNode]) { - - favoriteNameFieldWasTouched = YES; - - NSString *favoriteName = [self _stripInvalidCharactersFromString:[field stringValue]]; - - BOOL nameFieldIsEmpty = [favoriteName length] == 0; - - switch (previousType) - { - case SPTCPIPConnection: - if (nameFieldIsEmpty || (!favoriteNameFieldWasTouched && (field == standardUserField || field == standardSQLHostField))) { - [standardNameField setStringValue:[NSString stringWithFormat:@"%@@%@", [standardUserField stringValue], [standardSQLHostField stringValue]]]; - } - - break; - case SPSocketConnection: - if (nameFieldIsEmpty || (!favoriteNameFieldWasTouched && field == socketUserField)) { - [socketNameField setStringValue:[NSString stringWithFormat:@"%@@localhost", [socketUserField stringValue]]]; - } - - break; - case SPSSHTunnelConnection: - if (nameFieldIsEmpty || (!favoriteNameFieldWasTouched && (field == sshUserField || field == sshSQLHostField))) { - [sshNameField setStringValue:[NSString stringWithFormat:@"%@@%@", [sshUserField stringValue], [sshSQLHostField stringValue]]]; - } - - break; + if (![[self _stripInvalidCharactersFromString:[field stringValue]] length]) { + [self controlTextDidEndEditing:notification]; } - - // Trigger KVO update - [self setName:favoriteName]; - - // If name field is empty enable user@host update - if (nameFieldIsEmpty) favoriteNameFieldWasTouched = NO; + } + + [self _startEditingConnection]; + + if (favoriteNameFieldWasAutogenerated) { + [self setName:[self _generateNameForConnection]]; } } /** - * When a host field finishes editing, ensure that it hasn't been set to "localhost" - * to ensure that socket connections don't inadvertently occur. + * React to the end of control text changes in the connection interface. */ - (void)controlTextDidEndEditing:(NSNotification *)notification { - if ([notification object] == standardSQLHostField || [notification object] == sshSQLHostField) { - [self _checkHost]; + id field = [notification object]; + + // Handle updates to the 'name' field of the selected favourite. The favourite name should + // have leading or trailing spaces removed at the end of editing, and if it's left empty, + // should have a default name set. + if (((field == standardNameField) || (field == socketNameField) || (field == sshNameField)) && [self selectedFavoriteNode]) { + + NSString *favoriteName = [self _stripInvalidCharactersFromString:[field stringValue]]; + + if (![favoriteName length]) { + favoriteName = [self _generateNameForConnection]; + if (favoriteName) { + [self setName:favoriteName]; + } + + // Enable user@host update in reaction to other UI changes + favoriteNameFieldWasAutogenerated = YES; + } else if (![[field stringValue] isEqualToString:[self name]]) { + favoriteNameFieldWasAutogenerated = NO; + [self setName:favoriteName]; + } } -} -/** - * 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]; + // When a host field finishes editing, ensure that it hasn't been set to "localhost" to + // ensure that socket connections don't inadvertently occur. + if (field == standardSQLHostField || field == sshSQLHostField) { + [self _checkHost]; } - - // Proceed with editing - return YES; } #endif @@ -442,19 +491,18 @@ static NSString *SPDatabaseImage = @"database-small"; NSInteger selectedTabView = [tabView indexOfTabViewItem:tabViewItem]; 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 _startEditingConnection]; + [self _favoriteTypeDidChange]; } @@ -510,7 +558,11 @@ static NSString *SPDatabaseImage = @"database-small"; SPTreeNode *node = [self selectedFavoriteNode]; NSInteger selectedRows = [favoritesOutlineView numberOfSelectedRows]; - + + if (node == quickConnectItem) { + return NO; + } + if ((action == @selector(sortFavorites:)) || (action == @selector(reverseSortFavorites:))) { if ([[favoritesRoot allChildLeafs] count] < 2) return NO; diff --git a/Source/SPConnectionControllerInitializer.m b/Source/SPConnectionControllerInitializer.m index cee0770a..73ebcad8 100644 --- a/Source/SPConnectionControllerInitializer.m +++ b/Source/SPConnectionControllerInitializer.m @@ -33,6 +33,7 @@ #import "SPConnectionControllerInitializer.h" #import "SPKeychain.h" #import "SPFavoritesController.h" +#import "SPFavoriteTextFieldCell.h" #import "SPTreeNode.h" #import "SPFavoriteNode.h" #import "SPGroupNode.h" @@ -78,13 +79,14 @@ static NSString *SPConnectionViewNibName = @"ConnectionView"; connectionSSHKeychainItemAccount = nil; initComplete = NO; - isEditing = NO; + isEditingItemName = NO; isConnecting = NO; + isTestingConnection = NO; sshTunnel = nil; mySQLConnection = nil; cancellingConnection = NO; mySQLConnectionCancelled = NO; - favoriteNameFieldWasTouched = YES; + favoriteNameFieldWasAutogenerated = NO; [self loadNib]; [self registerForNotifications]; @@ -101,9 +103,8 @@ static NSString *SPConnectionViewNibName = @"ConnectionView"; // 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]; @@ -111,11 +112,20 @@ static NSString *SPConnectionViewNibName = @"ConnectionView"; // 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 references favoritesRoot = [favoritesController favoritesTree]; currentFavorite = nil; - + + // Create the "Quick Connect" placeholder group + quickConnectItem = [[SPTreeNode treeNodeWithRepresentedObject:[SPGroupNode groupNodeWithName:[NSLocalizedString(@"Quick Connect", @"Quick connect item label") uppercaseString]]] retain]; + [quickConnectItem setIsGroup:YES]; + + // Create a NSOutlineView cell for the Quick Connect group + quickConnectCell = [[SPFavoriteTextFieldCell alloc] init]; + [quickConnectCell setDrawsDividerUnderCell:YES]; + [quickConnectCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + // Update the UI [self _reloadFavoritesViewData]; [self setUpFavoritesOutlineView]; @@ -290,13 +300,16 @@ static NSString *SPConnectionViewNibName = @"ConnectionView"; SPTreeNode *favorite = [self _favoriteNodeForFavoriteID:[prefs integerForKey:[prefs boolForKey:SPSelectLastFavoriteUsed] ? SPLastFavoriteID : SPDefaultFavorite]]; if (favorite) { + + if (favorite == quickConnectItem) { + [self _selectNode:favorite]; + } else { + NSNumber *typeNumber = [[[favorite representedObject] nodeFavorite] objectForKey:SPFavoriteTypeKey]; + previousType = typeNumber ? [typeNumber integerValue] : SPTCPIPConnection; - 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]; + [self _selectNode:favorite]; + [self resizeTabViewToConnectionType:[[[[favorite representedObject] nodeFavorite] objectForKey:SPFavoriteTypeKey] integerValue] animating:NO]; + } [self _scrollToSelectedNode]; } diff --git a/Source/SPConnectionHandler.m b/Source/SPConnectionHandler.m index 5ae8d586..1a85bc50 100644 --- a/Source/SPConnectionHandler.m +++ b/Source/SPConnectionHandler.m @@ -45,8 +45,7 @@ static NSString *SPLocalhostAddress = @"127.0.0.1"; @interface SPConnectionController () - (void)_restoreConnectionInterface; - -- (void)_updateFavoritePasswordsFromField:(NSControl *)control; +- (void)_showConnectionTestResult:(NSString *)resultString; @end @@ -58,7 +57,17 @@ static NSString *SPLocalhostAddress = @"127.0.0.1"; - (void)initiateMySQLConnection { #ifndef SP_REFACTOR - [progressIndicatorText setStringValue:(sshTunnel) ? NSLocalizedString(@"MySQL connecting...", @"MySQL connecting very short status message") : NSLocalizedString(@"Connecting...", @"Generic connecting very short status message")]; + if (isTestingConnection) { + if (sshTunnel) { + [progressIndicatorText setStringValue:NSLocalizedString(@"Testing MySQL...", @"MySQL connection test very short status message")]; + } else { + [progressIndicatorText setStringValue:NSLocalizedString(@"Testing connection...", @"Connection test very short status message")]; + } + } else 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")]; @@ -200,7 +209,9 @@ static NSString *SPLocalhostAddress = @"127.0.0.1"; 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 rawErrorText:[mySQLConnection lastErrorMessage]]; + if (!isTestingConnection) { + [[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 rawErrorText:[mySQLConnection lastErrorMessage]]; + } // Tidy up isConnecting = NO; @@ -209,6 +220,10 @@ static NSString *SPLocalhostAddress = @"127.0.0.1"; [mySQLConnection release], mySQLConnection = nil; [self _restoreConnectionInterface]; + if (isTestingConnection) { + [self _showConnectionTestResult:NSLocalizedString(@"Invalid database", @"Invalid database very short status message")]; + } + [pool release]; return; @@ -228,7 +243,11 @@ static NSString *SPLocalhostAddress = @"127.0.0.1"; */ - (void)initiateSSHTunnelConnection { - [progressIndicatorText setStringValue:NSLocalizedString(@"SSH connecting...", @"SSH connecting very short status message")]; + if (isTestingConnection) { + [progressIndicatorText setStringValue:NSLocalizedString(@"Testing SSH...", @"SSH testing very short status message")]; + } else { + [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 @@ -266,9 +285,9 @@ static NSString *SPLocalhostAddress = @"127.0.0.1"; { isConnecting = NO; - // If the user hit cancel during the connection attempt, kill the connection once - // established and reset the UI. - if (mySQLConnectionCancelled) { + // If the user hit cancel during the connection attempt, or a test connection is + // occurring, kill the connection once established and reset the UI. + if (mySQLConnectionCancelled || isTestingConnection) { if ([mySQLConnection isConnected]) { [mySQLConnection disconnect]; [mySQLConnection release], mySQLConnection = nil; @@ -278,7 +297,11 @@ static NSString *SPLocalhostAddress = @"127.0.0.1"; [self cancelConnection]; [self _restoreConnectionInterface]; - + + if (isTestingConnection) { + [self _showConnectionTestResult:NSLocalizedString(@"Connection succeeded", @"Connection success very short status message")]; + } + return; } @@ -296,7 +319,6 @@ static NSString *SPLocalhostAddress = @"127.0.0.1"; [connectButton display]; [progressIndicator stopAnimation:self]; [progressIndicatorText setHidden:YES]; - [addToFavoritesButton setHidden:NO]; #endif // If SSL was enabled, check it was established correctly @@ -418,9 +440,8 @@ static NSString *SPLocalhostAddress = @"127.0.0.1"; [progressIndicator display]; [progressIndicatorText setHidden:YES]; [progressIndicatorText display]; - [addToFavoritesButton setHidden:NO]; - [addToFavoritesButton display]; [connectButton setEnabled:YES]; + [testConnectButton setEnabled:YES]; [dbDocument clearStatusIcon]; #endif @@ -483,7 +504,6 @@ static NSString *SPLocalhostAddress = @"127.0.0.1"; // Change connection details [self setPort:tunnelPort]; [self setHost:SPLocalhostAddress]; - [self _updateFavoritePasswordsFromField:standardSQLHostField]; #ifndef SP_REFACTOR // Change to standard TCP/IP connection view @@ -495,4 +515,19 @@ static NSString *SPLocalhostAddress = @"127.0.0.1"; } } +/** + * Display a connection test error or success message + */ +- (void)_showConnectionTestResult:(NSString *)resultString +{ + if (![NSThread isMainThread]) { + [[self onMainThread] _showConnectionTestResult:resultString]; + } + + [helpButton setHidden:NO]; + [progressIndicator stopAnimation:self]; + [progressIndicatorText setStringValue:resultString]; + [progressIndicatorText setHidden:NO]; +} + @end diff --git a/Source/SPFavoriteTextFieldCell.h b/Source/SPFavoriteTextFieldCell.h index c006c9e7..84bee6ba 100644 --- a/Source/SPFavoriteTextFieldCell.h +++ b/Source/SPFavoriteTextFieldCell.h @@ -34,20 +34,10 @@ @interface SPFavoriteTextFieldCell : ImageAndTextCell { - NSString *favoriteName; - NSString *favoriteHost; - - NSColor *mainStringColor; - NSColor *subStringColor; + BOOL drawsDividerUnderCell; } -- (NSString *)favoriteName; -- (void)setFavoriteName:(NSString *)name; - -- (NSString *)favoriteHost; -- (void)setFavoriteHost:(NSString *)host; - -- (void)invertFontColors; -- (void)restoreFontColors; +- (BOOL)drawsDividerUnderCell; +- (void)setDrawsDividerUnderCell:(BOOL)drawsDivider; @end diff --git a/Source/SPFavoriteTextFieldCell.m b/Source/SPFavoriteTextFieldCell.m index e084d173..f9ebd280 100644 --- a/Source/SPFavoriteTextFieldCell.m +++ b/Source/SPFavoriteTextFieldCell.m @@ -32,17 +32,6 @@ #import "SPFavoriteTextFieldCell.h" -#define FAVORITE_NAME_FONT_SIZE 12.0f - -@interface SPFavoriteTextFieldCell (PrivateAPI) - -- (NSAttributedString *)constructSubStringAttributedString; -- (NSAttributedString *)attributedStringForFavoriteName; -- (NSDictionary *)mainStringAttributedStringAttributes; -- (NSDictionary *)subStringAttributedStringAttributes; - -@end - @implementation SPFavoriteTextFieldCell /** @@ -51,10 +40,7 @@ - (id)init { if ((self = [super init])) { - mainStringColor = [NSColor blackColor]; - subStringColor = [NSColor grayColor]; - favoriteName = nil; - favoriteHost = nil; + drawsDividerUnderCell = NO; } return self; @@ -63,186 +49,64 @@ - (id)copyWithZone:(NSZone *)zone { SPFavoriteTextFieldCell *cell = (SPFavoriteTextFieldCell *)[super copyWithZone:zone]; - - cell->favoriteName = nil; - if (favoriteName) cell->favoriteName = [favoriteName copyWithZone:zone]; - cell->favoriteHost = nil; - if (favoriteHost) cell->favoriteHost = [favoriteHost copyWithZone:zone]; + cell->drawsDividerUnderCell = drawsDividerUnderCell; return cell; } /** - * Get the cell's favorite name. - */ -- (NSString *)favoriteName -{ - return favoriteName; -} - -/** - * Set the cell's favorite name to the supplied name. + * Returns whether this cell is set to draw a divider in the space directly below + * the cell (whatever currently populates that space). */ -- (void)setFavoriteName:(NSString *)name +- (BOOL)drawsDividerUnderCell { - if (favoriteName != name) { - [favoriteName release]; - favoriteName = [name retain]; - } + return drawsDividerUnderCell; } /** - * Get the cell's favorite host. + * Set whether this cell should draw a divider in the space directly below + * the cell (whatever currently populates that space). */ -- (NSString *)favoriteHost +- (void)setDrawsDividerUnderCell:(BOOL)drawsDivider { - return favoriteHost; + drawsDividerUnderCell = drawsDivider; } -/** - * Set the cell's favorite host to the supplied name. - */ -- (void)setFavoriteHost:(NSString *)host -{ - if (favoriteHost != host) { - [favoriteHost release]; - favoriteHost = [host retain]; - } -} +#pragma mark - /** - * Draws the actual cell. + * Draws the actual cell, with a divider if appropriate. */ - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { - (([self isHighlighted]) && (![[self highlightColorWithFrame:cellFrame inView:controlView] isEqualTo:[NSColor secondarySelectedControlColor]])) ? [self invertFontColors] : [self restoreFontColors]; - - // Construct and get the sub text attributed string - NSAttributedString *mainString = [self attributedStringForFavoriteName]; - NSAttributedString *subString = [self constructSubStringAttributedString]; - - NSRect subFrame = NSMakeRect(0.0f, 0.0f, [subString size].width, [subString size].height); - - // Total height of both strings with a 2 pixel separation space - CGFloat totalHeight = [mainString size].height + [subString size].height + 1.0f; - - cellFrame.origin.y += (cellFrame.size.height - totalHeight) / 2.0f; - cellFrame.origin.x += 10.0f; // Indent main string from image - - // Position the sub text's frame rect - subFrame.origin.y = [mainString size].height + cellFrame.origin.y + 1.0f; - subFrame.origin.x = cellFrame.origin.x; - - cellFrame.size.height = totalHeight; - - NSUInteger i; - CGFloat maxWidth = cellFrame.size.width; - CGFloat mainStringWidth = [mainString size].width; - CGFloat subStringWidth = [subString size].width; - - // Set a right-padding - maxWidth -= 10; - - if (maxWidth < mainStringWidth) { - for (i = 0; i <= [mainString length]; i++) { - if ([[mainString attributedSubstringFromRange:NSMakeRange(0, i)] size].width >= maxWidth && i >= 3) { - mainString = [[[NSMutableAttributedString alloc] initWithString:[[[mainString attributedSubstringFromRange:NSMakeRange(0, i - 3)] string] stringByAppendingString:@"..."] attributes:[self mainStringAttributedStringAttributes]] autorelease]; - } - } - } - - if (maxWidth < subStringWidth) { - for (i = 0; i <= [subString length]; i++) { - if ([[subString attributedSubstringFromRange:NSMakeRange(0, i)] size].width >= maxWidth && i >= 3) { - subString = [[[NSMutableAttributedString alloc] initWithString:[[[subString attributedSubstringFromRange:NSMakeRange(0, i - 3)] string] stringByAppendingString:@"..."] attributes:[self subStringAttributedStringAttributes]] autorelease]; - } - } - } - - [mainString drawInRect:cellFrame]; - [subString drawInRect:subFrame]; -} + [super drawInteriorWithFrame:cellFrame inView:controlView]; -- (NSSize)cellSize -{ - NSSize cellSize = [super cellSize]; - NSAttributedString *mainString = [self attributedStringForFavoriteName]; - NSAttributedString *subString = [self constructSubStringAttributedString]; - - // 15 := indention 10 from image to string plus 5 px padding - CGFloat theWidth = MAX([mainString size].width, [subString size].width) + (([self image] != nil) ? [[self image] size].width : 0) + 15; + if (drawsDividerUnderCell) { + NSRect viewFrame = [controlView frame]; - CGFloat totalHeight = [mainString size].height + [subString size].height + 1.0f; - - cellSize.width = theWidth; - cellSize.height = totalHeight + 13.0f; - return cellSize; -} + NSPoint startPoint = NSMakePoint(viewFrame.origin.x + 7.f, viewFrame.origin.y); + NSPoint endPoint = NSMakePoint(viewFrame.origin.x + viewFrame.size.width - 7.f, viewFrame.origin.y); -/** - * Inverts the displayed font colors when the cell is selected. - */ -- (void)invertFontColors -{ - mainStringColor = [NSColor whiteColor]; - subStringColor = [NSColor whiteColor]; -} - -/** - * Restores the displayed font colors once the cell is no longer selected. - */ -- (void)restoreFontColors -{ - mainStringColor = [NSColor blackColor]; - subStringColor = [NSColor grayColor]; -} - -/** - * Dealloc. - */ -- (void)dealloc -{ - [favoriteName release], favoriteName = nil; - [favoriteHost release], favoriteHost = nil; - - [super dealloc]; -} - -@end - -@implementation SPFavoriteTextFieldCell (PrivateAPI) - -/** - * Constructs the attributed string to be used as the cell's substring. - */ -- (NSAttributedString *)constructSubStringAttributedString -{ - return [[[NSAttributedString alloc] initWithString:favoriteHost attributes:[self subStringAttributedStringAttributes]] autorelease]; -} - -/** - * Constructs the attributed string for the cell's favorite name. - */ -- (NSAttributedString *)attributedStringForFavoriteName -{ - return [[[NSAttributedString alloc] initWithString:favoriteName attributes:[self mainStringAttributedStringAttributes]] autorelease]; -} - -/** - * Returns the attributes of the cell's main string. - */ -- (NSDictionary *)mainStringAttributedStringAttributes -{ - return [NSDictionary dictionaryWithObjectsAndKeys:mainStringColor, NSForegroundColorAttributeName, [NSFont systemFontOfSize:FAVORITE_NAME_FONT_SIZE], NSFontAttributeName, nil]; -} + if ([controlView isFlipped]) { + startPoint.y += cellFrame.size.height + 8.5f; + endPoint.y += cellFrame.size.height + 8.5f; + } else { + startPoint.y -= cellFrame.size.height + 8.5f; + endPoint.y -= cellFrame.size.height + 8.5f; + } -/** - * Returns the attributes of the cell's sub string. - */ -- (NSDictionary *)subStringAttributedStringAttributes -{ - return [NSDictionary dictionaryWithObjectsAndKeys:subStringColor, NSForegroundColorAttributeName, [NSFont systemFontOfSize:[NSFont smallSystemFontSize]], NSFontAttributeName, nil]; + [NSGraphicsContext saveGraphicsState]; + [[NSColor gridColor] set]; + NSShadow *lineGlow = [[NSShadow alloc] init]; + [lineGlow setShadowBlurRadius:1]; + [lineGlow setShadowColor:[[NSColor controlLightHighlightColor] colorWithAlphaComponent:0.75f]]; + [lineGlow setShadowOffset:NSMakeSize(0, -1)]; + [lineGlow set]; + [NSBezierPath strokeLineFromPoint:startPoint toPoint:endPoint]; + [lineGlow release]; + [NSGraphicsContext restoreGraphicsState]; + } } -@end +@end
\ No newline at end of file diff --git a/Source/SPFavoritesOutlineView.m b/Source/SPFavoritesOutlineView.m index 883896ef..08880ec8 100644 --- a/Source/SPFavoritesOutlineView.m +++ b/Source/SPFavoritesOutlineView.m @@ -31,6 +31,9 @@ // More info at <http://code.google.com/p/sequel-pro/> #import "SPFavoritesOutlineView.h" +#import "SPConnectionControllerDelegate.h" + +static NSUInteger SPFavoritesOutlineViewUnindent = 14; @implementation SPFavoritesOutlineView @@ -88,4 +91,64 @@ } } +/** + * Don't reserve a gap for the disclosure triangles for top-level items. This involves reducing the + * padding - and increasing the width - of all rows to compensate. + */ +- (NSRect)frameOfCellAtColumn:(NSInteger)columnIndex row:(NSInteger)rowIndex +{ + NSRect superFrame = [super frameOfCellAtColumn:columnIndex row:rowIndex]; + + return NSMakeRect(superFrame.origin.x - SPFavoritesOutlineViewUnindent, superFrame.origin.y, superFrame.size.width + SPFavoritesOutlineViewUnindent, superFrame.size.height); +} + +/** + * As no gap is reserved for the disclosure triangles at the top level, the frames for other + * disclosure items need to be similarly moved. + */ +- (NSRect)frameOfOutlineCellAtRow:(NSInteger)rowIndex +{ + NSRect superFrame = [super frameOfOutlineCellAtRow:rowIndex]; + + if (superFrame.origin.x > SPFavoritesOutlineViewUnindent) { + return NSMakeRect(superFrame.origin.x - SPFavoritesOutlineViewUnindent, superFrame.origin.y, superFrame.size.width, superFrame.size.height); + } + + return superFrame; +} + + +/** + * If the delegate is a SPConnectionControllerDelegate, and editing is currently in + * progress, draw a custom highlight. + */ +- (void)highlightSelectionInClipRect:(NSRect)clipRect +{ + + // Only proceed if a the delegate is a SPConnectionControllerDelegate and a favoruite being edited + if ([[self delegate] isKindOfClass:[SPConnectionController class]] + && [(SPConnectionController *)[self delegate] isEditingConnection] + && [(SPConnectionController *)[self delegate] selectedFavorite]) + { + + // Draw an editing dot instead of highlighting the whole row + NSRect rowRect = [self rectOfRow:[self selectedRow]]; + float dotSize = rowRect.size.height / 1.9; + NSRect dotRect = NSMakeRect(9.f, rowRect.origin.y + ((rowRect.size.height - dotSize) / 2), dotSize, dotSize); + [NSGraphicsContext saveGraphicsState]; + + NSBezierPath *clipPath = [NSBezierPath bezierPath]; + [clipPath appendBezierPathWithOvalInRect:dotRect]; + [clipPath addClip]; + + NSGradient *dotGradient = [[[NSGradient alloc] initWithStartingColor:[NSColor colorWithDeviceRed:0.44f green:0.72f blue:0.92f alpha:1.f] endingColor:[NSColor colorWithDeviceRed:0.21f green:0.53f blue:0.82f alpha:1.f]] autorelease]; + [dotGradient drawInRect:dotRect angle:90.f]; + + [NSGraphicsContext restoreGraphicsState]; + return; + } + + [super highlightSelectionInClipRect:clipRect]; +} + @end |