diff --git a/MasterPassword/ObjC/Mac/MPElementCollectionView.m b/MasterPassword/ObjC/Mac/MPElementCollectionView.m index 401adacf..25f18976 100644 --- a/MasterPassword/ObjC/Mac/MPElementCollectionView.m +++ b/MasterPassword/ObjC/Mac/MPElementCollectionView.m @@ -16,7 +16,6 @@ // Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. // -#import #import "MPElementCollectionView.h" #import "MPElementModel.h" #import "MPMacAppDelegate.h" @@ -75,7 +74,7 @@ NSAlert *alert = [NSAlert alertWithMessageText:@"Update Login Name" defaultButton:@"Update" alternateButton:@"Cancel" otherButton:nil - informativeTextWithFormat:@"Enter the login name for %@:", self.representedObject.site]; + informativeTextWithFormat:@"Enter the login name for %@:", self.representedObject.siteName]; NSTextField *passwordField = [[NSTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )]; [alert setAccessoryView:passwordField]; [alert layout]; @@ -88,7 +87,7 @@ NSAlert *alert = [NSAlert alertWithMessageText:@"Update Password" defaultButton:@"Update" alternateButton:@"Cancel" otherButton:nil - informativeTextWithFormat:@"Enter the new password for %@:", self.representedObject.site]; + informativeTextWithFormat:@"Enter the new password for %@:", self.representedObject.siteName]; NSSecureTextField *passwordField = [[NSSecureTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )]; [alert setAccessoryView:passwordField]; [alert layout]; @@ -101,9 +100,9 @@ NSAlert *alert = [NSAlert alertWithMessageText:@"Delete Site" defaultButton:@"Delete" alternateButton:@"Cancel" otherButton:nil - informativeTextWithFormat:@"Are you sure you want to delete the site: %@?", self.representedObject.site]; + informativeTextWithFormat:@"Are you sure you want to delete the site: %@?", self.representedObject.siteName]; [alert beginSheetModalForWindow:self.view.window modalDelegate:self - didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:MPAlertDeleteSite]; + didEndSelector:@selector( alertDidEnd:returnCode:contextInfo: ) contextInfo:MPAlertDeleteSite]; } - (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { diff --git a/MasterPassword/ObjC/Mac/MPElementModel.h b/MasterPassword/ObjC/Mac/MPElementModel.h index 09d2f982..a9fa61e2 100644 --- a/MasterPassword/ObjC/Mac/MPElementModel.h +++ b/MasterPassword/ObjC/Mac/MPElementModel.h @@ -20,7 +20,8 @@ @class MPElementEntity; @interface MPElementModel : NSObject -@property (nonatomic) NSString *site; + +@property (nonatomic) NSString *siteName; @property (nonatomic) MPElementType type; @property (nonatomic) NSString *typeName; @property (nonatomic) NSString *content; diff --git a/MasterPassword/ObjC/Mac/MPElementModel.m b/MasterPassword/ObjC/Mac/MPElementModel.m index 9d05ef2d..6f0b34b8 100644 --- a/MasterPassword/ObjC/Mac/MPElementModel.m +++ b/MasterPassword/ObjC/Mac/MPElementModel.m @@ -47,7 +47,7 @@ _entityOID = entity.objectID; self.algorithm = entity.algorithm; - self.site = entity.name; + self.siteName = entity.name; self.lastUsed = entity.lastUsed; self.loginName = entity.loginName; self.type = entity.type; diff --git a/MasterPassword/ObjC/Mac/MPPasswordDialogController.m b/MasterPassword/ObjC/Mac/MPPasswordDialogController.m index ca419579..d510a3de 100644 --- a/MasterPassword/ObjC/Mac/MPPasswordDialogController.m +++ b/MasterPassword/ObjC/Mac/MPPasswordDialogController.m @@ -200,6 +200,7 @@ #pragma mark - NSCollectionViewDelegate #pragma mark - NSTextFieldDelegate + - (void)doCommandBySelector:(SEL)commandSelector { if (commandSelector == @selector(insertNewline:)) @@ -410,9 +411,9 @@ NSUserNotification *notification = [NSUserNotification new]; notification.title = @"Password Copied"; if (selectedElement.loginName.length) - notification.subtitle = PearlString( @"%@ at %@", selectedElement.loginName, selectedElement.site ); + notification.subtitle = PearlString( @"%@ at %@", selectedElement.loginName, selectedElement.siteName ); else - notification.subtitle = selectedElement.site; + notification.subtitle = selectedElement.siteName; [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification]; } else { diff --git a/MasterPassword/ObjC/Mac/MPPasswordDialogController.xib b/MasterPassword/ObjC/Mac/MPPasswordDialogController.xib index 0d15062f..bcdee008 100644 --- a/MasterPassword/ObjC/Mac/MPPasswordDialogController.xib +++ b/MasterPassword/ObjC/Mac/MPPasswordDialogController.xib @@ -152,7 +152,7 @@ - + diff --git a/MasterPassword/ObjC/Mac/MPPasswordWindowController.h b/MasterPassword/ObjC/Mac/MPPasswordWindowController.h index 547fddcc..b2d6d37a 100644 --- a/MasterPassword/ObjC/Mac/MPPasswordWindowController.h +++ b/MasterPassword/ObjC/Mac/MPPasswordWindowController.h @@ -17,11 +17,22 @@ // #import +#import "MPElementModel.h" -@interface MPPasswordWindowController : NSWindowController +@interface MPPasswordWindowController : NSWindowController +@property(nonatomic, strong) NSMutableArray *elements; + +@property(nonatomic, weak) IBOutlet NSArrayController *elementsController; @property(nonatomic, weak) IBOutlet NSImageView *blurView; -@property(nonatomic, weak) IBOutlet NSTextField *userLabel; +@property(nonatomic, weak) IBOutlet NSTextField *inputLabel; @property(nonatomic, weak) IBOutlet NSSearchField *siteField; +@property(nonatomic, weak) IBOutlet NSSecureTextField *passwordField; +@property(nonatomic, weak) IBOutlet NSTableView *siteTable; +@property(nonatomic, weak) IBOutlet NSProgressIndicator *progressView; +@property(nonatomic, weak) IBOutlet NSButton *typeButton; +@property(nonatomic, weak) IBOutlet NSView *counterContainer; +@property(nonatomic, weak) IBOutlet NSStepper *counterStepper; +@property(nonatomic, weak) IBOutlet NSTextField *counterLabel; @end diff --git a/MasterPassword/ObjC/Mac/MPPasswordWindowController.m b/MasterPassword/ObjC/Mac/MPPasswordWindowController.m index 297e1242..6e1fb786 100644 --- a/MasterPassword/ObjC/Mac/MPPasswordWindowController.m +++ b/MasterPassword/ObjC/Mac/MPPasswordWindowController.m @@ -16,9 +16,15 @@ // Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. // +#import #import "MPPasswordWindowController.h" #import "MPMacAppDelegate.h" #import "MPAppDelegate_Store.h" +#import "MPElementModel.h" +#import "MPAppDelegate_Key.h" + +#define MPAlertIncorrectMP @"MPAlertIncorrectMP" +#define MPAlertCreateSite @"MPAlertCreateSite" @implementation MPPasswordWindowController @@ -39,9 +45,9 @@ [[NSNotificationCenter defaultCenter] addObserverForName:NSWindowDidBecomeKeyNotification object:self.window queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { [self fadeIn]; - [self.siteField becomeFirstResponder]; + [self updateUser]; }]; - [[NSNotificationCenter defaultCenter] addObserverForName:NSWindowDidResignKeyNotification object:self.window + [[NSNotificationCenter defaultCenter] addObserverForName:NSWindowDidResignMainNotification object:self.window queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { [self fadeOut]; }]; @@ -55,21 +61,260 @@ }]; } -- (void)updateUser { +#pragma mark - NSResponder - [MPMacAppDelegate managedObjectContextForMainThreadPerformBlock:^(NSManagedObjectContext *mainContext) { - MPUserEntity *mainActiveUser = [[MPMacAppDelegate get] activeUserInContext:mainContext]; - if (mainActiveUser) - self.userLabel.stringValue = strf( @"%@'s password for:", mainActiveUser.name ); - else - self.userLabel.stringValue = @""; - }]; +- (void)doCommandBySelector:(SEL)commandSelector { + + [self handleCommand:commandSelector]; +} + +#pragma mark - NSTextFieldDelegate + +- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector { + + if (control == self.passwordField) { + if (commandSelector == @selector( insertNewline: )) { + NSString *password = self.passwordField.stringValue; + [self.progressView startAnimation:self]; + [MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *moc) { + MPUserEntity *activeUser = [[MPMacAppDelegate get] activeUserInContext:moc]; + NSString *userName = activeUser.name; + BOOL success = [[MPMacAppDelegate get] signInAsUser:activeUser saveInContext:moc usingMasterPassword:password]; + + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + [self.progressView stopAnimation:self]; + if (!success) + [[NSAlert alertWithError:[NSError errorWithDomain:MPErrorDomain code:0 userInfo:@{ + NSLocalizedDescriptionKey : PearlString( @"Incorrect master password for user %@", userName ) + }]] beginSheetModalForWindow:self.window modalDelegate:self + didEndSelector:@selector( alertDidEnd:returnCode:contextInfo: ) contextInfo:MPAlertIncorrectMP]; + }]; + }]; + return YES; + } + } + + if (control == self.siteField) { + if (commandSelector == @selector( moveUp: )) { + [self.elementsController selectPrevious:self]; + return YES; + } + if (commandSelector == @selector( moveDown: )) { + [self.elementsController selectNext:self]; + return YES; + } + } + + return [self handleCommand:commandSelector]; +} + +- (void)controlTextDidChange:(NSNotification *)note { + + if (note.object == self.siteField) { + [self updateElements]; + + // Update the site content as the site name changes. +// if ([[NSApp currentEvent] type] == NSKeyDown && +// [[[NSApp currentEvent] charactersIgnoringModifiers] isEqualToString:@"\r"]) { // Return while completing. +// [self useSite]; +// return; +// } + +// if ([[NSApp currentEvent] type] == NSKeyDown && +// [[[NSApp currentEvent] charactersIgnoringModifiers] characterAtIndex:0] == 0x1b) { // Escape while completing. +// [self trySiteWithAction:NO]; +// return; +// } + } +} + +#pragma mark - NSTextViewDelegate + +- (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector { + + return [self handleCommand:commandSelector]; +} + +#pragma mark - NSTableViewDataSource + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { + + return (NSInteger)[self.elements count]; +} + +#pragma mark - NSAlert + +- (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { + + if (contextInfo == MPAlertIncorrectMP) { + [self close]; + return; + } + if (contextInfo == MPAlertCreateSite) { + switch (returnCode) { + case NSAlertFirstButtonReturn: { + // "Create" button. + [[MPMacAppDelegate get] addElementNamed:[self.siteField stringValue] completion:^(MPElementEntity *element) { + if (element) + PearlMainQueue( ^{ [self updateElements]; } ); + }]; + break; + } + case NSAlertThirdButtonReturn: + // "Cancel" button. + break; + default: + break; + } + } } #pragma mark - State +- (void)insertObject:(MPElementModel *)model inElementsAtIndex:(NSUInteger)index { + + [self.elements insertObject:model atIndex:index]; +} + +- (void)removeObjectFromElementsAtIndex:(NSUInteger)index { + + [self.elements removeObjectAtIndex:index]; +} + +- (MPElementModel *)selectedElement { + + return [self.elementsController.selectedObjects firstObject]; +} + #pragma mark - Private +- (BOOL)handleCommand:(SEL)commandSelector { + + if (commandSelector == @selector( insertNewline: )) { + [self useSite]; + return YES; + } + if (commandSelector == @selector( cancel: ) || commandSelector == @selector( cancelOperation: )) { + [self fadeOut]; + return YES; + } + + return NO; +} + +- (void)updateUser { + + [MPMacAppDelegate managedObjectContextForMainThreadPerformBlock:^(NSManagedObjectContext *mainContext) { + MPUserEntity *mainActiveUser = [[MPMacAppDelegate get] activeUserInContext:mainContext]; + if (mainActiveUser) { + if ([MPMacAppDelegate get].key) { + self.inputLabel.stringValue = strf( @"%@'s password for:", mainActiveUser.name ); + [self.passwordField setHidden:YES]; + [self.siteField setHidden:NO]; + [self.siteTable setHidden:NO]; + [self.siteField becomeFirstResponder]; + } + else { + self.inputLabel.stringValue = strf( @"Enter %@'s master password:", mainActiveUser.name ); + [self.passwordField setHidden:NO]; + [self.siteField setHidden:YES]; + [self.siteTable setHidden:YES]; + [self.passwordField becomeFirstResponder]; + } + } + else { + self.inputLabel.stringValue = @""; + [self.passwordField setHidden:YES]; + [self.siteField setHidden:YES]; + [self.siteTable setHidden:YES]; + } + + self.passwordField.stringValue = @""; + self.siteField.stringValue = @""; + [self updateElements]; + }]; +} + +- (void)updateElements { + + if (![MPMacAppDelegate get].key) { + self.elements = nil; + return; + } + + NSString *query = [self.siteField stringValue]; + [MPMacAppDelegate managedObjectContextPerformBlockAndWait:^(NSManagedObjectContext *context) { + NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPElementEntity class] )]; + fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"lastUsed" ascending:NO]]; + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND user == %@", + query, query, [[MPMacAppDelegate get] activeUserInContext:context]]; + + NSError *error = nil; + NSArray *siteResults = [context executeFetchRequest:fetchRequest error:&error]; + if (!siteResults) { + err( @"While fetching elements for completion: %@", error ); + return; + } + + NSMutableArray *newElements = [NSMutableArray arrayWithCapacity:[siteResults count]]; + for (MPElementEntity *element in siteResults) + [newElements addObject:[[MPElementModel alloc] initWithEntity:element]]; + self.elements = newElements; + }]; +} + +- (void)useSite { + + MPElementModel *selectedElement = [self selectedElement]; + if (selectedElement) { + // Performing action while content is available. Copy it. + [self copyContent:selectedElement.content]; + + [self close]; + + NSUserNotification *notification = [NSUserNotification new]; + notification.title = @"Password Copied"; + if (selectedElement.loginName.length) + notification.subtitle = PearlString( @"%@ at %@", selectedElement.loginName, selectedElement.siteName ); + else + notification.subtitle = selectedElement.siteName; + [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification]; + } + else { + NSString *siteName = [self.siteField stringValue]; + if ([siteName length]) + // Performing action without content but a site name is written. + [self createNewSite:siteName]; + } +} + +- (void)createNewSite:(NSString *)siteName { + + PearlMainQueue( ^{ + NSAlert *alert = [NSAlert new]; + [alert addButtonWithTitle:@"Create"]; + [alert addButtonWithTitle:@"Cancel"]; + [alert setMessageText:@"Create site?"]; + [alert setInformativeText:PearlString( @"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]; + } ); +} + +- (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; + } + + [MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *moc) { + [[self.selectedElement entityInContext:moc] use]; + [moc saveToStore]; + }]; +} + - (void)fadeIn { CGWindowID windowID = (CGWindowID)[self.window windowNumber]; @@ -92,11 +337,16 @@ self.blurView.image = smallImage; [self.window setFrame:self.window.screen.frame display:YES]; + [[NSAnimationContext currentContext] setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]]; [[self.window animator] setAlphaValue:1.0]; } - (void)fadeOut { + [[NSAnimationContext currentContext] setCompletionHandler:^{ + [self close]; + }]; + [[NSAnimationContext currentContext] setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]]; [[self.window animator] setAlphaValue:0.0]; } diff --git a/MasterPassword/ObjC/Mac/MPPasswordWindowController.xib b/MasterPassword/ObjC/Mac/MPPasswordWindowController.xib index 063eef45..d6362766 100644 --- a/MasterPassword/ObjC/Mac/MPPasswordWindowController.xib +++ b/MasterPassword/ObjC/Mac/MPPasswordWindowController.xib @@ -8,7 +8,16 @@ + + + + + + + + + @@ -17,14 +26,14 @@ - + - + - - + + @@ -44,21 +53,21 @@ - - + + - + - - + + - - + + - + @@ -72,17 +81,25 @@ - + - + - - + + + + + + + + + + @@ -98,15 +115,21 @@ + + + + + + - + - + - + - - - + + + + + + + + - + + + + -