From 0e4ad8eb9cddbbd755d55bb50f7707f1e8160121 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 12 Oct 2014 22:41:36 +0200 Subject: Add a "Go to Database" dialog The dialog enables * searching for a database by name (substring matching), * using C&P to select databases * navigating to databases not in the database dropdown * faster keyboard-based navigation --- Source/SPDatabaseDocument.h | 3 + Source/SPDatabaseDocument.m | 19 ++++ Source/SPGotoDatabaseController.h | 86 +++++++++++++++ Source/SPGotoDatabaseController.m | 223 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 331 insertions(+) create mode 100644 Source/SPGotoDatabaseController.h create mode 100644 Source/SPGotoDatabaseController.m (limited to 'Source') diff --git a/Source/SPDatabaseDocument.h b/Source/SPDatabaseDocument.h index 218f3bc7..a67619e8 100644 --- a/Source/SPDatabaseDocument.h +++ b/Source/SPDatabaseDocument.h @@ -55,6 +55,7 @@ @class SPDatabaseStructure; @class SPMySQLConnection; @class SPCharsetCollationHelper; +@class SPGotoDatabaseController; #import "SPDatabaseContentViewDelegate.h" #import "SPConnectionControllerDelegateProtocol.h" @@ -280,6 +281,7 @@ BOOL windowTitleStatusViewIsVisible; #endif SPDatabaseStructure *databaseStructureRetrieval; + SPGotoDatabaseController *gotoDatabaseController; } #ifdef SP_CODA /* ivars */ @@ -352,6 +354,7 @@ - (IBAction)showServerVariables:(id)sender; - (IBAction)showServerProcesses:(id)sender; - (IBAction)openCurrentConnectionInNewWindow:(id)sender; +- (IBAction)showGotoDatabase:(id)sender; #endif - (NSArray *)allDatabaseNames; - (NSArray *)allSystemDatabaseNames; diff --git a/Source/SPDatabaseDocument.m b/Source/SPDatabaseDocument.m index 330716c3..9a736f90 100644 --- a/Source/SPDatabaseDocument.m +++ b/Source/SPDatabaseDocument.m @@ -109,6 +109,7 @@ enum { #endif #import "SPCharsetCollationHelper.h" +#import "SPGotoDatabaseController.h" #import @@ -206,6 +207,7 @@ static NSString *SPAlterDatabaseAction = @"SPAlterDatabase"; mySQLVersion = nil; allDatabases = nil; allSystemDatabases = nil; + gotoDatabaseController = nil; #ifndef SP_CODA /* init ivars */ mainToolbar = nil; @@ -1131,6 +1133,22 @@ static NSString *SPAlterDatabaseAction = @"SPAlterDatabase"; return [[SPNavigatorController sharedNavigatorController] allSchemaKeysForConnection:[self connectionID]]; } +- (IBAction)showGotoDatabase:(id)sender +{ + if(!gotoDatabaseController) { + gotoDatabaseController = [[SPGotoDatabaseController alloc] init]; + } + + NSMutableArray *dbList = [[NSMutableArray alloc] init]; + [dbList addObjectsFromArray:[self allSystemDatabaseNames]]; + [dbList addObjectsFromArray:[self allDatabaseNames]]; + [gotoDatabaseController setDatabaseList:[dbList autorelease]]; + + if([gotoDatabaseController runModal]) { + [self selectDatabase:[gotoDatabaseController selectedDatabase] item:nil]; + } +} + #ifndef SP_CODA /* console and navigator methods */ #pragma mark - @@ -6269,6 +6287,7 @@ static NSString *SPAlterDatabaseAction = @"SPAlterDatabase"; [allDatabases release]; [allSystemDatabases release]; + [gotoDatabaseController release]; #ifndef SP_CODA /* dealloc ivars */ [undoManager release]; [printWebView release]; diff --git a/Source/SPGotoDatabaseController.h b/Source/SPGotoDatabaseController.h new file mode 100644 index 00000000..99dfc289 --- /dev/null +++ b/Source/SPGotoDatabaseController.h @@ -0,0 +1,86 @@ +// +// GotoDatbaseController.h +// sequel-pro +// +// Created by Max Lohrmann on 12.10.14. +// Copyright (c) 2014 Max Lohrmann. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at + +#import +@class SPDatabaseDocument; + +/** + * This class provides a dialog with a single-column table view and a + * search field. It can be used for finding databases by name and/or faster, + * keyboard-based navigation between databases. The dialog also enables + * jumping to a database by C&P-ing its full name. + */ +@interface SPGotoDatabaseController : NSWindowController { + IBOutlet NSSearchField *searchField; + IBOutlet NSButton *okButton; + IBOutlet NSButton *cancelButton; + IBOutlet NSTableView *databaseListView; + + NSMutableArray *unfilteredList; + NSMutableArray *filteredList; + BOOL isFiltered; +} + +/** + * Specifies whether custom names (i.e. names that were not in the list supplied + * by setDatabaseList:) will be allowed. This is useful if it has to be assumed + * that the list of databases is not exhaustive (eg. databases added after fetching + * the database list). + */ +@property BOOL allowCustomNames; + +/** + * Set the list of databases the user can pick from. + * @param list An array of NSStrings + * + * This method must be called before runModal. The list will not be updated + * when the dialog is on screen. + */ +- (void)setDatabaseList:(NSArray *)list; + +/** + * Retrieve the user selection. + * @return The selected database or nil, if there is no selection + * + * This method retrieves the database selected by the user. Note that this is + * not neccesarily one of the objects which were passed in, if allowCustomNames + * is enabled. The return value of this function is undefined after calling + * setDatabaseList:! + */ +- (NSString *)selectedDatabase; + +/** + * Starts displaying the dialog as application modal. + * @return YES if the user pressed "OK", NO otherwise + * + * This method will only return once the dialog was closed again. + */ +- (BOOL)runModal; +@end diff --git a/Source/SPGotoDatabaseController.m b/Source/SPGotoDatabaseController.m new file mode 100644 index 00000000..0c6cbef0 --- /dev/null +++ b/Source/SPGotoDatabaseController.m @@ -0,0 +1,223 @@ +// +// GotoDatbaseController.m +// sequel-pro +// +// Created by Max Lohrmann on 12.10.14. +// Copyright (c) 2014 Max Lohrmann. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at + +#import "SPGotoDatabaseController.h" +#import "SPDatabaseDocument.h" + +@interface SPGotoDatabaseController (Private) +- (void)_buildHightlightedFilterList:(NSString *)filter didFindExactMatch:(BOOL *)exactMatch; + +- (IBAction)okClicked:(id)sender; +- (IBAction)cancelClicked:(id)sender; +- (IBAction)searchChanged:(id)sender; +@end + +@implementation SPGotoDatabaseController + +- (id)init +{ + self = [super initWithWindowNibName:@"GotoDatabaseDialog"]; + if (self) { + unfilteredList = [[NSMutableArray alloc] init]; + filteredList = [[NSMutableArray alloc] init]; + isFiltered = NO; + [self setAllowCustomNames:YES]; + } + return self; +} + +#pragma mark - +#pragma mark IBAction + +- (IBAction)okClicked:(id)sender +{ + [NSApp stopModalWithCode:YES]; + [[self window] orderOut:nil]; +} + +- (IBAction)cancelClicked:(id)sender +{ + [NSApp stopModalWithCode:NO]; + [[self window] orderOut:nil]; +} + +- (IBAction)searchChanged:(id)sender +{ + [filteredList removeAllObjects]; + NSString *newFilter = [searchField stringValue]; + if(!newFilter || [newFilter isEqualToString:@""]) { + isFiltered = NO; + } + else { + isFiltered = YES; + BOOL exactMatch = NO; + [self _buildHightlightedFilterList:newFilter didFindExactMatch:&exactMatch]; + //always add the search string to the end of the list (in case the user + //wants to switch to a DB not in the list) unless there was an exact match + if([self allowCustomNames] && !exactMatch) { + NSMutableAttributedString *searchValue = [[NSMutableAttributedString alloc] initWithString:newFilter]; + [searchValue applyFontTraits:NSItalicFontMask range:NSMakeRange(0, [newFilter length])]; + [filteredList addObject:[searchValue autorelease]]; + } + } + [databaseListView reloadData]; + //ensure we have a selection + if([databaseListView selectedRow] < 0) + [databaseListView selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO]; + + [okButton setEnabled:([databaseListView selectedRow] >= 0)]; +} + +#pragma mark - +#pragma mark Public + +- (NSString *)selectedDatabase { + NSInteger row = [databaseListView selectedRow]; + id attrValue; + if(isFiltered) { + attrValue = [filteredList objectOrNilAtIndex:row]; + } + else { + attrValue = [unfilteredList objectOrNilAtIndex:row]; + } + if([attrValue isKindOfClass:[NSAttributedString class]]) + return [attrValue string]; + return attrValue; +} + +- (void)setDatabaseList:(NSArray *)list +{ + //update list of databases + [unfilteredList removeAllObjects]; + [unfilteredList addObjectsFromArray:list]; +} + +- (BOOL)runModal +{ + //NSWindowController is lazy with loading nibs + [self window]; + + //reset the search field + [searchField setStringValue:@""]; + [self searchChanged:nil]; + //give focus to search field + [[self window] makeFirstResponder:searchField]; + //start modal dialog + return [NSApp runModalForWindow:[self window]]; +} + +#pragma mark - +#pragma mark Private + +- (void)_buildHightlightedFilterList:(NSString *)filter didFindExactMatch:(BOOL *)exactMatch +{ + NSDictionary *attrs = [[NSDictionary alloc] initWithObjectsAndKeys: + [NSColor colorWithCalibratedRed:249/255.0 green:247/255.0 blue:62/255.0 alpha:0.5],NSBackgroundColorAttributeName, + [NSColor colorWithCalibratedRed:180/255.0 green:164/255.0 blue:31/255.0 alpha:1.0],NSUnderlineColorAttributeName, + [NSNumber numberWithInt:NSUnderlineStyleSingle],NSUnderlineStyleAttributeName, + nil]; + + for(NSString *db in unfilteredList) { + NSRange match = [db rangeOfString:filter]; + if(match.location == NSNotFound) + continue; + //check for exact match? + if(exactMatch && !*exactMatch) { + if(match.location == 0 && match.length == [db length]) + *exactMatch = YES; + } + + NSMutableAttributedString *attrMatch = [[NSMutableAttributedString alloc] initWithString:db]; + [attrMatch setAttributes:attrs range:match]; + [filteredList addObject:[attrMatch autorelease]]; + } + + [attrs release]; +} + +#pragma mark - +#pragma mark NSTableViewDataSource + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView +{ + if(!isFiltered) { + return [unfilteredList count]; + } + else { + return [filteredList count]; + } +} + +- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex +{ + if(!isFiltered) + return [unfilteredList objectAtIndex:rowIndex]; + else + return [filteredList objectAtIndex:rowIndex]; +} + +#pragma mark - +#pragma mark NSControlTextEditingDelegate + +- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector +{ + //the ESC key will usually clear the search field. we want to close the dialog + if(commandSelector == @selector(cancelOperation:)) { + [cancelButton performClick:control]; + return YES; + } + //arrow down/up will usually go to start/end of the text field. we want to change the selected table row. + if(commandSelector == @selector(moveDown:)) { + [databaseListView selectRowIndexes:[NSIndexSet indexSetWithIndex:([databaseListView selectedRow]+1)] byExtendingSelection:NO]; + return YES; + } + if(commandSelector == @selector(moveUp:)) { + [databaseListView selectRowIndexes:[NSIndexSet indexSetWithIndex:([databaseListView selectedRow]-1)] byExtendingSelection:NO]; + return YES; + } + //forward return to OK button (enter will not be caught by search field) + if(commandSelector == @selector(insertNewline:)) { + [okButton performClick:control]; + return YES; + } + + return NO; +} + +#pragma mark - + +- (void)dealloc +{ + [unfilteredList release], unfilteredList = nil; + [filteredList release], filteredList = nil; + [super dealloc]; +} + +@end -- cgit v1.2.3