From 8615f6af5d05f9444a55689256a008b1240635c9 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Wed, 29 Oct 2014 21:24:35 -0400 Subject: [PATCH] Fuzzy site name search and highlight fuzzy results. --- External/KCOrderedAccessorFix | 2 +- External/Pearl | 2 +- .../ObjC/Mac/MPPasswordWindowController.h | 1 - .../ObjC/Mac/MPPasswordWindowController.m | 121 +++++++----------- .../ObjC/Mac/MPPasswordWindowController.xib | 8 +- MasterPassword/ObjC/Mac/MPSiteModel.h | 8 +- MasterPassword/ObjC/Mac/MPSiteModel.m | 26 +++- 7 files changed, 81 insertions(+), 87 deletions(-) diff --git a/External/KCOrderedAccessorFix b/External/KCOrderedAccessorFix index e1955221..1b8f8b79 160000 --- a/External/KCOrderedAccessorFix +++ b/External/KCOrderedAccessorFix @@ -1 +1 @@ -Subproject commit e1955221bf52d53736e7d3e7d38465c509e02562 +Subproject commit 1b8f8b79ad12b70976c7a417ff1a9d29e8c0ed73 diff --git a/External/Pearl b/External/Pearl index ef628f38..2237aaf4 160000 --- a/External/Pearl +++ b/External/Pearl @@ -1 +1 @@ -Subproject commit ef628f388e70459725de21e3528831c705f88795 +Subproject commit 2237aaf4295f74627b63040c4c246cddff339958 diff --git a/MasterPassword/ObjC/Mac/MPPasswordWindowController.h b/MasterPassword/ObjC/Mac/MPPasswordWindowController.h index ee4fdcbd..2bc790f6 100644 --- a/MasterPassword/ObjC/Mac/MPPasswordWindowController.h +++ b/MasterPassword/ObjC/Mac/MPPasswordWindowController.h @@ -29,7 +29,6 @@ @property(nonatomic) BOOL alternatePressed; @property(nonatomic) BOOL locked; @property(nonatomic) BOOL newUser; -@property(nonatomic) BOOL alwaysYes; @property(nonatomic, weak) IBOutlet NSArrayController *sitesController; @property(nonatomic, weak) IBOutlet NSImageView *blurView; diff --git a/MasterPassword/ObjC/Mac/MPPasswordWindowController.m b/MasterPassword/ObjC/Mac/MPPasswordWindowController.m index 04340736..1c89d976 100644 --- a/MasterPassword/ObjC/Mac/MPPasswordWindowController.m +++ b/MasterPassword/ObjC/Mac/MPPasswordWindowController.m @@ -36,7 +36,7 @@ @end -@implementation MPPasswordWindowController { BOOL _skipTextChange; } +@implementation MPPasswordWindowController #pragma mark - Life @@ -54,31 +54,33 @@ // }]; [[NSNotificationCenter defaultCenter] addObserverForName:NSWindowDidBecomeKeyNotification object:self.window queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { - [self fadeIn]; - [self updateUser]; - }]; + [self fadeIn]; + [self updateUser]; + }]; [[NSNotificationCenter defaultCenter] addObserverForName:NSWindowWillCloseNotification object:self.window queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { - NSWindow *sheet = [self.window attachedSheet]; - if (sheet) - [NSApp endSheet:sheet]; - }]; + NSWindow *sheet = [self.window attachedSheet]; + if (sheet) + [NSApp endSheet:sheet]; + }]; [[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillResignActiveNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { +#ifndef DEBUG [self fadeOut]; - }]; +#endif + }]; [[NSNotificationCenter defaultCenter] addObserverForName:MPSignedInNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { - [self updateUser]; - }]; + [self updateUser]; + }]; [[NSNotificationCenter defaultCenter] addObserverForName:MPSignedOutNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { - [self updateUser]; - }]; + [self updateUser]; + }]; [self observeKeyPath:@"sitesController.selection" withBlock:^(id from, id to, NSKeyValueChange cause, id _self) { - [_self updateSelection]; - }]; + [_self updateSelection]; + }]; NSSearchFieldCell *siteFieldCell = self.siteField.cell; siteFieldCell.searchButtonCell = nil; @@ -124,17 +126,15 @@ - (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector { if (control == self.siteField) { - if ([NSStringFromSelector( commandSelector ) rangeOfString:@"delete"].location == 0) { - _skipTextChange = YES; - dbg_return_tr( NO, @, control, NSStringFromSelector( commandSelector ) ); - } + if ([NSStringFromSelector( commandSelector ) rangeOfString:@"delete"].location == 0) + return NO; } if (control == self.securePasswordField || control == self.revealPasswordField) { if (commandSelector == @selector( insertNewline: )) - dbg_return_tr( NO, @, control, NSStringFromSelector( commandSelector ) ); + return NO; } - dbg_return_tr( [self handleCommand:commandSelector], @, control, NSStringFromSelector( commandSelector ) ); + return [self handleCommand:commandSelector]; } - (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor { @@ -174,7 +174,6 @@ - (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector { - dbg( @"textView:%@doCommandBySelector:%@", textView, NSStringFromSelector( commandSelector ) ); return [self handleCommand:commandSelector]; } @@ -227,9 +226,9 @@ // "Create" button. [[MPMacAppDelegate get] addSiteNamed:[self.siteField stringValue] completion: ^(MPSiteEntity *site, NSManagedObjectContext *context) { - if (site) - PearlMainQueue( ^{ [self updateSites]; } ); - }]; + if (site) + PearlMainQueue( ^{ [self updateSites]; } ); + }]; break; } default: @@ -305,19 +304,6 @@ #pragma mark - State -- (NSString *)query { - - return [self.siteField.stringValue stringByReplacingCharactersInRange:self.siteField.currentEditor.selectedRange withString:@""]?: @""; -} - -- (BOOL)alwaysYes { - - return YES; -} - -- (void)setAlwaysYes:(BOOL)alwaysYes { -} - - (void)insertObject:(MPSiteModel *)model inSitesAtIndex:(NSUInteger)index { [self.sites insertObject:model atIndex:index]; @@ -347,7 +333,7 @@ [alert addButtonWithTitle:@"Delete"]; [alert addButtonWithTitle:@"Cancel"]; [alert setMessageText:@"Delete Site?"]; - [alert setInformativeText:strf( @"Do you want to delete the site named:\n\n%@", self.selectedSite.siteName )]; + [alert setInformativeText:strf( @"Do you want to delete the site named:\n\n%@", self.selectedSite.name )]; [alert beginSheetModalForWindow:self.window modalDelegate:self didEndSelector:@selector( alertDidEnd:returnCode:contextInfo: ) contextInfo:MPAlertDeleteSite]; } @@ -358,7 +344,7 @@ [alert addButtonWithTitle:@"Save"]; [alert addButtonWithTitle:@"Cancel"]; [alert setMessageText:@"Change Login Name"]; - [alert setInformativeText:strf( @"Enter the login name for: %@", self.selectedSite.siteName )]; + [alert setInformativeText:strf( @"Enter the login name for: %@", self.selectedSite.name )]; NSTextField *loginField = [[NSTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )]; loginField.stringValue = self.selectedSite.loginName?: @""; [loginField selectText:self]; @@ -392,7 +378,7 @@ [alert addButtonWithTitle:@"Save"]; [alert addButtonWithTitle:@"Cancel"]; [alert setMessageText:@"Change Password"]; - [alert setInformativeText:strf( @"Enter the new password for: %@", self.selectedSite.siteName )]; + [alert setInformativeText:strf( @"Enter the new password for: %@", self.selectedSite.name )]; [alert setAccessoryView:[[NSSecureTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )]]; [alert layout]; [alert beginSheetModalForWindow:self.window modalDelegate:self @@ -408,8 +394,9 @@ MPSiteType type = [types[t] unsignedIntegerValue]; NSString *title = [site.algorithm nameOfType:type]; if (type & MPSiteTypeClassGenerated) - title = [site.algorithm generatePasswordForSiteNamed:site.siteName ofType:type - withCounter:site.counter usingKey:[MPMacAppDelegate get].key]; + title = [site.algorithm generatePasswordForSiteNamed:site.name ofType:type + withCounter:site.counter + usingKey:[MPMacAppDelegate get].key]; NSButtonCell *cell = [self.passwordTypesMatrix cellAtRow:(NSInteger)t column:0]; cell.tag = type; @@ -421,7 +408,7 @@ [alert addButtonWithTitle:@"Save"]; [alert addButtonWithTitle:@"Cancel"]; [alert setMessageText:@"Change Password Type"]; - [alert setInformativeText:strf( @"Choose a new password type for: %@", site.siteName )]; + [alert setInformativeText:strf( @"Choose a new password type for: %@", site.name )]; [alert setAccessoryView:self.passwordTypesBox]; [alert layout]; [alert beginSheetModalForWindow:self.window modalDelegate:self @@ -464,9 +451,9 @@ NSUserNotification *notification = [NSUserNotification new]; notification.title = @"Password Copied"; if (selectedSite.loginName.length) - notification.subtitle = strf( @"%@ at %@", selectedSite.loginName, selectedSite.siteName ); + notification.subtitle = strf( @"%@ at %@", selectedSite.loginName, selectedSite.name ); else - notification.subtitle = selectedSite.siteName; + notification.subtitle = selectedSite.name; [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification]; } else { @@ -515,12 +502,24 @@ return; } - NSString *query = [self query]; + static NSRegularExpression *fuzzyRE; + static dispatch_once_t once = 0; + dispatch_once( &once, ^{ + fuzzyRE = [NSRegularExpression regularExpressionWithPattern:@"(.)" options:0 error:nil]; + } ); + + NSString *queryString = self.siteField.stringValue; + NSString *queryPattern = [queryString stringByReplacingMatchesOfExpression:fuzzyRE withTemplate:@"*$1*"]; + NSMutableArray *fuzzyGroups = [NSMutableArray new]; + [fuzzyRE enumerateMatchesInString:queryString options:0 range:NSMakeRange( 0, queryString.length ) + usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { + [fuzzyGroups addObject:[queryString substringWithRange:result.range] ]; + }]; [MPMacAppDelegate managedObjectContextPerformBlockAndWait:^(NSManagedObjectContext *context) { NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPSiteEntity class] )]; fetchRequest.sortDescriptors = @[ [[NSSortDescriptor alloc] initWithKey:@"lastUsed" ascending:NO] ]; - fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND user == %@", - query, query, [[MPMacAppDelegate get] activeUserInContext:context]]; + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name LIKE[cd] %@) AND user == %@", + queryPattern, queryPattern, [[MPMacAppDelegate get] activeUserInContext:context]]; NSError *error = nil; NSArray *siteResults = [context executeFetchRequest:fetchRequest error:&error]; @@ -531,36 +530,14 @@ NSMutableArray *newSites = [NSMutableArray arrayWithCapacity:[siteResults count]]; for (MPSiteEntity *site in siteResults) - [newSites addObject:[[MPSiteModel alloc] initWithEntity:site]]; + [newSites addObject:[[MPSiteModel alloc] initWithEntity:site fuzzyGroups:fuzzyGroups]]; self.sites = newSites; }]; } - (void)updateSelection { - if (_skipTextChange) { - _skipTextChange = NO; - return; - } - - NSString *siteName = self.selectedSite.siteName; - if (!siteName) - return; - - if ([self.window isKeyWindow] && [self.siteField isEqual:[self.window firstResponder]]) { - NSRange siteNameQueryRange = [siteName rangeOfString:[self query]]; - self.siteField.stringValue = siteName; - - if (siteNameQueryRange.location == 0) - self.siteField.currentEditor.selectedRange = - NSMakeRange( siteNameQueryRange.length, siteName.length - siteNameQueryRange.length ); - } - [self.siteTable scrollRowToVisible:(NSInteger)self.sitesController.selectionIndex]; - [self updateGradient]; -} - -- (void)updateGradient { NSView *siteScrollView = self.siteTable.superview.superview; NSRect selectedCellFrame = [self.siteTable frameOfCellAtColumn:0 row:((NSInteger)self.sitesController.selectionIndex)]; diff --git a/MasterPassword/ObjC/Mac/MPPasswordWindowController.xib b/MasterPassword/ObjC/Mac/MPPasswordWindowController.xib index d99fcf9a..7df150cc 100644 --- a/MasterPassword/ObjC/Mac/MPPasswordWindowController.xib +++ b/MasterPassword/ObjC/Mac/MPPasswordWindowController.xib @@ -48,7 +48,7 @@ - + @@ -181,11 +181,11 @@ - + - + @@ -438,7 +438,7 @@ NSNegateBoolean - + diff --git a/MasterPassword/ObjC/Mac/MPSiteModel.h b/MasterPassword/ObjC/Mac/MPSiteModel.h index 2a83576e..9d0c561c 100644 --- a/MasterPassword/ObjC/Mac/MPSiteModel.h +++ b/MasterPassword/ObjC/Mac/MPSiteModel.h @@ -17,15 +17,17 @@ // #import +#import "MPSiteEntity.h" @class MPSiteEntity; @interface MPSiteModel : NSObject -@property (nonatomic) NSString *siteName; +@property (nonatomic) NSString *name; +@property (nonatomic) NSAttributedString *displayedName; @property (nonatomic) MPSiteType type; @property (nonatomic) NSString *typeName; @property (nonatomic) NSString *content; -@property (nonatomic) NSString *contentDisplay; +@property (nonatomic) NSString *displayedContent; @property (nonatomic) NSString *loginName; @property (nonatomic) NSNumber *uses; @property (nonatomic) NSUInteger counter; @@ -34,7 +36,7 @@ @property (nonatomic) BOOL generated; @property (nonatomic) BOOL stored; -- (id)initWithEntity:(MPSiteEntity *)entity; +- (instancetype)initWithEntity:(MPSiteEntity *)entity fuzzyGroups:(NSArray *)fuzzyGroups; - (MPSiteEntity *)entityInContext:(NSManagedObjectContext *)moc; - (void)updateContent; diff --git a/MasterPassword/ObjC/Mac/MPSiteModel.m b/MasterPassword/ObjC/Mac/MPSiteModel.m index c7dd6ed7..c3e71205 100644 --- a/MasterPassword/ObjC/Mac/MPSiteModel.m +++ b/MasterPassword/ObjC/Mac/MPSiteModel.m @@ -28,25 +28,41 @@ BOOL _initialized; } -- (id)initWithEntity:(MPSiteEntity *)entity { +- (id)initWithEntity:(MPSiteEntity *)entity fuzzyGroups:(NSArray *)fuzzyGroups { if (!(self = [super init])) return nil; - [self setEntity:entity]; + [self setEntity:entity fuzzyGroups:fuzzyGroups]; _initialized = YES; return self; } -- (void)setEntity:(MPSiteEntity *)entity { +- (void)setEntity:(MPSiteEntity *)entity fuzzyGroups:(NSArray *)fuzzyGroups { if ([_entityOID isEqual:entity.objectID]) return; _entityOID = entity.objectID; + NSString *siteName = entity.name; + NSMutableAttributedString *attributedSiteName = [[NSMutableAttributedString alloc] initWithString:siteName]; + for (NSUInteger f = 0, s = (NSUInteger)-1; f < [fuzzyGroups count]; ++f) { + s = [siteName rangeOfString:fuzzyGroups[f] options:NSDiacriticInsensitiveSearch | NSCaseInsensitiveSearch + range:NSMakeRange( s + 1, [siteName length] - (s + 1) )].location; + if (s == NSNotFound) + break; + + [attributedSiteName addAttribute:NSBackgroundColorAttributeName value:[NSColor alternateSelectedControlColor] + range:NSMakeRange( s, [fuzzyGroups[f] length] )]; + } + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; + paragraphStyle.alignment = NSCenterTextAlignment; + [attributedSiteName addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange( 0, [siteName length] )]; + + self.displayedName = attributedSiteName; + self.name = siteName; self.algorithm = entity.algorithm; - self.siteName = entity.name; self.lastUsed = entity.lastUsed; self.type = entity.type; self.typeName = entity.typeName; @@ -123,7 +139,7 @@ PearlMainQueue( ^{ self.content = result; - self.contentDisplay = displayResult; + self.displayedContent = displayResult; } ); }]; [entity resolveLoginUsingKey:[MPAppDelegate_Shared get].key result:^(NSString *result) {