From 424479dada9092e103d0ecc0de34affc38ce623d Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sat, 5 May 2012 00:15:51 +0200 Subject: [PATCH] Usability improvements to OS X MP app. [ADDED] OS X: A status item to activate the MP window. [ADDED] OS X: A global hotkey (cmd-ctrl-p) to activate the MP window. [ADDED] OS X: Make the MP window dismissable by hitting Esc. [ADDED] OS X: Copy the site content by hitting Enter. [FIXED] OS X: Make the password field first responder. [FIXED] OS X: Don't pop the password window multiple times if the application gets activated while the key isn't set yet. [IMPROVED] OS X: Remove the MP icon from the dock. --- MasterPassword-Mac.xcodeproj/project.pbxproj | 6 +- MasterPassword/Mac/MPAppDelegate.h | 1 + MasterPassword/Mac/MPAppDelegate.m | 57 ++- .../Mac/MPPasswordWindowController.h | 3 +- .../Mac/MPPasswordWindowController.m | 138 +++--- .../Mac/MPPasswordWindowController.xib | 432 ++++++++++-------- MasterPassword/Mac/MasterPassword-Info.plist | 2 + 7 files changed, 386 insertions(+), 253 deletions(-) diff --git a/MasterPassword-Mac.xcodeproj/project.pbxproj b/MasterPassword-Mac.xcodeproj/project.pbxproj index 3853fe19..ffbfa7ff 100644 --- a/MasterPassword-Mac.xcodeproj/project.pbxproj +++ b/MasterPassword-Mac.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + DA44255715546C580052177D /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA44255615546C570052177D /* Carbon.framework */; }; DA600BEB150420AC008E9AB6 /* MPPasswordWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = DA600BE9150420AC008E9AB6 /* MPPasswordWindowController.m */; }; DA600BEC150420AC008E9AB6 /* MPPasswordWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = DA600BEA150420AC008E9AB6 /* MPPasswordWindowController.xib */; }; DA600C2D150565FC008E9AB6 /* MPConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = DA600C2A150565FC008E9AB6 /* MPConfig.m */; }; @@ -1430,6 +1431,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + DA44255615546C570052177D /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; DA600BE8150420AC008E9AB6 /* MPPasswordWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPPasswordWindowController.h; sourceTree = ""; }; DA600BE9150420AC008E9AB6 /* MPPasswordWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPPasswordWindowController.m; sourceTree = ""; }; DA600BEA150420AC008E9AB6 /* MPPasswordWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MPPasswordWindowController.xib; sourceTree = ""; }; @@ -3235,6 +3237,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DA44255715546C580052177D /* Carbon.framework in Frameworks */, DADEA5D51503EEDF00FD084E /* Security.framework in Frameworks */, DAFE4A6415039CDC003ABA7C /* Pearl.dylib in Frameworks */, DAB8D98D150374AD00CED3BC /* Cocoa.framework in Frameworks */, @@ -3982,7 +3985,6 @@ DAB8D97D150374AC00CED3BC = { isa = PBXGroup; children = ( - DADEA5D31503EEA700FD084E /* Security.framework */, DAB8D992150374AD00CED3BC /* MasterPassword */, DA600C5915057F0F008E9AB6 /* Resources */, DAB8D9DA1503940100CED3BC /* Pearl */, @@ -4007,6 +4009,8 @@ DAB8D98B150374AD00CED3BC /* Frameworks */ = { isa = PBXGroup; children = ( + DA44255615546C570052177D /* Carbon.framework */, + DADEA5D31503EEA700FD084E /* Security.framework */, DAB8D98C150374AD00CED3BC /* Cocoa.framework */, DAB8D98E150374AD00CED3BC /* Other Frameworks */, ); diff --git a/MasterPassword/Mac/MPAppDelegate.h b/MasterPassword/Mac/MPAppDelegate.h index ec75038a..21cb2bc7 100644 --- a/MasterPassword/Mac/MPAppDelegate.h +++ b/MasterPassword/Mac/MPAppDelegate.h @@ -12,6 +12,7 @@ @interface MPAppDelegate : NSObject @property (assign) IBOutlet NSWindow *window; +@property (strong) NSStatusItem *statusItem; - (IBAction)saveAction:(id)sender; diff --git a/MasterPassword/Mac/MPAppDelegate.m b/MasterPassword/Mac/MPAppDelegate.m index b230c905..26c66d5c 100644 --- a/MasterPassword/Mac/MPAppDelegate.m +++ b/MasterPassword/Mac/MPAppDelegate.m @@ -9,16 +9,20 @@ #import "MPAppDelegate_Key.h" #import "MPConfig.h" #import "MPElementEntity.h" +#import @interface MPAppDelegate () @property (readwrite, strong, nonatomic) MPPasswordWindowController *passwordWindow; +- (void)activate; + @end @implementation MPAppDelegate -@synthesize window = _window; +@synthesize window; +@synthesize statusItem; @synthesize passwordWindow; @dynamic persistentStoreCoordinator, managedObjectModel, managedObjectContext; @@ -26,19 +30,57 @@ @synthesize keyHash; @synthesize keyHashHex; +static EventHotKeyID MPShowHotKey = { .signature = 'show', .id = 1 }; + + (void)initialize { [MPConfig get]; #ifdef DEBUG - [PearlLogger get].autoprintLevel = PearlLogLevelTrace - ; + [PearlLogger get].autoprintLevel = PearlLogLevelTrace; #endif } +static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData){ + + // Extract the hotkey ID. + EventHotKeyID hotKeyID; + GetEventParameter(theEvent,kEventParamDirectObject,typeEventHotKeyID, + NULL,sizeof(hotKeyID),NULL,&hotKeyID); + + // Check which hotkey this was. + if (hotKeyID.signature == MPShowHotKey.signature && hotKeyID.id == MPShowHotKey.id) { + [((__bridge MPAppDelegate *)userData) activate]; + return noErr; + } + + return eventNotHandledErr; +} + +- (void)activate { + + [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; +} + - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { - [self managedObjectContext]; + // Status item. + self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; + self.statusItem.title = @"•••"; + self.statusItem.highlightMode = YES; + self.statusItem.target = self; + self.statusItem.action = @selector(activate); + + // Global hotkey. + EventHotKeyRef hotKeyRef; + EventTypeSpec hotKeyEvents[1] = { { .eventClass = kEventClassKeyboard, .eventKind = kEventHotKeyPressed } }; + OSStatus status = InstallApplicationEventHandler(NewEventHandlerUPP(MPHotKeyHander), GetEventTypeCount(hotKeyEvents), hotKeyEvents, + (__bridge void *)self, NULL); + if(status != noErr) + err(@"Error installing application event handler: %d", status); + status = RegisterEventHotKey(35 /* p */, controlKey + cmdKey, MPShowHotKey, GetApplicationEventTarget(), 0, &hotKeyRef); + if(status != noErr) + err(@"Error registering hotkey: %d", status); } - (void)applicationDidBecomeActive:(NSNotification *)notification { @@ -59,13 +101,16 @@ if (!self.key) // Ask the user to set the key through his master password. dispatch_async(dispatch_get_main_queue(), ^{ + if (self.key) + return; + NSAlert *alert = [NSAlert alertWithMessageText:@"Master Password is locked." defaultButton:@"Unlock" alternateButton:@"Change" otherButton:@"Quit" informativeTextWithFormat:@"Your master password is required to unlock the application."]; - NSTextField *passwordField = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 200, 22)]; + NSSecureTextField *passwordField = [[NSSecureTextField alloc] initWithFrame:NSMakeRect(0, 0, 200, 22)]; [alert setAccessoryView:passwordField]; - [passwordField becomeFirstResponder]; [alert layout]; + [passwordField becomeFirstResponder]; do { NSInteger button = [alert runModal]; diff --git a/MasterPassword/Mac/MPPasswordWindowController.h b/MasterPassword/Mac/MPPasswordWindowController.h index 237b9a3b..9c83d3a3 100644 --- a/MasterPassword/Mac/MPPasswordWindowController.h +++ b/MasterPassword/Mac/MPPasswordWindowController.h @@ -9,8 +9,9 @@ #import @interface MPPasswordWindowController : NSWindowController + @property (weak) IBOutlet NSTextField *siteField; @property (weak) IBOutlet NSTextField *contentField; -- (IBAction)empty:(id)sender; +@property (weak) IBOutlet NSTextField *tipField; @end diff --git a/MasterPassword/Mac/MPPasswordWindowController.m b/MasterPassword/Mac/MPPasswordWindowController.m index 198df4ff..404fc7be 100644 --- a/MasterPassword/Mac/MPPasswordWindowController.m +++ b/MasterPassword/Mac/MPPasswordWindowController.m @@ -22,10 +22,17 @@ @synthesize oldSiteName, siteResults; @synthesize siteField; @synthesize contentField; +@synthesize tipField; - (void)windowDidLoad { [self.contentField setStringValue:@""]; + [self.tipField setStringValue:@""]; + + [[NSNotificationCenter defaultCenter] addObserverForName:NSWindowDidBecomeKeyNotification object:self.window queue:nil + usingBlock:^(NSNotification *note) { + [self.siteField selectText:self]; + }]; [[NSNotificationCenter defaultCenter] addObserverForName:NSWindowWillCloseNotification object:self.window queue:nil usingBlock:^(NSNotification *note) { [[NSApplication sharedApplication] hide:self]; @@ -35,6 +42,10 @@ NSString *newSiteName = [self.siteField stringValue]; BOOL shouldComplete = [self.oldSiteName length] < [newSiteName length]; self.oldSiteName = newSiteName; + + if ([self trySite]) + shouldComplete = NO; + if (shouldComplete) [[[note userInfo] objectForKey:@"NSFieldEditor"] complete:nil]; }]; @@ -45,9 +56,9 @@ - (NSArray *)control:(NSControl *)control textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index { NSString *query = [[control stringValue] substringWithRange:charRange]; + if (![query length] || ![MPAppDelegate get].keyHashHex) + return nil; - assert(query); - assert([MPAppDelegate get].keyHashHex); NSFetchRequest *fetchRequest = [MPAppDelegate.managedObjectModel fetchRequestFromTemplateWithName:@"MPElements" substitutionVariables:[NSDictionary dictionaryWithObjectsAndKeys: @@ -66,70 +77,91 @@ if (self.siteResults) for (MPElementEntity *element in self.siteResults) [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. return mutableResults; } - (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector { - dbg(@"Selector = %@", NSStringFromSelector(commandSelector)); + if (commandSelector == @selector(cancel:)) + [self.window close]; + if (commandSelector == @selector(insertNewline:) && [[self.contentField stringValue] length]) { + [[NSPasteboard generalPasteboard] declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil]; + if ([[NSPasteboard generalPasteboard] setString:[self.contentField stringValue] forType:NSPasteboardTypeString]) { + self.tipField.alphaValue = 1; + [self.tipField setStringValue:@"Copied!"]; + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 3.0f * NSEC_PER_SEC); + dispatch_after(popTime, dispatch_get_main_queue(), ^{ + [NSAnimationContext beginGrouping]; + [[NSAnimationContext currentContext] setDuration:0.2f]; + [self.tipField.animator setAlphaValue:0]; + [NSAnimationContext endGrouping]; + }); + return YES; + } else + wrn(@"Couldn't copy password to pasteboard."); + } return NO; } - (void)controlTextDidEndEditing:(NSNotification *)obj { - if (obj.object == self.siteField) { - NSString *siteName = [self.siteField stringValue]; - - MPElementEntity *result = nil; - for (MPElementEntity *element in self.siteResults) - if ([element.name isEqualToString:siteName]) { - result = element; - break; - } - - if (result) - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ - NSString *description = [result description]; - [result use]; - - dispatch_async(dispatch_get_main_queue(), ^{ - [self.contentField setStringValue:description]; - }); - }); - - // For when the app should be able to create new sites. -// else -// [[MPAppDelegate get].managedObjectContext performBlock:^{ -// MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPElementGeneratedEntity class]) -// inManagedObjectContext:[MPAppDelegate get].managedObjectContext]; -// assert([element isKindOfClass:ClassFromMPElementType(element.type)]); -// assert([MPAppDelegate get].keyHashHex); -// -// element.name = siteName; -// element.mpHashHex = [MPAppDelegate get].keyHashHex; -// -// NSString *description = [element description]; -// [element use]; -// -// dispatch_async(dispatch_get_main_queue(), ^{ -// [self.contentField setStringValue:description? description: @""]; -// }); -// }]; - } + if (obj.object == self.siteField) + [self trySite]; } -- (IBAction)empty:(id)sender { - - for(NSEntityDescription *entity in [[MPAppDelegate managedObjectModel] entities]) { - NSFetchRequest *request = [NSFetchRequest new]; - [request setEntity:entity]; - NSError *error; - NSArray *results = [[MPAppDelegate managedObjectContext] executeFetchRequest:request error:&error]; - for(NSManagedObject *o in results) { - [[MPAppDelegate managedObjectContext] deleteObject:o]; - } +- (BOOL)trySite { + + MPElementEntity *result = [self findElement]; + if (!result) { + [self.contentField setStringValue:@""]; + [self.tipField setStringValue:@""]; + return NO; } + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + NSString *description = [result description]; + [result use]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self.contentField setStringValue:description]; + [self.tipField setStringValue:@"Hit enter to copy the password."]; + self.tipField.alphaValue = 1; + }); + }); + + // For when the app should be able to create new sites. + /* + else + [[MPAppDelegate get].managedObjectContext performBlock:^{ + MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPElementGeneratedEntity class]) + inManagedObjectContext:[MPAppDelegate get].managedObjectContext]; + assert([element isKindOfClass:ClassFromMPElementType(element.type)]); + assert([MPAppDelegate get].keyHashHex); + + element.name = siteName; + element.mpHashHex = [MPAppDelegate get].keyHashHex; + + NSString *description = [element description]; + [element use]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self.contentField setStringValue:description? description: @""]; + }); + }]; + */ + + return YES; } + +- (MPElementEntity *)findElement { + + for (MPElementEntity *element in self.siteResults) + if ([element.name isEqualToString:[self.siteField stringValue]]) + return element; + + return nil; +} + @end diff --git a/MasterPassword/Mac/MPPasswordWindowController.xib b/MasterPassword/Mac/MPPasswordWindowController.xib index 56b3ac85..6184cee0 100644 --- a/MasterPassword/Mac/MPPasswordWindowController.xib +++ b/MasterPassword/Mac/MPPasswordWindowController.xib @@ -12,13 +12,11 @@ NSTextField - NSView - NSWindowTemplate - NSCustomObject - IBNSLayoutConstraint - NSButtonCell - NSButton NSTextFieldCell + NSWindowTemplate + NSView + IBNSLayoutConstraint + NSCustomObject com.apple.InterfaceBuilder.CocoaPlugin @@ -38,15 +36,15 @@ NSApplication - 8215 + 8209 2 - {{600, 530}, {480, 134}} + {{600, 530}, {480, 159}} 611845120 Master Password NSPanel - {480, 134} + {480, 320} {480, 134} @@ -55,9 +53,10 @@ 268 - {{140, 92}, {200, 22}} + {{140, 117}, {200, 22}} - + + 2 _NS:9 YES @@ -70,16 +69,15 @@ 13 1044 - Enter site name X + Site _NS:9 - YES 1 6 System textBackgroundColor - + 3 MQA @@ -98,8 +96,10 @@ 268 - {{17, 20}, {446, 64}} + {{17, 45}, {446, 64}} + + 2 _NS:9 {250, 750} @@ -115,7 +115,7 @@ _NS:9 - + 6 System controlColor @@ -130,33 +130,35 @@ - + 268 - {{384, 85}, {82, 32}} + {{17, 20}, {446, 17}} - + 2 - _NS:9 + _NS:1505 YES - - 67239424 - 134217728 - Empty + + 68288064 + 138413056 + Hit enter to copy the password. - _NS:9 - - -2038284033 - 129 - - - 200 - 25 + _NS:1505 + + + + 6 + System + controlLightHighlightColor + + - {480, 134} + {480, 159} + YES 2 @@ -164,7 +166,7 @@ {{0, 0}, {1680, 1028}} {480, 153} - {480, 153} + {480, 339} YES @@ -195,12 +197,12 @@ 42 - - empty: + + tipField - + - 48 + 95 @@ -210,6 +212,14 @@ 39 + + + initialFirstResponder + + + + 49 + delegate @@ -257,38 +267,6 @@ 23 - - - 5 - 0 - - 5 - 1 - - 20 - - 1000 - 8 - 29 - 3 - - - - - 9 - 0 - - 9 - 1 - - 0.0 - - 1000 - 6 - 24 - 2 - - 3 @@ -337,54 +315,6 @@ 3 - - - 6 - 0 - - 6 - 1 - - 20 - - 1000 - 8 - 29 - 3 - - - - - 3 - 0 - - 3 - 1 - - 20 - - 1000 - 8 - 29 - 3 - - - - - 4 - 0 - - 4 - 1 - - 20 - - 1000 - 9 - 40 - 3 - - 6 @@ -419,12 +349,44 @@ - - + + + 5 + 0 + + 5 + 1 + + 20 + + 1000 + 8 + 29 + 3 + + + + + + 9 + 0 + + 9 + 1 + + 0.0 + + 1000 + 6 + 24 + 2 + + + 6 0 - + 6 1 @@ -436,35 +398,73 @@ 3 - - - 11 + + + 4 0 - - 11 + + 4 1 - - 0.0 + + 20 1000 - 6 - 24 - 2 + 8 + 29 + 3 + + + + + 6 + 0 + + 6 + 1 + + 20 + + 1000 + 8 + 29 + 3 + + + + + 3 + 0 + + 3 + 1 + + 20 + + 1000 + 8 + 29 + 3 + + + + + 5 + 0 + + 5 + 1 + + 20 + + 1000 + 8 + 29 + 3 - - 24 - - - - - 25 - - - 26 @@ -480,21 +480,6 @@ - - 29 - - - - - 30 - - - - - 31 - - - 32 @@ -517,6 +502,7 @@ 35 + 7 @@ -533,15 +519,9 @@ 1 - - - 36 - - - 37 @@ -553,26 +533,56 @@ - 44 - + 50 + - + - 45 - - + 51 + + - 46 - + 74 + - 47 - + 85 + + + + + 87 + + + + + 90 + + + + + 91 + + + + + 92 + + + + + 93 + + + + + 94 + @@ -585,29 +595,24 @@ com.apple.InterfaceBuilder.CocoaPlugin - + - - - - - - + + + + + + com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -617,22 +622,65 @@ com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin - 48 + 95 + + + + + MPPasswordWindowController + NSWindowController + + NSTextField + NSTextField + NSTextField + + + + contentField + NSTextField + + + siteField + NSTextField + + + tipField + NSTextField + + + + IBProjectSource + ./Classes/MPPasswordWindowController.h + + + + NSLayoutConstraint + NSObject + + IBProjectSource + ./Classes/NSLayoutConstraint.h + + + - 0 IBCocoaFramework YES diff --git a/MasterPassword/Mac/MasterPassword-Info.plist b/MasterPassword/Mac/MasterPassword-Info.plist index 4e80f7ee..813915e4 100644 --- a/MasterPassword/Mac/MasterPassword-Info.plist +++ b/MasterPassword/Mac/MasterPassword-Info.plist @@ -26,6 +26,8 @@ public.app-category.productivity LSMinimumSystemVersion ${MACOSX_DEPLOYMENT_TARGET} + LSUIElement + NSHumanReadableCopyright Copyright © 2012 Lyndir. All rights reserved. NSMainNibFile