2
0

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.
This commit is contained in:
Maarten Billemont 2013-05-07 00:45:06 -04:00
parent 8f4eb6df84
commit e45b9985c2
8 changed files with 153 additions and 108 deletions

View File

@ -25,6 +25,8 @@ typedef enum {
+ (BOOL)managedObjectContextPerformBlockAndWait:(void (^)(NSManagedObjectContext *))mocBlock; + (BOOL)managedObjectContextPerformBlockAndWait:(void (^)(NSManagedObjectContext *))mocBlock;
- (UbiquityStoreManager *)storeManager; - (UbiquityStoreManager *)storeManager;
- (void)addElementNamed:(NSString *)siteName completion:(void (^)(MPElementEntity *element))completion;
- (MPImportResult)importSites:(NSString *)importedSitesString - (MPImportResult)importSites:(NSString *)importedSitesString
askImportPassword:(NSString *(^)(NSString *userName))importPassword askImportPassword:(NSString *(^)(NSString *userName))importPassword
askUserPassword:(NSString *(^)(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))userPassword; askUserPassword:(NSString *(^)(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))userPassword;

View File

@ -384,7 +384,44 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
#endif #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 - (MPImportResult)importSites:(NSString *)importedSitesString
askImportPassword:(NSString *(^)(NSString *userName))importPassword askImportPassword:(NSString *(^)(NSString *userName))importPassword

View File

@ -105,6 +105,8 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven
- (void)selectUser:(NSMenuItem *)item { - (void)selectUser:(NSMenuItem *)item {
[self signOutAnimated:NO];
NSError *error = nil; NSError *error = nil;
NSManagedObjectContext *moc = [MPMacAppDelegate managedObjectContextForThreadIfReady]; NSManagedObjectContext *moc = [MPMacAppDelegate managedObjectContextForThreadIfReady];
self.activeUser = (MPUserEntity *)[moc existingObjectWithID:[item representedObject] error:&error]; self.activeUser = (MPUserEntity *)[moc existingObjectWithID:[item representedObject] error:&error];

View File

@ -13,12 +13,14 @@
#define MPAlertUnlockMP @"MPAlertUnlockMP" #define MPAlertUnlockMP @"MPAlertUnlockMP"
#define MPAlertIncorrectMP @"MPAlertIncorrectMP" #define MPAlertIncorrectMP @"MPAlertIncorrectMP"
#define MPAlertCreateSite @"MPAlertCreateSite"
@interface MPPasswordWindowController() @interface MPPasswordWindowController()
@property(nonatomic) BOOL inProgress; @property(nonatomic) BOOL inProgress;
@property(nonatomic) BOOL siteFieldPreventCompletion; @property(nonatomic) BOOL siteFieldPreventCompletion;
@property(nonatomic, strong) NSOperationQueue *backgroundQueue;
@end @end
@implementation MPPasswordWindowController { @implementation MPPasswordWindowController {
@ -32,6 +34,9 @@
else else
self.window.styleMask = NSTexturedBackgroundWindowMask | NSResizableWindowMask | NSTitledWindowMask | NSClosableWindowMask; self.window.styleMask = NSTexturedBackgroundWindowMask | NSResizableWindowMask | NSTitledWindowMask | NSClosableWindowMask;
self.backgroundQueue = [NSOperationQueue new];
self.backgroundQueue.maxConcurrentOperationCount = 1;
[self setContent:@""]; [self setContent:@""];
[self.tipField setStringValue:@""]; [self.tipField setStringValue:@""];
@ -81,7 +86,7 @@
if (![MPMacAppDelegate get].key) if (![MPMacAppDelegate get].key)
// Ask the user to set the key through his master password. // Ask the user to set the key through his master password.
dispatch_async( dispatch_get_main_queue(), ^{ [[NSOperationQueue mainQueue] addOperationWithBlock:^{
if ([MPMacAppDelegate get].key) if ([MPMacAppDelegate get].key)
return; return;
@ -99,7 +104,7 @@
[passwordField becomeFirstResponder]; [passwordField becomeFirstResponder];
[alert beginSheetModalForWindow:self.window modalDelegate:self [alert beginSheetModalForWindow:self.window modalDelegate:self
didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:MPAlertUnlockMP]; didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:MPAlertUnlockMP];
} ); }];
} }
- (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { - (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo {
@ -112,9 +117,8 @@
NSManagedObjectContext *moc = [MPMacAppDelegate managedObjectContextForThreadIfReady]; NSManagedObjectContext *moc = [MPMacAppDelegate managedObjectContextForThreadIfReady];
MPUserEntity *activeUser = [[MPMacAppDelegate get] activeUserInContext:moc]; MPUserEntity *activeUser = [[MPMacAppDelegate get] activeUserInContext:moc];
switch (returnCode) { switch (returnCode) {
case NSAlertAlternateReturn: case NSAlertAlternateReturn: {
// "Change" button. // "Change" button.
{
NSInteger returnCode_ = [[NSAlert NSInteger returnCode_ = [[NSAlert
alertWithMessageText:@"Changing Master Password" defaultButton:nil alertWithMessageText:@"Changing Master Password" defaultButton:nil
alternateButton:[PearlStrings get].commonButtonCancel otherButton:nil informativeTextWithFormat: alternateButton:[PearlStrings get].commonButtonCancel otherButton:nil informativeTextWithFormat:
@ -131,13 +135,14 @@
[[MPMacAppDelegate get] signOutAnimated:YES]; [[MPMacAppDelegate get] signOutAnimated:YES];
[moc saveToStore]; [moc saveToStore];
} }
}
break; break;
}
case NSAlertOtherReturn: case NSAlertOtherReturn: {
// "Cancel" button. // "Cancel" button.
[self.window close]; [self.window close];
return; return;
}
case NSAlertDefaultReturn: { case NSAlertDefaultReturn: {
// "Unlock" button. // "Unlock" button.
@ -156,7 +161,7 @@
usingMasterPassword:password]; usingMasterPassword:password];
self.inProgress = NO; self.inProgress = NO;
dispatch_async( dispatch_get_main_queue(), ^{ [[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self.progressView stopAnimation:nil]; [self.progressView stopAnimation:nil];
if (success) if (success)
@ -167,8 +172,9 @@
}]] beginSheetModalForWindow:self.window modalDelegate:self }]] beginSheetModalForWindow:self.window modalDelegate:self
didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:MPAlertIncorrectMP]; didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:MPAlertIncorrectMP];
} }
} ); }];
}]; }];
break;
} }
default: default:
@ -177,12 +183,27 @@
return; 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 - (NSArray *)control:(NSControl *)control textView:(NSTextView *)textView completions:(NSArray *)words
forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index { forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index {
NSString *query = [[control stringValue] substringWithRange:charRange]; NSString *query = [[textView string] substringWithRange:charRange];
if (![query length] || ![MPMacAppDelegate get].key) if (![query length] || ![MPMacAppDelegate get].key)
return nil; return nil;
@ -203,12 +224,14 @@
[mutableResults addObject:element.name]; [mutableResults addObject:element.name];
//[mutableResults addObject:query]; // For when the app should be able to create new sites. //[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 setString:[(MPElementEntity *)[siteResults objectAtIndex:0] name]];
//[textView setSelectedRange:NSMakeRange( [query length], [[textView string] length] - [query length] )]; //[textView setSelectedRange:NSMakeRange( [query length], [[textView string] length] - [query length] )];
[self trySiteAndCopyContent:NO]; [self trySiteWithAction:NO];
} }
return mutableResults; return mutableResults;
@ -216,14 +239,17 @@
- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector { - (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector {
if (commandSelector == @selector(cancel:)) { if (commandSelector == @selector(cancel:)) { // Escape without completion.
[self.window close]; [self.window close];
return YES; 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; return NO;
if (commandSelector == @selector(insertNewline:)) { }
[self trySiteAndCopyContent:YES]; if (commandSelector == @selector(insertNewline:)) { // Return without completion.
[self trySiteWithAction:YES];
return YES; return YES;
} }
@ -235,7 +261,7 @@
if (note.object != self.siteField) if (note.object != self.siteField)
return; return;
[self trySiteAndCopyContent:NO]; [self trySiteWithAction:NO];
} }
- (void)controlTextDidChange:(NSNotification *)note { - (void)controlTextDidChange:(NSNotification *)note {
@ -244,12 +270,18 @@
return; return;
// Update the site content as the site name changes. // Update the site content as the site name changes.
BOOL enterPressed = [[NSApp currentEvent] type] == NSKeyDown && if ([[NSApp currentEvent] type] == NSKeyDown &&
[[[NSApp currentEvent] charactersIgnoringModifiers] isEqualToString:@"\r"]; [[[NSApp currentEvent] charactersIgnoringModifiers] isEqualToString:@"\r"]) { // Return while completing.
[self trySiteAndCopyContent:enterPressed]; [self trySiteWithAction:YES];
if (enterPressed)
return; return;
}
if ([[NSApp currentEvent] type] == NSKeyDown &&
[[[NSApp currentEvent] charactersIgnoringModifiers] characterAtIndex:0] == 0x1b) { // Escape while completing.
_activeElementOID = nil;
[self trySiteWithAction:NO];
return;
}
if (self.siteFieldPreventCompletion) { if (self.siteFieldPreventCompletion) {
self.siteFieldPreventCompletion = NO; self.siteFieldPreventCompletion = NO;
@ -289,67 +321,75 @@
}]]; }]];
} }
- (void)trySiteAndCopyContent:(BOOL)copyContent { - (void)trySiteWithAction:(BOOL)doAction {
[self setContent:@""]; [self.backgroundQueue addOperationWithBlock:^{
[self.tipField setStringValue:@"Generating..."];
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0 ), ^{
NSString *content = [[self activeElementForThread].content description]; NSString *content = [[self activeElementForThread].content description];
if (!content) if (!content)
content = @""; content = @"";
if (copyContent) {
[[NSPasteboard generalPasteboard] declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil]; NSString *siteName = [self.siteField stringValue];
if (![[NSPasteboard generalPasteboard] setString:content forType:NSPasteboardTypeString]) { dbg(@"name: %@, action: %d", siteName, doAction);
wrn(@"Couldn't copy password to pasteboard."); 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; 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 setContent:content];
self.tipField.alphaValue = 1; 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."]; [self.tipField setStringValue:@"Hit ⌤ (ENTER) to copy the password."];
else { else {
[self.tipField setStringValue:@"Copied! Hit ⎋ (ESC) to close window."]; [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( dispatch_time( DISPATCH_TIME_NOW, (int64_t)(5.0f * NSEC_PER_SEC) ), dispatch_get_main_queue(), ^{
dispatch_after( popTime, dispatch_get_main_queue(), ^{
[NSAnimationContext beginGrouping]; [NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:0.2f]; [[NSAnimationContext currentContext] setDuration:0.2f];
[self.tipField.animator setAlphaValue:0]; [self.tipField.animator setAlphaValue:0];
[NSAnimationContext endGrouping]; [NSAnimationContext endGrouping];
} ); } );
} }
} ); }];
} ); }];
}
// For when the app should be able to create new sites. - (void)copyContent:(NSString *)content {
/*
else [[NSPasteboard generalPasteboard] declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil];
[[MPMacAppDelegate get].managedObjectContext performBlock:^{ if (![[NSPasteboard generalPasteboard] setString:content forType:NSPasteboardTypeString]) {
MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPElementGeneratedEntity class]) wrn(@"Couldn't copy password to pasteboard.");
inManagedObjectContext:[MPMacAppDelegate get].managedObjectContext]; return;
assert([element isKindOfClass:ClassFromMPElementType(element.type)]); }
assert([MPMacAppDelegate get].keyID);
NSManagedObjectContext *moc = [MPMacAppDelegate managedObjectContextForThreadIfReady];
element.name = siteName; MPElementEntity *activeElement = [self activeElementInContext:moc];
element.keyID = [MPMacAppDelegate get].keyID; [activeElement use];
[moc saveToStore];
NSString *description = [element.content description]; }
[element use];
- (void)createNewSite:(NSString *)siteName {
dispatch_async(dispatch_get_main_queue(), ^{
[self setContent:description]; [[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 @end

View File

@ -55,9 +55,11 @@
return; return;
__weak MPElementListAllViewController *wSelf = self; __weak MPElementListAllViewController *wSelf = self;
[self addElementNamed:[alert textFieldAtIndex:0].text completion:^(BOOL success) { [[MPiOSAppDelegate get] addElementNamed:[alert textFieldAtIndex:0].text completion:^(MPElementEntity *element) {
if (success) if (element) {
[wSelf.delegate didSelectElement:element];
[wSelf close:nil]; [wSelf close:nil];
}
}]; }];
} }
cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonOkay, nil]; cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonOkay, nil];

View File

@ -15,7 +15,6 @@
@property(readonly) NSDateFormatter *dateFormatter; @property(readonly) NSDateFormatter *dateFormatter;
- (void)updateData; - (void)updateData;
- (void)addElementNamed:(NSString *)siteName completion:(void (^)(BOOL success))completion;
- (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atTableIndexPath:(NSIndexPath *)indexPath; - (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atTableIndexPath:(NSIndexPath *)indexPath;
- (void)customTableViewUpdates; - (void)customTableViewUpdates;

View File

@ -23,48 +23,6 @@
[super viewDidLoad]; [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 { - (NSFetchedResultsController *)fetchedResultsControllerByLastUsed {
if (!_fetchedResultsControllerByLastUsed) { if (!_fetchedResultsControllerByLastUsed) {

View File

@ -9,6 +9,7 @@
#import "MPElementListSearchController.h" #import "MPElementListSearchController.h"
#import "MPMainViewController.h" #import "MPMainViewController.h"
#import "MPiOSAppDelegate.h" #import "MPiOSAppDelegate.h"
#import "MPAppDelegate_Store.h"
@interface MPElementListSearchController() @interface MPElementListSearchController()
@ -216,7 +217,11 @@
if (buttonIndex == [alert cancelButtonIndex]) if (buttonIndex == [alert cancelButtonIndex])
return; 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]; } cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonYes, nil];
} }