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/SPGotoDatabaseController.m | 223 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 Source/SPGotoDatabaseController.m (limited to 'Source/SPGotoDatabaseController.m') 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