diff --git a/MasterPassword/ObjC/MPAlgorithm.h b/MasterPassword/ObjC/MPAlgorithm.h index 622c2709..8bbf6b4f 100644 --- a/MasterPassword/ObjC/MPAlgorithm.h +++ b/MasterPassword/ObjC/MPAlgorithm.h @@ -37,6 +37,8 @@ - (NSString *)shortNameOfType:(MPElementType)type; - (NSString *)classNameOfType:(MPElementType)type; - (Class)classOfType:(MPElementType)type; +- (MPElementType)nextType:(MPElementType)type; +- (MPElementType)previousType:(MPElementType)type; - (NSString *)generateContentNamed:(NSString *)name ofType:(MPElementType)type withCounter:(NSUInteger)counter usingKey:(MPKey *)key; - (NSString *)storedContentForElement:(MPElementStoredEntity *)element usingKey:(MPKey *)key; diff --git a/MasterPassword/ObjC/MPAlgorithmV0.m b/MasterPassword/ObjC/MPAlgorithmV0.m index 40e746de..8693581a 100644 --- a/MasterPassword/ObjC/MPAlgorithmV0.m +++ b/MasterPassword/ObjC/MPAlgorithmV0.m @@ -127,7 +127,7 @@ return @"Device Private Password"; } - Throw(@"Type not supported: %d", type); + Throw(@"Type not supported: %lu", type); } - (NSString *)shortNameOfType:(MPElementType)type { @@ -161,7 +161,7 @@ return @"Device"; } - Throw(@"Type not supported: %d", type); + Throw(@"Type not supported: %lu", type); } - (NSString *)classNameOfType:(MPElementType)type { @@ -200,7 +200,43 @@ return [MPElementStoredEntity class]; } - Throw(@"Type not supported: %d", type); + Throw(@"Type not supported: %lu", type); +} + +- (MPElementType)nextType:(MPElementType)type { + + if (!type) + Throw(@"No type given."); + + switch (type) { + case MPElementTypeGeneratedMaximum: + return MPElementTypeStoredDevicePrivate; + case MPElementTypeGeneratedLong: + return MPElementTypeGeneratedMaximum; + case MPElementTypeGeneratedMedium: + return MPElementTypeGeneratedLong; + case MPElementTypeGeneratedBasic: + return MPElementTypeGeneratedMedium; + case MPElementTypeGeneratedShort: + return MPElementTypeGeneratedBasic; + case MPElementTypeGeneratedPIN: + return MPElementTypeGeneratedShort; + case MPElementTypeStoredPersonal: + return MPElementTypeGeneratedPIN; + case MPElementTypeStoredDevicePrivate: + return MPElementTypeStoredPersonal; + } + + Throw(@"Type not supported: %lu", type); +} + +- (MPElementType)previousType:(MPElementType)type { + + MPElementType previousType = type, nextType = type; + while ((nextType = [self nextType:nextType]) != type) + previousType = nextType; + + return previousType; } - (NSString *)generateContentNamed:(NSString *)name ofType:(MPElementType)type withCounter:(NSUInteger)counter usingKey:(MPKey *)key { @@ -264,13 +300,13 @@ case MPElementTypeGeneratedBasic: case MPElementTypeGeneratedShort: case MPElementTypeGeneratedPIN: { - NSAssert(NO, @"Cannot save content to element with generated type %d.", element.type); + NSAssert(NO, @"Cannot save content to element with generated type %lu.", element.type); break; } case MPElementTypeStoredPersonal: { NSAssert([element isKindOfClass:[MPElementStoredEntity class]], - @"Element with stored type %d is not an MPElementStoredEntity, but a %@.", element.type, [element class]); + @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", element.type, [element class]); NSData *encryptedContent = [[clearContent dataUsingEncoding:NSUTF8StringEncoding] encryptWithSymmetricKey:[elementKey subKeyOfLength:PearlCryptKeySize].keyData padding:YES]; @@ -279,7 +315,7 @@ } case MPElementTypeStoredDevicePrivate: { NSAssert([element isKindOfClass:[MPElementStoredEntity class]], - @"Element with stored type %d is not an MPElementStoredEntity, but a %@.", element.type, [element class]); + @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", element.type, [element class]); NSData *encryptedContent = [[clearContent dataUsingEncoding:NSUTF8StringEncoding] encryptWithSymmetricKey:[elementKey subKeyOfLength:PearlCryptKeySize].keyData padding:YES]; @@ -324,7 +360,7 @@ case MPElementTypeGeneratedShort: case MPElementTypeGeneratedPIN: { NSAssert([element isKindOfClass:[MPElementGeneratedEntity class]], - @"Element with generated type %d is not an MPElementGeneratedEntity, but a %@.", element.type, [element class]); + @"Element with generated type %lu is not an MPElementGeneratedEntity, but a %@.", element.type, [element class]); NSString *name = element.name; MPElementType type = element.type; @@ -346,7 +382,7 @@ case MPElementTypeStoredPersonal: { NSAssert([element isKindOfClass:[MPElementStoredEntity class]], - @"Element with stored type %d is not an MPElementStoredEntity, but a %@.", element.type, [element class]); + @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", element.type, [element class]); NSData *encryptedContent = ((MPElementStoredEntity *)element).contentObject; @@ -358,7 +394,7 @@ } case MPElementTypeStoredDevicePrivate: { NSAssert([element isKindOfClass:[MPElementStoredEntity class]], - @"Element with stored type %d is not an MPElementStoredEntity, but a %@.", element.type, [element class]); + @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", element.type, [element class]); NSDictionary *elementQuery = [self queryForDevicePrivateElementNamed:element.name]; NSData *encryptedContent = [PearlKeyChain dataOfItemForQuery:elementQuery]; @@ -387,7 +423,7 @@ case MPElementTypeStoredPersonal: { NSAssert([element isKindOfClass:[MPElementStoredEntity class]], - @"Element with stored type %d is not an MPElementStoredEntity, but a %@.", element.type, [element class]); + @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", element.type, [element class]); if ([importKey.keyID isEqualToData:elementKey.keyID]) ((MPElementStoredEntity *)element).contentObject = [protectedContent decodeBase64]; @@ -445,7 +481,7 @@ case MPElementTypeStoredPersonal: { NSAssert([element isKindOfClass:[MPElementStoredEntity class]], - @"Element with stored type %d is not an MPElementStoredEntity, but a %@.", element.type, [element class]); + @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", element.type, [element class]); result = [((MPElementStoredEntity *)element).contentObject encodeBase64]; break; } diff --git a/MasterPassword/ObjC/MPAppDelegate_Store.m b/MasterPassword/ObjC/MPAppDelegate_Store.m index f7120155..44427486 100644 --- a/MasterPassword/ObjC/MPAppDelegate_Store.m +++ b/MasterPassword/ObjC/MPAppDelegate_Store.m @@ -409,6 +409,8 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, newElement.loginName = element.loginName; [context deleteObject:element]; + // TODO: Dodgy... we're not saving consistently here. + // Either we should save regardless and change the method signature to saveInContext: or not save at all. [context saveToStore]; NSError *error; @@ -699,7 +701,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, [export appendFormat:@"%@ %8ld %8s %20s\t%@\n", [[NSDateFormatter rfc3339DateFormatter] stringFromDate:lastUsed], (long)uses, - [PearlString( @"%u:%lu", type, (unsigned long)version ) UTF8String], [name UTF8String], content + [PearlString( @"%lu:%lu", type, (unsigned long)version ) UTF8String], [name UTF8String], content ? content: @""]; } diff --git a/MasterPassword/ObjC/MPEntities.m b/MasterPassword/ObjC/MPEntities.m index d2d17b3d..ebac71ac 100644 --- a/MasterPassword/ObjC/MPEntities.m +++ b/MasterPassword/ObjC/MPEntities.m @@ -53,6 +53,8 @@ aType = [self.user defaultType]; if (!aType || aType == (MPElementType)NSNotFound) aType = MPElementTypeGeneratedLong; + if (![self isKindOfClass:[self.algorithm classOfType:aType]]) + Throw(@"This object's class does not support the type: %lu", aType); self.type_ = @(aType); } @@ -125,7 +127,7 @@ - (NSString *)debugDescription { - return PearlString( @"{%@: name=%@, user=%@, type=%d, uses=%ld, lastUsed=%@, version=%ld, loginName=%@, requiresExplicitMigration=%d}", + return PearlString( @"{%@: name=%@, user=%@, type=%lu, uses=%ld, lastUsed=%@, version=%ld, loginName=%@, requiresExplicitMigration=%d}", NSStringFromClass( [self class] ), self.name, self.user.name, self.type, (long)self.uses, self.lastUsed, (long)self.version, self.loginName, self.requiresExplicitMigration ); } diff --git a/MasterPassword/ObjC/MPTypes.h b/MasterPassword/ObjC/MPTypes.h index 3c026352..c9fd7949 100644 --- a/MasterPassword/ObjC/MPTypes.h +++ b/MasterPassword/ObjC/MPTypes.h @@ -8,27 +8,27 @@ #import "MPKey.h" -typedef enum { +typedef NS_ENUM(NSUInteger, MPElementContentType) { MPElementContentTypePassword, MPElementContentTypeNote, MPElementContentTypePicture, -} MPElementContentType; +}; -typedef enum { +typedef NS_ENUM(NSUInteger, MPElementTypeClass) { /** Generate the password. */ MPElementTypeClassGenerated = 1 << 4, /** Store the password. */ MPElementTypeClassStored = 1 << 5, -} MPElementTypeClass; +}; -typedef enum { +typedef NS_ENUM(NSUInteger, MPElementFeature) { /** Export the key-protected content data. */ MPElementFeatureExportContent = 1 << 10, /** Never export content. */ MPElementFeatureDevicePrivate = 1 << 11, -} MPElementFeature; +}; -typedef enum { +typedef NS_ENUM(NSUInteger, MPElementType) { MPElementTypeGeneratedMaximum = 0x0 | MPElementTypeClassGenerated | 0x0, MPElementTypeGeneratedLong = 0x1 | MPElementTypeClassGenerated | 0x0, MPElementTypeGeneratedMedium = 0x2 | MPElementTypeClassGenerated | 0x0, @@ -38,7 +38,7 @@ typedef enum { MPElementTypeStoredPersonal = 0x0 | MPElementTypeClassStored | MPElementFeatureExportContent, MPElementTypeStoredDevicePrivate = 0x1 | MPElementTypeClassStored | MPElementFeatureDevicePrivate, -} MPElementType; +}; #define MPErrorDomain @"MPErrorDomain" diff --git a/MasterPassword/ObjC/Mac/MPElementCollectionView.h b/MasterPassword/ObjC/Mac/MPElementCollectionView.h index 7a99d723..1d2ad402 100644 --- a/MasterPassword/ObjC/Mac/MPElementCollectionView.h +++ b/MasterPassword/ObjC/Mac/MPElementCollectionView.h @@ -17,9 +17,17 @@ // #import +@class MPElementModel; @interface MPElementCollectionView : NSCollectionViewItem -@property(nonatomic, weak) IBOutlet NSTextField *siteField; -@property(nonatomic, weak) IBOutlet NSTextField *contentField; +@property (nonatomic) MPElementModel *representedObject; +@property (nonatomic) NSString *typeTitle; +@property (nonatomic) NSString *loginNameTitle; +@property (nonatomic) NSString *counterTitle; + +- (IBAction)toggleType:(id)sender; +- (IBAction)setLoginName:(id)sender; +- (IBAction)incrementCounter:(id)sender; + @end diff --git a/MasterPassword/ObjC/Mac/MPElementCollectionView.m b/MasterPassword/ObjC/Mac/MPElementCollectionView.m index 001f2660..fc96ae7e 100644 --- a/MasterPassword/ObjC/Mac/MPElementCollectionView.m +++ b/MasterPassword/ObjC/Mac/MPElementCollectionView.m @@ -17,7 +17,237 @@ // #import "MPElementCollectionView.h" +#import "MPElementModel.h" +#import "MPMacAppDelegate.h" +#import "MPAppDelegate_Store.h" + +#define MPAlertChangeType @"MPAlertChangeType" +#define MPAlertChangeLogin @"MPAlertChangeLogin" +#define MPAlertChangeCounter @"MPAlertChangeCounter" +#define MPAlertChangeContent @"MPAlertChangeContent" @implementation MPElementCollectionView { + id _representedObjectObserver; } + +@dynamic representedObject; + +- (id)initWithCoder:(NSCoder *)coder { + + if (!(self = [super initWithCoder:coder])) + return nil; + + __weak MPElementCollectionView *wSelf = self; + _representedObjectObserver = [self addObserverBlock:^(NSString *keyPath, id object, NSDictionary *change, void *context) { + dispatch_async( dispatch_get_main_queue(), ^{ + dbg(@"updating login name of %@ to: %@", wSelf.representedObject.site, wSelf.representedObject.loginName); + wSelf.typeTitle = PearlString( @"Type:\n%@", wSelf.representedObject.typeName ); + wSelf.loginNameTitle = PearlString( @"Login Name:\n%@", wSelf.representedObject.loginName ); + + if (wSelf.representedObject.type & MPElementTypeClassGenerated) + wSelf.counterTitle = PearlString( @"Number:\n%@", wSelf.representedObject.counter ); + else if (wSelf.representedObject.type & MPElementTypeClassStored) + wSelf.counterTitle = PearlString( @"Update Password" ); + } ); + } forKeyPath:@"representedObject" options:0 context:nil]; + + return self; +} + +- (void)dealloc { + + if (_representedObjectObserver) + [self removeObserver:_representedObjectObserver forKeyPath:@"representedObject"]; +} + +- (IBAction)toggleType:(id)sender { + + id algorithm = self.representedObject.algorithm; + NSString *previousType = [algorithm nameOfType:[algorithm previousType:self.representedObject.type]]; + NSString *nextType = [algorithm nameOfType:[algorithm nextType:self.representedObject.type]]; + + [[NSAlert alertWithMessageText:@"Change Password Type" + defaultButton:nextType alternateButton:@"Cancel" otherButton:previousType + informativeTextWithFormat:@"Changing the password type for this site will cause the password to change.\n" + @"You will need to update your account with the new password.\n\n" + @"Changing back to the old type will restore your current password."] + beginSheetModalForWindow:self.view.window modalDelegate:self + didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:MPAlertChangeType]; +} + +- (IBAction)setLoginName:(id)sender { + + NSAlert *alert = [NSAlert alertWithMessageText:@"Update Login Name" + defaultButton:@"Update" alternateButton:@"Cancel" otherButton:nil + informativeTextWithFormat:@"Enter the login name for %@:", self.representedObject.site]; + NSTextField *passwordField = [[NSTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )]; + [alert setAccessoryView:passwordField]; + [alert layout]; + [passwordField becomeFirstResponder]; + [alert beginSheetModalForWindow:self.view.window modalDelegate:self + didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:MPAlertChangeLogin]; +} + +- (IBAction)incrementCounter:(id)sender { + + if (self.representedObject.type & MPElementTypeClassGenerated) { + [[NSAlert alertWithMessageText:@"Change Password Number" + defaultButton:@"New Password" alternateButton:@"Cancel" otherButton:@"Initial Password" + informativeTextWithFormat:@"Increasing the password number gives you a new password for the site.\n" + @"You will need to update your account with the new password.\n\n" + @"Changing back to the initial password will reset the password number to 1."] + beginSheetModalForWindow:self.view.window modalDelegate:self + didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:MPAlertChangeCounter]; + } + + else if (self.representedObject.type & MPElementTypeClassStored) { + NSAlert *alert = [NSAlert alertWithMessageText:@"Update Password" + defaultButton:@"Update" alternateButton:@"Cancel" otherButton:nil + informativeTextWithFormat:@"Enter the new password for %@:", self.representedObject.site]; + NSSecureTextField *passwordField = [[NSSecureTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )]; + [alert setAccessoryView:passwordField]; + [alert layout]; + [passwordField becomeFirstResponder]; + [alert beginSheetModalForWindow:self.view.window modalDelegate:self + didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:MPAlertChangeContent]; + } +} + +- (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { + + if (contextInfo == MPAlertChangeType) { + switch (returnCode) { + case NSAlertDefaultReturn: { + // "Next type" button. + [MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + MPElementEntity *element = [self.representedObject entityInContext:context]; + element = [[MPMacAppDelegate get] changeElement:element inContext:context + toType:[element.algorithm nextType:element.type]]; + [context saveToStore]; + + self.representedObject = [[MPElementModel alloc] initWithEntity:element]; + }]; + break; + } + + case NSAlertAlternateReturn: { + // "Cancel" button. + break; + } + + case NSAlertOtherReturn: { + // "Previous type" button. + [MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + MPElementEntity *element = [self.representedObject entityInContext:context]; + element = [[MPMacAppDelegate get] changeElement:element inContext:context + toType:[element.algorithm previousType:element.type]]; + [context saveToStore]; + + self.representedObject = [[MPElementModel alloc] initWithEntity:element]; + }]; + break; + } + + default: + break; + } + + return; + } + if (contextInfo == MPAlertChangeLogin) { + switch (returnCode) { + case NSAlertDefaultReturn: { + // "Update" button. + [MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + MPElementEntity *element = [self.representedObject entityInContext:context]; + element.loginName = [(NSTextField *)alert.accessoryView stringValue]; + [context saveToStore]; + + self.representedObject = [[MPElementModel alloc] initWithEntity:element]; + }]; + break; + } + + case NSAlertAlternateReturn: { + // "Cancel" button. + break; + } + + default: + break; + } + + return; + } + if (contextInfo == MPAlertChangeCounter) { + switch (returnCode) { + case NSAlertDefaultReturn: { + // "New Password" button. + [MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + MPElementEntity *element = [self.representedObject entityInContext:context]; + if ([element isKindOfClass:[MPElementGeneratedEntity class]]) { + MPElementGeneratedEntity *generatedElement = (MPElementGeneratedEntity *)element; + ++generatedElement.counter; + [context saveToStore]; + + self.representedObject = [[MPElementModel alloc] initWithEntity:element]; + } + }]; + break; + } + + case NSAlertAlternateReturn: { + // "Cancel" button. + break; + } + + case NSAlertOtherReturn: { + // "Initial Password" button. + [MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + MPElementEntity *element = [self.representedObject entityInContext:context]; + if ([element isKindOfClass:[MPElementGeneratedEntity class]]) { + MPElementGeneratedEntity *generatedElement = (MPElementGeneratedEntity *)element; + generatedElement.counter = 1; + [context saveToStore]; + + self.representedObject = [[MPElementModel alloc] initWithEntity:element]; + } + }]; + break; + } + + default: + break; + } + + return; + } + if (contextInfo == MPAlertChangeContent) { + switch (returnCode) { + case NSAlertDefaultReturn: { + // "Update" button. + [MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + MPElementEntity *element = [self.representedObject entityInContext:context]; + [element.algorithm saveContent:[(NSSecureTextField *)alert.accessoryView stringValue] + toElement:element usingKey:[MPMacAppDelegate get].key]; + [context saveToStore]; + + self.representedObject = [[MPElementModel alloc] initWithEntity:element]; + }]; + break; + } + + case NSAlertAlternateReturn: { + // "Cancel" button. + break; + } + + default: + break; + } + + return; + } +} + @end diff --git a/MasterPassword/ObjC/Mac/MPElementModel.h b/MasterPassword/ObjC/Mac/MPElementModel.h index f7abf918..da5ce71f 100644 --- a/MasterPassword/ObjC/Mac/MPElementModel.h +++ b/MasterPassword/ObjC/Mac/MPElementModel.h @@ -21,11 +21,14 @@ @interface MPElementModel : NSObject @property (nonatomic) NSString *site; -@property (nonatomic) NSString *type; +@property (nonatomic) MPElementType type; +@property (nonatomic) NSString *typeName; @property (nonatomic) NSString *content; @property (nonatomic) NSString *loginName; @property (nonatomic) NSNumber *uses; +@property (nonatomic) NSNumber *counter; @property (nonatomic) NSDate *lastUsed; +@property (nonatomic, strong) id algorithm; - (MPElementEntity *)entityForMainThread; - (MPElementEntity *)entityInContext:(NSManagedObjectContext *)moc; diff --git a/MasterPassword/ObjC/Mac/MPElementModel.m b/MasterPassword/ObjC/Mac/MPElementModel.m index 813d8d90..7b16cd6e 100644 --- a/MasterPassword/ObjC/Mac/MPElementModel.m +++ b/MasterPassword/ObjC/Mac/MPElementModel.m @@ -39,9 +39,12 @@ self.site = entity.name; self.lastUsed = entity.lastUsed; self.loginName = entity.loginName; - self.type = entity.typeName; + self.type = entity.type; + self.typeName = entity.typeName; self.uses = entity.uses_; + self.counter = @([entity isKindOfClass:[MPElementGeneratedEntity class]]? [(MPElementGeneratedEntity *)entity counter]: 0); self.content = [entity.algorithm resolveContentForElement:entity usingKey:[MPAppDelegate_Shared get].key]; + self.algorithm = entity.algorithm; self.entityOID = entity.objectID; return self; diff --git a/MasterPassword/ObjC/Mac/MPInitialWindow.xib b/MasterPassword/ObjC/Mac/MPInitialWindow.xib index b84ef004..0a97dac2 100644 --- a/MasterPassword/ObjC/Mac/MPInitialWindow.xib +++ b/MasterPassword/ObjC/Mac/MPInitialWindow.xib @@ -1,7 +1,6 @@ - @@ -16,7 +15,7 @@ - + diff --git a/MasterPassword/ObjC/Mac/MPPasswordWindowController.h b/MasterPassword/ObjC/Mac/MPPasswordWindowController.h index 57572780..649dc377 100644 --- a/MasterPassword/ObjC/Mac/MPPasswordWindowController.h +++ b/MasterPassword/ObjC/Mac/MPPasswordWindowController.h @@ -7,12 +7,13 @@ // #import +@class MPElementModel; -@interface MPPasswordWindowController : NSWindowController +@interface MPPasswordWindowController : NSWindowController @property(nonatomic, strong) NSMutableArray *elements; +@property(nonatomic, strong) NSIndexSet *elementSelectionIndexes; @property(nonatomic, weak) IBOutlet NSTextField *siteField; -@property(nonatomic, weak) IBOutlet NSTextField *tipField; @property(nonatomic, weak) IBOutlet NSView *contentContainer; @property(nonatomic, weak) IBOutlet NSTextField *userLabel; @property(nonatomic, weak) IBOutlet NSCollectionView *siteCollectionView; diff --git a/MasterPassword/ObjC/Mac/MPPasswordWindowController.m b/MasterPassword/ObjC/Mac/MPPasswordWindowController.m index 0b0ff0bc..7ffe6757 100644 --- a/MasterPassword/ObjC/Mac/MPPasswordWindowController.m +++ b/MasterPassword/ObjC/Mac/MPPasswordWindowController.m @@ -42,8 +42,6 @@ self.backgroundQueue = [NSOperationQueue new]; self.backgroundQueue.maxConcurrentOperationCount = 1; - [self.tipField setStringValue:@""]; - [self.userLabel setStringValue:PearlString( @"%@'s password for:", [[MPMacAppDelegate get] activeUserForMainThread].name )]; [[MPMacAppDelegate get] addObserverBlock:^(NSString *keyPath, id object, NSDictionary *change, void *context) { // [MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *moc) { @@ -160,7 +158,6 @@ return; [self.siteField setStringValue:@""]; - [self.tipField setStringValue:@""]; NSAlert *alert = [NSAlert alertWithMessageText:@"Master Password is locked." defaultButton:@"Unlock" alternateButton:@"Change" otherButton:@"Cancel" @@ -265,12 +262,49 @@ } } +#pragma mark - NSCollectionViewDelegate + +- (void)setElementSelectionIndexes:(NSIndexSet *)elementSelectionIndexes { + + // First reset bounds. + PearlMainThread(^{ + NSUInteger selectedIndex = self.elementSelectionIndexes.firstIndex; + if (selectedIndex != NSNotFound && selectedIndex < self.elements.count) + [[self selectedView].animator setBoundsOrigin:NSZeroPoint]; + } ); + + _elementSelectionIndexes = elementSelectionIndexes; +} + +#pragma mark - NSTextFieldDelegate + - (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector { if (commandSelector == @selector(cancel:)) { // Escape without completion. [self close]; return YES; } + if (self.elements.count) { + if (commandSelector == @selector(moveUp:)) { + self.elementSelectionIndexes = + [NSIndexSet indexSetWithIndex:(self.elementSelectionIndexes.firstIndex - 1 + self.elements.count) % self.elements.count]; + return NO; + } + if (commandSelector == @selector(moveDown:)) { + self.elementSelectionIndexes = + [NSIndexSet indexSetWithIndex:(self.elementSelectionIndexes.firstIndex + 1) % self.elements.count]; + return NO; + } + if (commandSelector == @selector(moveLeft:)) { + [[self selectedView].animator setBoundsOrigin:NSZeroPoint]; + return NO; + } + if (commandSelector == @selector(moveRight:)) { + NSBox *selectedView = [self selectedView]; + [selectedView.animator setBoundsOrigin:NSMakePoint( selectedView.bounds.size.width / 2, 0 )]; + return NO; + } + } // if ((self.siteFieldPreventCompletion = [NSStringFromSelector( commandSelector ) hasPrefix:@"delete"])) { // Backspace any time. // _activeElementOID = nil; // [self trySiteWithAction:NO]; @@ -278,7 +312,7 @@ // } if (commandSelector == @selector(insertNewline:)) { // Return without completion. [self useSite]; - return YES; + return NO; } return NO; @@ -305,6 +339,8 @@ [self updateElements]; } +#pragma mark - Private + - (void)updateElements { NSString *query = [self.siteField.currentEditor string]; if (![query length] || ![MPMacAppDelegate get].key) { @@ -327,17 +363,32 @@ for (MPElementEntity *element in siteResults) [newElements addObject:[[MPElementModel alloc] initWithEntity:element]]; self.elements = newElements; + if (!self.selectedElement && [newElements count]) + self.elementSelectionIndexes = [NSIndexSet indexSetWithIndex:0]; } }]; } -- (MPElementModel *)selectedElement { +- (NSBox *)selectedView { - NSIndexSet *selectedIndexes = self.siteCollectionView.selectionIndexes; - if (!selectedIndexes.count) + NSUInteger selectedIndex = self.elementSelectionIndexes.firstIndex; + if (selectedIndex == NSNotFound || selectedIndex >= self.elements.count) return nil; - return (MPElementModel *)self.elements[selectedIndexes.firstIndex]; + return (NSBox *)[self.siteCollectionView itemAtIndex:selectedIndex].view; +} + +- (MPElementModel *)selectedElement { + + if (!self.elementSelectionIndexes.count) + return nil; + + return (MPElementModel *)self.elements[self.elementSelectionIndexes.firstIndex]; +} + +- (void)setSelectedElement:(MPElementModel *)element { + + self.elementSelectionIndexes = [NSIndexSet indexSetWithIndex:[self.elements indexOfObject:element]]; } - (void)useSite { @@ -346,60 +397,22 @@ if (selectedElement) { // Performing action while content is available. Copy it. [self copyContent:selectedElement.content]; - - self.tipField.alphaValue = 1; - self.tipField.stringValue = @"Copied! Hit ⎋ (ESC) to close window."; - 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]; + NSBox *selectedView = [self selectedView]; + dispatch_async( dispatch_get_main_queue(), ^{ + [selectedView.animator setFillColor:[NSColor controlHighlightColor]]; + dispatch_after( dispatch_time( DISPATCH_TIME_NOW, (int64_t)(0.3f * NSEC_PER_SEC) ), dispatch_get_main_queue(), ^{ + dispatch_async( dispatch_get_main_queue(), ^{ + [selectedView.animator setFillColor:[NSColor selectedControlColor]]; } ); + } ); + } ); } else { NSString *siteName = [self.siteField stringValue]; - if ([siteName length]) { + if ([siteName length]) // Performing action without content but a site name is written. [self createNewSite:siteName]; - [self.tipField setStringValue:@""]; - } } - -// [MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { -// MPElementEntity *activeElement = [self activeElementInContext:context]; -// NSString *typeName = [activeElement typeShortName]; -// [activeElement.algorithm resolveContentForElement:activeElement usingKey:[MPAppDelegate_Shared get].key result: -// ^(NSString *result) { -// BOOL actionHandled = NO; -// if (doAction) { -// } -// -// [[NSOperationQueue mainQueue] addOperationWithBlock:^{ -// [self setContent:result]; -// if (actionHandled) -// [self.tipField setStringValue:@""]; -// else if (![result length]) { -// 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_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]; -// } ); -// } -// }]; -// }]; -// }]; } - (void)copyContent:(NSString *)content { @@ -427,6 +440,8 @@ }]; } +#pragma mark - KVO + - (void)insertObject:(MPElementModel *)model inElementsAtIndex:(NSUInteger)index { [self.elements insertObject:model atIndex:index]; diff --git a/MasterPassword/ObjC/Mac/MPPasswordWindowController.xib b/MasterPassword/ObjC/Mac/MPPasswordWindowController.xib index bb49c64a..4092a307 100644 --- a/MasterPassword/ObjC/Mac/MPPasswordWindowController.xib +++ b/MasterPassword/ObjC/Mac/MPPasswordWindowController.xib @@ -10,7 +10,6 @@ - @@ -19,20 +18,20 @@ - + - - + + - + - + - + @@ -46,21 +45,8 @@ - - - - - - - - - - - - - - + @@ -72,20 +58,21 @@ - - + + - + - + + @@ -96,18 +83,15 @@ - - - - + @@ -127,98 +111,175 @@ - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - site - type - content - loginName - uses - lastUsed - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSNegateBoolean + + + + \ No newline at end of file diff --git a/MasterPassword/ObjC/Mac/MasterPassword-Mac.xcodeproj/project.pbxproj b/MasterPassword/ObjC/Mac/MasterPassword-Mac.xcodeproj/project.pbxproj index 9eea64e8..93c93ab3 100644 --- a/MasterPassword/ObjC/Mac/MasterPassword-Mac.xcodeproj/project.pbxproj +++ b/MasterPassword/ObjC/Mac/MasterPassword-Mac.xcodeproj/project.pbxproj @@ -13,7 +13,6 @@ 93D39C5789EFA607CF788082 /* MPElementModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39E73BF5CBF8E5B005CD3 /* MPElementModel.m */; }; 93D39C7C2BE7C0E0763B0177 /* MPElementCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D394495528B10D1B61A2C3 /* MPElementCollectionView.m */; }; 93D39E281E3658B30550CB55 /* NSDictionary+Indexing.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39AA1EE2E1E7B81372240 /* NSDictionary+Indexing.m */; }; - DA0933CA1747A56A00DE1CEF /* MPInitialWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = DA0933C91747A56A00DE1CEF /* MPInitialWindow.xib */; }; DA0933CC1747AD2D00DE1CEF /* shot-laptop-leaning-iphone.png in Resources */ = {isa = PBXBuildFile; fileRef = DA0933CB1747AD2D00DE1CEF /* shot-laptop-leaning-iphone.png */; }; DA0933D01747B91B00DE1CEF /* appstore.png in Resources */ = {isa = PBXBuildFile; fileRef = DA0933CF1747B91B00DE1CEF /* appstore.png */; }; DA16B33F170661D4000A0EAB /* libUbiquityStoreManager.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DA4425CB1557BED40052177D /* libUbiquityStoreManager.a */; }; @@ -204,6 +203,7 @@ DAEB942718AB0FFD000490CC /* sha256.h in Headers */ = {isa = PBXBuildFile; fileRef = DAEB93D618AB0FFD000490CC /* sha256.h */; }; DAEB942818AB0FFD000490CC /* sysendian.h in Headers */ = {isa = PBXBuildFile; fileRef = DAEB93D718AB0FFD000490CC /* sysendian.h */; }; DAEB942918AB0FFD000490CC /* warn.h in Headers */ = {isa = PBXBuildFile; fileRef = DAEB93D818AB0FFD000490CC /* warn.h */; }; + DAEB942E18B47FB3000490CC /* MPInitialWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = DA0933C91747A56A00DE1CEF /* MPInitialWindow.xib */; }; DAFE4A1315039824003ABA7C /* NSObject+PearlExport.h in Headers */ = {isa = PBXBuildFile; fileRef = DAFE45D815039823003ABA7C /* NSObject+PearlExport.h */; }; DAFE4A1415039824003ABA7C /* NSObject+PearlExport.m in Sources */ = {isa = PBXBuildFile; fileRef = DAFE45D915039823003ABA7C /* NSObject+PearlExport.m */; }; DAFE4A1515039824003ABA7C /* NSString+PearlNSArrayFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = DAFE45DA15039823003ABA7C /* NSString+PearlNSArrayFormat.h */; }; @@ -1452,6 +1452,7 @@ DACA27231705DF81002C6C22 /* avatar-15@2x.png in Resources */, DACA27241705DF81002C6C22 /* avatar-5@2x.png in Resources */, DACA27251705DF81002C6C22 /* avatar-6.png in Resources */, + DAEB942E18B47FB3000490CC /* MPInitialWindow.xib in Resources */, DACA27261705DF81002C6C22 /* avatar-6@2x.png in Resources */, DACA27271705DF81002C6C22 /* avatar-16@2x.png in Resources */, DACA27281705DF81002C6C22 /* avatar-10.png in Resources */, @@ -1486,7 +1487,6 @@ DA5E5D0A1724A667003798D8 /* InfoPlist.strings in Resources */, DA5E5D0B1724A667003798D8 /* MainMenu.xib in Resources */, DA5E5D551724F9C8003798D8 /* MasterPassword.iconset in Resources */, - DA0933CA1747A56A00DE1CEF /* MPInitialWindow.xib in Resources */, DA0933CC1747AD2D00DE1CEF /* shot-laptop-leaning-iphone.png in Resources */, DA0933D01747B91B00DE1CEF /* appstore.png in Resources */, );