From 4b2251d4fa776ee1ded6881e766628638f7e1596 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Mon, 3 Nov 2014 12:11:46 -0500 Subject: [PATCH] Support for fuzzy searching on iOS. --- .../ObjC/Mac/MPPasswordWindowController.m | 2 +- MasterPassword/ObjC/iOS/MPPasswordCell.h | 2 ++ MasterPassword/ObjC/iOS/MPPasswordCell.m | 33 +++++++++++++++++-- .../ObjC/iOS/MPPasswordsViewController.m | 26 ++++++++++++--- 4 files changed, 55 insertions(+), 8 deletions(-) diff --git a/MasterPassword/ObjC/Mac/MPPasswordWindowController.m b/MasterPassword/ObjC/Mac/MPPasswordWindowController.m index 1c89d976..90447278 100644 --- a/MasterPassword/ObjC/Mac/MPPasswordWindowController.m +++ b/MasterPassword/ObjC/Mac/MPPasswordWindowController.m @@ -519,7 +519,7 @@ NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPSiteEntity class] )]; fetchRequest.sortDescriptors = @[ [[NSSortDescriptor alloc] initWithKey:@"lastUsed" ascending:NO] ]; fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name LIKE[cd] %@) AND user == %@", - queryPattern, queryPattern, [[MPMacAppDelegate get] activeUserInContext:context]]; + queryPattern, queryPattern, [MPMacAppDelegate get].activeUserOID]; NSError *error = nil; NSArray *siteResults = [context executeFetchRequest:fetchRequest error:&error]; diff --git a/MasterPassword/ObjC/iOS/MPPasswordCell.h b/MasterPassword/ObjC/iOS/MPPasswordCell.h index 4bb9a67a..a9b21dc0 100644 --- a/MasterPassword/ObjC/iOS/MPPasswordCell.h +++ b/MasterPassword/ObjC/iOS/MPPasswordCell.h @@ -27,6 +27,8 @@ typedef NS_ENUM ( NSUInteger, MPPasswordCellMode ) { @interface MPPasswordCell : MPCell +@property (nonatomic) NSArray *fuzzyGroups; + - (void)setSite:(MPSiteEntity *)site animated:(BOOL)animated; - (void)setTransientSite:(NSString *)siteName animated:(BOOL)animated; - (void)setMode:(MPPasswordCellMode)mode animated:(BOOL)animated; diff --git a/MasterPassword/ObjC/iOS/MPPasswordCell.m b/MasterPassword/ObjC/iOS/MPPasswordCell.m index b2140d18..369ffffc 100644 --- a/MasterPassword/ObjC/iOS/MPPasswordCell.m +++ b/MasterPassword/ObjC/iOS/MPPasswordCell.m @@ -133,6 +133,7 @@ [super prepareForReuse]; _siteOID = nil; + _fuzzyGroups = nil; self.transientSite = nil; self.mode = MPPasswordCellModePassword; [self updateAnimated:NO]; @@ -147,6 +148,15 @@ #pragma mark - State +- (void)setFuzzyGroups:(NSArray *)fuzzyGroups { + + if (_fuzzyGroups == fuzzyGroups) + return; + _fuzzyGroups = fuzzyGroups; + + [self updateSiteName:[self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]]]; +} + - (void)setMode:(MPPasswordCellMode)mode animated:(BOOL)animated { if (mode == _mode) @@ -490,8 +500,7 @@ [self.loginNameButton setTitle:@"Tap the pencil to save a username" forState:UIControlStateNormal]; // Site Name - self.siteNameLabel.text = strl( @"%@ - %@", self.transientSite?: mainSite.name, - self.transientSite? @"Tap to create": [mainSite.algorithm shortNameOfType:mainSite.type] ); + [self updateSiteName:mainSite]; // Site Password self.passwordField.secureTextEntry = [[MPiOSConfig get].hidePasswords boolValue]; @@ -557,6 +566,26 @@ }]; } +- (void)updateSiteName:(MPSiteEntity *)site { + + NSString *siteName = self.transientSite?: site.name; + NSMutableAttributedString *attributedSiteName = [[NSMutableAttributedString alloc] initWithString:siteName?: @""]; + if ([attributedSiteName length]) + for (NSUInteger f = 0, s = (NSUInteger)-1; f < [self.fuzzyGroups count]; ++f) { + s = [siteName rangeOfString:self.fuzzyGroups[f] options:NSDiacriticInsensitiveSearch | NSCaseInsensitiveSearch + range:NSMakeRange( s + 1, [siteName length] - (s + 1) )].location; + if (s == NSNotFound) + break; + + [attributedSiteName addAttribute:NSBackgroundColorAttributeName value:[UIColor redColor] + range:NSMakeRange( s, [self.fuzzyGroups[f] length] )]; + } + + [attributedSiteName appendAttributedString:stra( + strf( @" - %@", self.transientSite? @"Tap to create": [site.algorithm shortNameOfType:site.type] ), @{ } )]; + self.siteNameLabel.attributedText = attributedSiteName; +} + - (BOOL)copyContentOfSite:(MPSiteEntity *)site saveInContext:(NSManagedObjectContext *)context { inf( @"Copying password for: %@", site.name ); diff --git a/MasterPassword/ObjC/iOS/MPPasswordsViewController.m b/MasterPassword/ObjC/iOS/MPPasswordsViewController.m index fee25fcd..e0aa4753 100644 --- a/MasterPassword/ObjC/iOS/MPPasswordsViewController.m +++ b/MasterPassword/ObjC/iOS/MPPasswordsViewController.m @@ -44,6 +44,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { BOOL _showTransientItem; NSUInteger _transientItem; NSCharacterSet *_siteNameAcceptableCharactersSet; + NSArray *_fuzzyGroups; } #pragma mark - Life @@ -147,6 +148,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { MPPasswordCell *cell = [MPPasswordCell dequeueCellFromCollectionView:collectionView indexPath:indexPath]; + [cell setFuzzyGroups:_fuzzyGroups]; if (indexPath.item < ((id)self.fetchedResultsController.sections[indexPath.section]).numberOfObjects) [cell setSite:[self.fetchedResultsController objectAtIndexPath:indexPath] animated:NO]; else @@ -253,7 +255,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { if (searchBar == self.passwordsSearchBar) { - if ([self.query length] && [[self.query stringByTrimmingCharactersInSet:_siteNameAcceptableCharactersSet] length]) + if ([[self.query stringByTrimmingCharactersInSet:_siteNameAcceptableCharactersSet] length]) [self showTips:MPPasswordsBadNameTip]; [self updatePasswords]; @@ -364,7 +366,6 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { - (void)updatePasswords { - NSString *query = self.query; NSManagedObjectID *activeUserOID = [MPiOSAppDelegate get].activeUserOID; if (!activeUserOID) { PearlMainQueue( ^{ @@ -375,6 +376,20 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { return; } + static NSRegularExpression *fuzzyRE; + static dispatch_once_t once = 0; + dispatch_once( &once, ^{ + fuzzyRE = [NSRegularExpression regularExpressionWithPattern:@"(.)" options:0 error:nil]; + } ); + + NSString *queryString = self.query; + NSString *queryPattern = [queryString stringByReplacingMatchesOfExpression:fuzzyRE withTemplate:@"*$1*"]; + NSMutableArray *fuzzyGroups = [NSMutableArray arrayWithCapacity:[queryString length]]; + [fuzzyRE enumerateMatchesInString:queryString options:0 range:NSMakeRange( 0, queryString.length ) + usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { + [fuzzyGroups addObject:[queryString substringWithRange:result.range]]; + }]; + _fuzzyGroups = fuzzyGroups; [self.fetchedResultsController.managedObjectContext performBlock:^{ NSArray *oldSectionInfos = [self.fetchedResultsController sections]; NSMutableArray *oldSections = [[NSMutableArray alloc] initWithCapacity:[oldSectionInfos count]]; @@ -383,9 +398,8 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { NSError *error = nil; self.fetchedResultsController.fetchRequest.predicate = - [query length]? - [NSPredicate predicateWithFormat:@"user == %@ AND name BEGINSWITH[cd] %@", activeUserOID, query]: - [NSPredicate predicateWithFormat:@"user == %@", activeUserOID]; + [NSPredicate predicateWithFormat:@"(%@ == '' OR name LIKE[cd] %@) AND user == %@", + queryPattern, queryPattern, activeUserOID]; if (![self.fetchedResultsController performFetch:&error]) err( @"Couldn't fetch sites: %@", [error fullDescription] ); @@ -411,6 +425,8 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { if (finished) [self.passwordCollectionView setContentOffset:CGPointMake( 0, -self.passwordCollectionView.contentInset.top ) animated:YES]; + for (MPPasswordCell *cell in self.passwordCollectionView.visibleCells) + [cell setFuzzyGroups:_fuzzyGroups]; }]; } @catch (NSException *exception) {