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 --- Interfaces/English.lproj/GotoDatabaseDialog.xib | 117 +++++++++++++ Interfaces/English.lproj/MainMenu.xib | 105 +++++------ Source/SPDatabaseDocument.h | 3 + Source/SPDatabaseDocument.m | 19 ++ Source/SPGotoDatabaseController.h | 86 +++++++++ Source/SPGotoDatabaseController.m | 223 ++++++++++++++++++++++++ sequel-pro.xcodeproj/project.pbxproj | 10 ++ 7 files changed, 504 insertions(+), 59 deletions(-) create mode 100644 Interfaces/English.lproj/GotoDatabaseDialog.xib create mode 100644 Source/SPGotoDatabaseController.h create mode 100644 Source/SPGotoDatabaseController.m diff --git a/Interfaces/English.lproj/GotoDatabaseDialog.xib b/Interfaces/English.lproj/GotoDatabaseDialog.xib new file mode 100644 index 00000000..310b74b0 --- /dev/null +++ b/Interfaces/English.lproj/GotoDatabaseDialog.xib @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Interfaces/English.lproj/MainMenu.xib b/Interfaces/English.lproj/MainMenu.xib index f8d24d80..a8639839 100644 --- a/Interfaces/English.lproj/MainMenu.xib +++ b/Interfaces/English.lproj/MainMenu.xib @@ -1227,6 +1227,25 @@ Database + + + Go to Database… + d + 1048576 + 2147483647 + + + + + + YES + YES + + + 2147483647 + + + Add Database... @@ -3150,6 +3169,14 @@ BUY-hF-nKy + + + showGotoDatabase: + + + + jS0-aP-Y7o + delegate @@ -4145,6 +4172,8 @@ + + @@ -4733,6 +4762,16 @@ + + 8MG-hk-1qS + + + + + Yzp-Qk-v7Y + + + @@ -5266,6 +5305,7 @@ com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -5362,6 +5402,7 @@ com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -5371,47 +5412,6 @@ - - NSDocument - - id - id - id - id - id - id - - - - printDocument: - id - - - revertDocumentToSaved: - id - - - runPageLayout: - id - - - saveDocument: - id - - - saveDocumentAs: - id - - - saveDocumentTo: - id - - - - IBProjectSource - ./Classes/NSDocument.h - - NSTextView @@ -6877,6 +6877,7 @@ id id id + id id id id @@ -7044,6 +7045,10 @@ showFilterTable: id + + showGotoDatabase: + id + showMySQLHelp: id @@ -9728,24 +9733,6 @@ ./Classes/SUUpdater.h - - WebView - - reloadFromOrigin: - id - - - reloadFromOrigin: - - reloadFromOrigin: - id - - - - IBProjectSource - ./Classes/WebView.h - - 0 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 diff --git a/sequel-pro.xcodeproj/project.pbxproj b/sequel-pro.xcodeproj/project.pbxproj index 073ad76f..0014bc2f 100644 --- a/sequel-pro.xcodeproj/project.pbxproj +++ b/sequel-pro.xcodeproj/project.pbxproj @@ -179,6 +179,8 @@ 4DECC48F0EC2B436008D359E /* Sparkle.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 4DECC3320EC2A170008D359E /* Sparkle.framework */; }; 4DECC4910EC2B436008D359E /* Growl.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 4DECC3340EC2A170008D359E /* Growl.framework */; }; 501B1D181728A3DA0017C92E /* SPCharsetCollationHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 501B1D171728A3DA0017C92E /* SPCharsetCollationHelper.m */; }; + 50A9F8AD19EAD4860053E571 /* GotoDatabaseDialog.xib in Resources */ = {isa = PBXBuildFile; fileRef = 50A9F8AC19EAD4860053E571 /* GotoDatabaseDialog.xib */; }; + 50A9F8B119EAD4B90053E571 /* SPGotoDatabaseController.m in Sources */ = {isa = PBXBuildFile; fileRef = 50A9F8B019EAD4B90053E571 /* SPGotoDatabaseController.m */; }; 50E217B318174246009D3580 /* SPColorSelectorView.m in Sources */ = {isa = PBXBuildFile; fileRef = 50E217B218174246009D3580 /* SPColorSelectorView.m */; }; 50E217B618174280009D3580 /* SPFavoriteColorSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 50E217B518174280009D3580 /* SPFavoriteColorSupport.m */; }; 5806B76411A991EC00813A88 /* SPDocumentController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5806B76311A991EC00813A88 /* SPDocumentController.m */; }; @@ -879,6 +881,9 @@ 4DECC3340EC2A170008D359E /* Growl.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Growl.framework; path = Frameworks/Growl.framework; sourceTree = ""; }; 501B1D161728A3DA0017C92E /* SPCharsetCollationHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPCharsetCollationHelper.h; sourceTree = ""; }; 501B1D171728A3DA0017C92E /* SPCharsetCollationHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPCharsetCollationHelper.m; sourceTree = ""; }; + 50A9F8AC19EAD4860053E571 /* GotoDatabaseDialog.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = GotoDatabaseDialog.xib; path = English.lproj/GotoDatabaseDialog.xib; sourceTree = ""; }; + 50A9F8AF19EAD4B90053E571 /* SPGotoDatabaseController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPGotoDatabaseController.h; sourceTree = ""; }; + 50A9F8B019EAD4B90053E571 /* SPGotoDatabaseController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPGotoDatabaseController.m; sourceTree = ""; }; 50E217B118174246009D3580 /* SPColorSelectorView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPColorSelectorView.h; sourceTree = ""; }; 50E217B218174246009D3580 /* SPColorSelectorView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPColorSelectorView.m; sourceTree = ""; }; 50E217B418174280009D3580 /* SPFavoriteColorSupport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPFavoriteColorSupport.h; sourceTree = ""; }; @@ -1583,6 +1588,8 @@ 17846B9D170C95D800414499 /* Process List */, 17381853151FB29C0078FFE2 /* User Manager */, 1713C73D140D88D400CFD461 /* Query Controller */, + 50A9F8AF19EAD4B90053E571 /* SPGotoDatabaseController.h */, + 50A9F8B019EAD4B90053E571 /* SPGotoDatabaseController.m */, ); name = "Subview Controllers"; sourceTree = ""; @@ -2202,6 +2209,7 @@ 1792C13010AD752100ABE758 /* DatabaseServerVariables.xib */, 58C3507310B9ADEA00D37E14 /* ContentPaginationView.xib */, 17A7773611C52E61001E27B4 /* IndexesView.xib */, + 50A9F8AC19EAD4860053E571 /* GotoDatabaseDialog.xib */, ); path = Interfaces; sourceTree = ""; @@ -2914,6 +2922,7 @@ C9AD7C7C1676158C00234EEE /* toolbar-switch-to-sql@2x.png in Resources */, C9AD7C7F167619B400234EEE /* button_refresh.png in Resources */, C9AD7C80167619B400234EEE /* button_refresh@2x.png in Resources */, + 50A9F8AD19EAD4860053E571 /* GotoDatabaseDialog.xib in Resources */, C9AD7C8316761B3300234EEE /* button_action.png in Resources */, C9AD7C8416761B3300234EEE /* button_action@2x.png in Resources */, C9AD7C891676204300234EEE /* button_info_pane_hide.png in Resources */, @@ -3085,6 +3094,7 @@ 17E6415A0EF01EF6001BC333 /* SPDatabaseDocument.m in Sources */, 17E6415B0EF01EF6001BC333 /* SPDataImport.m in Sources */, 17E6415C0EF01EF6001BC333 /* SPTableStructure.m in Sources */, + 50A9F8B119EAD4B90053E571 /* SPGotoDatabaseController.m in Sources */, 17E641640EF01F15001BC333 /* SPTableInfo.m in Sources */, 17E641650EF01F15001BC333 /* SPTablesList.m in Sources */, 17E6416C0EF01F37001BC333 /* ImageAndTextCell.m in Sources */, -- cgit v1.2.3