From e45b9985c2133653895afd0f4cfe53dfa37d4029 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Tue, 7 May 2013 00:45:06 -0400 Subject: [PATCH] Mac: New sites. [MOVED] Creation of new elements moved to shared code. [FIXED] When switching user, unset active key. [FIXED] Synchronize content calculation to avoid race issues while typing. [ADDED] Ability to create new sites. [FIXED] Unset active element when hitting backspace or escape. --- MasterPassword/ObjC/MPAppDelegate_Store.h | 2 + MasterPassword/ObjC/MPAppDelegate_Store.m | 39 ++++- MasterPassword/ObjC/Mac/MPMacAppDelegate.m | 2 + .../ObjC/Mac/MPPasswordWindowController.m | 162 +++++++++++------- .../ObjC/iOS/MPElementListAllViewController.m | 6 +- .../ObjC/iOS/MPElementListController.h | 1 - .../ObjC/iOS/MPElementListController.m | 42 ----- .../ObjC/iOS/MPElementListSearchController.m | 7 +- 8 files changed, 153 insertions(+), 108 deletions(-) diff --git a/MasterPassword/ObjC/MPAppDelegate_Store.h b/MasterPassword/ObjC/MPAppDelegate_Store.h index ae14f37d..8b6ee409 100644 --- a/MasterPassword/ObjC/MPAppDelegate_Store.h +++ b/MasterPassword/ObjC/MPAppDelegate_Store.h @@ -25,6 +25,8 @@ typedef enum { + (BOOL)managedObjectContextPerformBlockAndWait:(void (^)(NSManagedObjectContext *))mocBlock; - (UbiquityStoreManager *)storeManager; + +- (void)addElementNamed:(NSString *)siteName completion:(void (^)(MPElementEntity *element))completion; - (MPImportResult)importSites:(NSString *)importedSitesString askImportPassword:(NSString *(^)(NSString *userName))importPassword askUserPassword:(NSString *(^)(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))userPassword; diff --git a/MasterPassword/ObjC/MPAppDelegate_Store.m b/MasterPassword/ObjC/MPAppDelegate_Store.m index d76f9809..9a73dade 100644 --- a/MasterPassword/ObjC/MPAppDelegate_Store.m +++ b/MasterPassword/ObjC/MPAppDelegate_Store.m @@ -384,7 +384,44 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, #endif -#pragma mark - Import / Export +#pragma mark - Utilities + +- (void)addElementNamed:(NSString *)siteName completion:(void (^)(MPElementEntity *element))completion { + + if (![siteName length]) { + completion( nil ); + return; + } + + [MPAppDelegate_Shared managedObjectContextPerformBlock:^(NSManagedObjectContext *moc) { + MPUserEntity *activeUser = [self activeUserInContext:moc]; + assert(activeUser); + + MPElementType type = activeUser.defaultType; + if (!type) + type = activeUser.defaultType = MPElementTypeGeneratedLong; + NSString *typeEntityClassName = [MPAlgorithmDefault classNameOfType:type]; + + MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:typeEntityClassName + inManagedObjectContext:moc]; + + element.name = siteName; + element.user = activeUser; + element.type = type; + element.lastUsed = [NSDate date]; + element.version = MPAlgorithmDefaultVersion; + [moc saveToStore]; + + NSError *error = nil; + if (element.objectID.isTemporaryID && ![moc obtainPermanentIDsForObjects:@[ element ] error:&error]) + err(@"Failed to obtain a permanent object ID after creating new element: %@", error); + + NSManagedObjectID *elementOID = [element objectID]; + dispatch_async( dispatch_get_main_queue(), ^{ + completion( (MPElementEntity *)[[MPAppDelegate_Shared managedObjectContextForThreadIfReady] objectRegisteredForID:elementOID] ); + } ); + }]; +} - (MPImportResult)importSites:(NSString *)importedSitesString askImportPassword:(NSString *(^)(NSString *userName))importPassword diff --git a/MasterPassword/ObjC/Mac/MPMacAppDelegate.m b/MasterPassword/ObjC/Mac/MPMacAppDelegate.m index 96cf8000..fc0f4b54 100644 --- a/MasterPassword/ObjC/Mac/MPMacAppDelegate.m +++ b/MasterPassword/ObjC/Mac/MPMacAppDelegate.m @@ -105,6 +105,8 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven - (void)selectUser:(NSMenuItem *)item { + [self signOutAnimated:NO]; + NSError *error = nil; NSManagedObjectContext *moc = [MPMacAppDelegate managedObjectContextForThreadIfReady]; self.activeUser = (MPUserEntity *)[moc existingObjectWithID:[item representedObject] error:&error]; diff --git a/MasterPassword/ObjC/Mac/MPPasswordWindowController.m b/MasterPassword/ObjC/Mac/MPPasswordWindowController.m index 3b7f6727..619be215 100644 --- a/MasterPassword/ObjC/Mac/MPPasswordWindowController.m +++ b/MasterPassword/ObjC/Mac/MPPasswordWindowController.m @@ -13,12 +13,14 @@ #define MPAlertUnlockMP @"MPAlertUnlockMP" #define MPAlertIncorrectMP @"MPAlertIncorrectMP" +#define MPAlertCreateSite @"MPAlertCreateSite" @interface MPPasswordWindowController() @property(nonatomic) BOOL inProgress; @property(nonatomic) BOOL siteFieldPreventCompletion; +@property(nonatomic, strong) NSOperationQueue *backgroundQueue; @end @implementation MPPasswordWindowController { @@ -32,6 +34,9 @@ else self.window.styleMask = NSTexturedBackgroundWindowMask | NSResizableWindowMask | NSTitledWindowMask | NSClosableWindowMask; + self.backgroundQueue = [NSOperationQueue new]; + self.backgroundQueue.maxConcurrentOperationCount = 1; + [self setContent:@""]; [self.tipField setStringValue:@""]; @@ -81,7 +86,7 @@ if (![MPMacAppDelegate get].key) // Ask the user to set the key through his master password. - dispatch_async( dispatch_get_main_queue(), ^{ + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ if ([MPMacAppDelegate get].key) return; @@ -99,7 +104,7 @@ [passwordField becomeFirstResponder]; [alert beginSheetModalForWindow:self.window modalDelegate:self didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:MPAlertUnlockMP]; - } ); + }]; } - (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { @@ -112,9 +117,8 @@ NSManagedObjectContext *moc = [MPMacAppDelegate managedObjectContextForThreadIfReady]; MPUserEntity *activeUser = [[MPMacAppDelegate get] activeUserInContext:moc]; switch (returnCode) { - case NSAlertAlternateReturn: + case NSAlertAlternateReturn: { // "Change" button. - { NSInteger returnCode_ = [[NSAlert alertWithMessageText:@"Changing Master Password" defaultButton:nil alternateButton:[PearlStrings get].commonButtonCancel otherButton:nil informativeTextWithFormat: @@ -131,13 +135,14 @@ [[MPMacAppDelegate get] signOutAnimated:YES]; [moc saveToStore]; } - } break; + } - case NSAlertOtherReturn: + case NSAlertOtherReturn: { // "Cancel" button. [self.window close]; return; + } case NSAlertDefaultReturn: { // "Unlock" button. @@ -156,7 +161,7 @@ usingMasterPassword:password]; self.inProgress = NO; - dispatch_async( dispatch_get_main_queue(), ^{ + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ [self.progressView stopAnimation:nil]; if (success) @@ -167,8 +172,9 @@ }]] beginSheetModalForWindow:self.window modalDelegate:self didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:MPAlertIncorrectMP]; } - } ); + }]; }]; + break; } default: @@ -177,12 +183,27 @@ return; } + if (contextInfo == MPAlertCreateSite) { + switch (returnCode) { + case NSAlertDefaultReturn: { + [[MPMacAppDelegate get] addElementNamed:[self.siteField stringValue] completion:^(MPElementEntity *element) { + if (element) { + _activeElementOID = element.objectID; + [self trySiteWithAction:NO]; + } + }]; + break; + } + default: + break; + } + } } - (NSArray *)control:(NSControl *)control textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index { - NSString *query = [[control stringValue] substringWithRange:charRange]; + NSString *query = [[textView string] substringWithRange:charRange]; if (![query length] || ![MPMacAppDelegate get].key) return nil; @@ -203,12 +224,14 @@ [mutableResults addObject:element.name]; //[mutableResults addObject:query]; // For when the app should be able to create new sites. } + else + _activeElementOID = nil; }]; - if ([mutableResults count] == 1) { + if ([mutableResults count] < 2) { //[textView setString:[(MPElementEntity *)[siteResults objectAtIndex:0] name]]; //[textView setSelectedRange:NSMakeRange( [query length], [[textView string] length] - [query length] )]; - [self trySiteAndCopyContent:NO]; + [self trySiteWithAction:NO]; } return mutableResults; @@ -216,14 +239,17 @@ - (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector { - if (commandSelector == @selector(cancel:)) { + if (commandSelector == @selector(cancel:)) { // Escape without completion. [self.window close]; return YES; } - if ((self.siteFieldPreventCompletion = [NSStringFromSelector( commandSelector ) hasPrefix:@"delete"])) + if ((self.siteFieldPreventCompletion = [NSStringFromSelector( commandSelector ) hasPrefix:@"delete"])) { // Backspace any time. + _activeElementOID = nil; + [self trySiteWithAction:NO]; return NO; - if (commandSelector == @selector(insertNewline:)) { - [self trySiteAndCopyContent:YES]; + } + if (commandSelector == @selector(insertNewline:)) { // Return without completion. + [self trySiteWithAction:YES]; return YES; } @@ -235,7 +261,7 @@ if (note.object != self.siteField) return; - [self trySiteAndCopyContent:NO]; + [self trySiteWithAction:NO]; } - (void)controlTextDidChange:(NSNotification *)note { @@ -244,12 +270,18 @@ return; // Update the site content as the site name changes. - BOOL enterPressed = [[NSApp currentEvent] type] == NSKeyDown && - [[[NSApp currentEvent] charactersIgnoringModifiers] isEqualToString:@"\r"]; - [self trySiteAndCopyContent:enterPressed]; - - if (enterPressed) + if ([[NSApp currentEvent] type] == NSKeyDown && + [[[NSApp currentEvent] charactersIgnoringModifiers] isEqualToString:@"\r"]) { // Return while completing. + [self trySiteWithAction:YES]; return; + } + + if ([[NSApp currentEvent] type] == NSKeyDown && + [[[NSApp currentEvent] charactersIgnoringModifiers] characterAtIndex:0] == 0x1b) { // Escape while completing. + _activeElementOID = nil; + [self trySiteWithAction:NO]; + return; + } if (self.siteFieldPreventCompletion) { self.siteFieldPreventCompletion = NO; @@ -289,67 +321,75 @@ }]]; } -- (void)trySiteAndCopyContent:(BOOL)copyContent { +- (void)trySiteWithAction:(BOOL)doAction { - [self setContent:@""]; - [self.tipField setStringValue:@"Generating..."]; - - dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0 ), ^{ + [self.backgroundQueue addOperationWithBlock:^{ NSString *content = [[self activeElementForThread].content description]; if (!content) content = @""; - if (copyContent) { - [[NSPasteboard generalPasteboard] declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil]; - if (![[NSPasteboard generalPasteboard] setString:content forType:NSPasteboardTypeString]) { - wrn(@"Couldn't copy password to pasteboard."); + + NSString *siteName = [self.siteField stringValue]; + dbg(@"name: %@, action: %d", siteName, doAction); + if (doAction) { + if ([content length]) { + // Performing action while content is available. Copy it. + [self copyContent:content]; + } + else if ([siteName length]) { + // Performing action without content but a site name is written. + [self createNewSite:siteName]; return; } - - NSManagedObjectContext *moc = [MPMacAppDelegate managedObjectContextForThreadIfReady]; - MPElementEntity *activeElement = [self activeElementInContext:moc]; - [activeElement use]; - [moc saveToStore]; } - dispatch_async( dispatch_get_main_queue(), ^{ + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ [self setContent:content]; self.tipField.alphaValue = 1; - if (!copyContent) + if ([content length] == 0) { + if ([siteName length]) + [self.tipField setStringValue:@"Hit ⌤ (ENTER) to create a new site."]; + else + [self.tipField setStringValue:@""]; + } + else if (!doAction) [self.tipField setStringValue:@"Hit ⌤ (ENTER) to copy the password."]; else { [self.tipField setStringValue:@"Copied! Hit ⎋ (ESC) to close window."]; - dispatch_time_t popTime = dispatch_time( DISPATCH_TIME_NOW, (int64_t)(5.0f * NSEC_PER_SEC) ); - dispatch_after( popTime, dispatch_get_main_queue(), ^{ + dispatch_after( dispatch_time( DISPATCH_TIME_NOW, (int64_t)(5.0f * NSEC_PER_SEC) ), dispatch_get_main_queue(), ^{ [NSAnimationContext beginGrouping]; [[NSAnimationContext currentContext] setDuration:0.2f]; [self.tipField.animator setAlphaValue:0]; [NSAnimationContext endGrouping]; } ); } - } ); - } ); + }]; + }]; +} - // For when the app should be able to create new sites. - /* - else - [[MPMacAppDelegate get].managedObjectContext performBlock:^{ - MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPElementGeneratedEntity class]) - inManagedObjectContext:[MPMacAppDelegate get].managedObjectContext]; - assert([element isKindOfClass:ClassFromMPElementType(element.type)]); - assert([MPMacAppDelegate get].keyID); - - element.name = siteName; - element.keyID = [MPMacAppDelegate get].keyID; - - NSString *description = [element.content description]; - [element use]; - - dispatch_async(dispatch_get_main_queue(), ^{ - [self setContent:description]; - }); - }]; - */ +- (void)copyContent:(NSString *)content { + + [[NSPasteboard generalPasteboard] declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil]; + if (![[NSPasteboard generalPasteboard] setString:content forType:NSPasteboardTypeString]) { + wrn(@"Couldn't copy password to pasteboard."); + return; + } + + NSManagedObjectContext *moc = [MPMacAppDelegate managedObjectContextForThreadIfReady]; + MPElementEntity *activeElement = [self activeElementInContext:moc]; + [activeElement use]; + [moc saveToStore]; +} + +- (void)createNewSite:(NSString *)siteName { + + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + NSAlert *alert = [NSAlert alertWithMessageText:@"Create site?" + defaultButton:@"Create" alternateButton:nil otherButton:@"Cancel" + informativeTextWithFormat:@"Do you want to create a new site named:\n\n%@", siteName]; + [alert beginSheetModalForWindow:self.window modalDelegate:self + didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:MPAlertCreateSite]; + }]; } @end diff --git a/MasterPassword/ObjC/iOS/MPElementListAllViewController.m b/MasterPassword/ObjC/iOS/MPElementListAllViewController.m index 86749d17..da655877 100644 --- a/MasterPassword/ObjC/iOS/MPElementListAllViewController.m +++ b/MasterPassword/ObjC/iOS/MPElementListAllViewController.m @@ -55,9 +55,11 @@ return; __weak MPElementListAllViewController *wSelf = self; - [self addElementNamed:[alert textFieldAtIndex:0].text completion:^(BOOL success) { - if (success) + [[MPiOSAppDelegate get] addElementNamed:[alert textFieldAtIndex:0].text completion:^(MPElementEntity *element) { + if (element) { + [wSelf.delegate didSelectElement:element]; [wSelf close:nil]; + } }]; } cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonOkay, nil]; diff --git a/MasterPassword/ObjC/iOS/MPElementListController.h b/MasterPassword/ObjC/iOS/MPElementListController.h index a9f55354..170e926b 100644 --- a/MasterPassword/ObjC/iOS/MPElementListController.h +++ b/MasterPassword/ObjC/iOS/MPElementListController.h @@ -15,7 +15,6 @@ @property(readonly) NSDateFormatter *dateFormatter; - (void)updateData; -- (void)addElementNamed:(NSString *)siteName completion:(void (^)(BOOL success))completion; - (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atTableIndexPath:(NSIndexPath *)indexPath; - (void)customTableViewUpdates; diff --git a/MasterPassword/ObjC/iOS/MPElementListController.m b/MasterPassword/ObjC/iOS/MPElementListController.m index abb7e73e..61333779 100644 --- a/MasterPassword/ObjC/iOS/MPElementListController.m +++ b/MasterPassword/ObjC/iOS/MPElementListController.m @@ -23,48 +23,6 @@ [super viewDidLoad]; } -- (void)addElementNamed:(NSString *)siteName completion:(void (^)(BOOL success))completion { - - if (![siteName length]) { - if (completion) - completion( false ); - return; - } - - [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *moc) { - MPUserEntity *activeUser = [[MPiOSAppDelegate get] activeUserInContext:moc]; - assert(activeUser); - - MPElementType type = activeUser.defaultType; - if (!type) - type = activeUser.defaultType = MPElementTypeGeneratedLong; - NSString *typeEntityClassName = [MPAlgorithmDefault classNameOfType:type]; - - MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:typeEntityClassName - inManagedObjectContext:moc]; - - element.name = siteName; - element.user = activeUser; - element.type = type; - element.lastUsed = [NSDate date]; - element.version = MPAlgorithmDefaultVersion; - [moc saveToStore]; - - NSError *error = nil; - if (element.objectID.isTemporaryID && ![moc obtainPermanentIDsForObjects:@[ element ] error:&error]) - err(@"Failed to obtain a permanent object ID after creating new element: %@", error); - - NSManagedObjectID *elementOID = [element objectID]; - dispatch_async( dispatch_get_main_queue(), ^{ - MPElementEntity *element_ = (MPElementEntity *)[[MPiOSAppDelegate managedObjectContextForThreadIfReady] - objectRegisteredForID:elementOID]; - [self.delegate didSelectElement:element_]; - if (completion) - completion( true ); - } ); - }]; -} - - (NSFetchedResultsController *)fetchedResultsControllerByLastUsed { if (!_fetchedResultsControllerByLastUsed) { diff --git a/MasterPassword/ObjC/iOS/MPElementListSearchController.m b/MasterPassword/ObjC/iOS/MPElementListSearchController.m index 963948ab..27167546 100644 --- a/MasterPassword/ObjC/iOS/MPElementListSearchController.m +++ b/MasterPassword/ObjC/iOS/MPElementListSearchController.m @@ -9,6 +9,7 @@ #import "MPElementListSearchController.h" #import "MPMainViewController.h" #import "MPiOSAppDelegate.h" +#import "MPAppDelegate_Store.h" @interface MPElementListSearchController() @@ -216,7 +217,11 @@ if (buttonIndex == [alert cancelButtonIndex]) return; - [self addElementNamed:siteName completion:nil]; + __weak MPElementListController *wSelf = self; + [[MPiOSAppDelegate get] addElementNamed:siteName completion:^(MPElementEntity *element) { + if (element) + [wSelf.delegate didSelectElement:element]; + }]; } cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonYes, nil]; }