// // $Id$ // // SPConnectionControllerDelegate.m // sequel-pro // // Created by Stuart Connolly (stuconnolly.com) on November 9, 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 #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" #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; - (void)_setNodeIsExpanded:(BOOL)expanded fromNotification:(NSNotification *)notification; @end @implementation SPConnectionController (SPConnectionControllerDelegate) #pragma mark - #pragma mark SplitView delegate methods - (NSRect)splitView:(NSSplitView *)splitView additionalEffectiveRectOfDividerAtIndex:(NSInteger)dividerIndex { return [connectionSplitViewButtonBar splitView:splitView additionalEffectiveRectOfDividerAtIndex:dividerIndex]; } /** * 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 sync. */ - (void)splitViewDidResizeSubviews:(NSNotification *)notification { [databaseConnectionView setPosition:[[[connectionSplitView subviews] objectAtIndex:0] frame].size.width ofDividerAtIndex:0]; } #pragma mark - #pragma mark Outline view delegate methods - (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item { return ([[(SPTreeNode *)item parentNode] parentNode] == nil); } - (void)outlineViewSelectionDidChange:(NSNotification *)notification { NSInteger selected = [favoritesOutlineView numberOfSelectedRows]; 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")]; } } - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item { SPTreeNode *node = (SPTreeNode *)item; [CELL(cell) setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; [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]; } } - (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item { return ([[item parentNode] parentNode]) ? 17 : 22; } - (NSString *)outlineView:(NSOutlineView *)outlineView toolTipForCell:(NSCell *)cell rect:(NSRectPointer)rect tableColumn:(NSTableColumn *)tableColumn item:(id)item mouseLocation:(NSPoint)mouseLocation { 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 descendant of the root node else if ([[node parentNode] parentNode]) { NSUInteger favCount = 0; NSUInteger groupCount = 0; for (SPTreeNode *eachNode in [node childNodes]) { if ([eachNode isGroup]) { groupCount++; } else { favCount++; } } NSMutableArray *tooltipParts = [NSMutableArray arrayWithCapacity:2]; if (favCount || !groupCount) { [tooltipParts addObject:[NSString stringWithFormat:@"%d %@", favCount, (favCount == 1) ? NSLocalizedString(@"favorite", @"favorite singular label") : NSLocalizedString(@"favorites", @"favorites plural label")]]; } if (groupCount) { [tooltipParts addObject:[NSString stringWithFormat:@"%d %@", groupCount, (groupCount == 1) ? NSLocalizedString(@"group", @"favorite group singular label") : NSLocalizedString(@"groups", @"favorite groups plural label")]]; } toolTip = [NSString stringWithFormat:@"%@ - %@", [[node representedObject] nodeName], [tooltipParts componentsJoinedByString:@", "]]; } return toolTip; } - (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item { return ([[item parentNode] parentNode] != nil); } - (BOOL)outlineView:(NSOutlineView *)outlineView shouldShowOutlineCellForItem:(id)item { return ([[item parentNode] parentNode] != nil); } - (BOOL)outlineView:(NSOutlineView *)outlineView shouldCollapseItem:(id)item { return ([[item parentNode] parentNode] != nil); } - (void)outlineViewItemDidCollapse:(NSNotification *)notification { [self _setNodeIsExpanded:NO fromNotification:notification]; } - (void)outlineViewItemDidExpand:(NSNotification *)notification { [self _setNodeIsExpanded:YES fromNotification:notification]; } #pragma mark - #pragma mark Outline view drag & drop - (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard { // Prevent a drag which includes the outline title group from taking place for (id item in items) { if (![[item parentNode] parentNode]) return NO; } // 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 )info proposedItem:(id)item proposedChildIndex:(NSInteger)childIndex { NSDragOperation result = NSDragOperationNone; // Prevent dropping favorites on other favorites (non-groups) if ((childIndex == NSOutlineViewDropOnItemIndex) && (![item isGroup])) return result; // Ensure that none of the dragged nodes are being dragged into children of themselves; if they are, // prevent the drag. id itemToCheck = item; do { if ([draggedNodes containsObject:itemToCheck]) { return result; } } while ((itemToCheck = [itemToCheck parentNode])); if ([info draggingSource] == outlineView) { [outlineView setDropItem:item dropChildIndex:childIndex]; result = NSDragOperationMove; } return result; } - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id )info item:(id)item childIndex:(NSInteger)childIndex { BOOL acceptedDrop = NO; if ((!item) || ([info draggingSource] != outlineView)) return acceptedDrop; SPTreeNode *node = item ? item : [[[[favoritesRoot childNodes] objectAtIndex:0] childNodes] objectAtIndex:0]; // Cache the selected nodes for selection restoration afterwards NSArray *preDragSelection = [self selectedFavoriteNodes]; // 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]; } if (![draggedNodes count]) return acceptedDrop; if ([node isGroup]) { if (childIndex == NSOutlineViewDropOnItemIndex) { childIndex = 0; } } else { if (childIndex == NSOutlineViewDropOnItemIndex) { childIndex = 0; } } if (![[node representedObject] nodeName]) { node = [[favoritesRoot childNodes] objectAtIndex:0]; } NSMutableArray *childNodeArray = [node mutableChildNodes]; for (SPTreeNode *treeNode in draggedNodes) { // Remove the node from its old location NSInteger oldIndex = [childNodeArray indexOfObject:treeNode]; NSInteger newIndex = childIndex; if (oldIndex != NSNotFound) { [childNodeArray removeObjectAtIndex:oldIndex]; if (childIndex > oldIndex) { newIndex--; } } else { [[[treeNode parentNode] mutableChildNodes] removeObject:treeNode]; } [childNodeArray insertObject:treeNode atIndex:newIndex]; newIndex++; } [favoritesController saveFavorites]; [self _reloadFavoritesViewData]; [[[[NSApp delegate] preferenceController] generalPreferencePane] updateDefaultFavoritePopup]; // Update the selection to account for rearranged faourites NSMutableIndexSet *restoredSelection = [NSMutableIndexSet indexSet]; for (SPTreeNode *eachNode in preDragSelection) { [restoredSelection addIndex:[favoritesOutlineView rowForItem:eachNode]]; } [favoritesOutlineView selectRowIndexes:restoredSelection byExtendingSelection:NO]; acceptedDrop = YES; return acceptedDrop; } #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 { id field = [notification object]; 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; } } } /** * 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]; } // Proceed with editing return YES; } #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 { 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]; } } #pragma mark - #pragma mark Menu Validation /** * Menu item validation. */ - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { 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(renameNode:)) { 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]; } } /** * Called by the favorites importer when the imported data is available. */ - (void)favoritesImportData:(NSArray *)data { // 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]; } } #pragma mark - #pragma mark Private API - (void)_setNodeIsExpanded:(BOOL)expanded fromNotification:(NSNotification *)notification { SPGroupNode *node = [[[notification userInfo] valueForKey:@"NSObject"] representedObject]; [node setNodeIsExpanded:expanded]; } @end