2
0

Mac fixes and improvements to usability.

[FIXED]     Mac: Locking shouldn't unset the active user.
[IMPROVED]  Mac: Behavior of auto-completing site-name field improved.
[ADDED]     Mac: Alert when MP is incorrect when unlocking.
This commit is contained in:
Maarten Billemont 2013-01-26 22:05:57 -05:00
parent b07298e203
commit d5bffd86d6
5 changed files with 160 additions and 184 deletions

View File

@ -40,6 +40,8 @@ typedef enum {
MPElementTypeStoredDevicePrivate = 0x1 | MPElementTypeClassStored | MPElementFeatureDevicePrivate, MPElementTypeStoredDevicePrivate = 0x1 | MPElementTypeClassStored | MPElementFeatureDevicePrivate,
} MPElementType; } MPElementType;
#define MPErrorDomain @"MPErrorDomain"
#define MPCheckpointHelpChapter @"MPCheckpointHelpChapter" #define MPCheckpointHelpChapter @"MPCheckpointHelpChapter"
#define MPCheckpointCopyToPasteboard @"MPCheckpointCopyToPasteboard" #define MPCheckpointCopyToPasteboard @"MPCheckpointCopyToPasteboard"
#define MPCheckpointCopyLoginNameToPasteboard @"MPCheckpointCopyLoginNameToPasteboard" #define MPCheckpointCopyLoginNameToPasteboard @"MPCheckpointCopyLoginNameToPasteboard"

View File

@ -29,5 +29,6 @@
- (IBAction)togglePreference:(NSMenuItem *)sender; - (IBAction)togglePreference:(NSMenuItem *)sender;
- (IBAction)newUser:(NSMenuItem *)sender; - (IBAction)newUser:(NSMenuItem *)sender;
- (IBAction)signOut:(id)sender; - (IBAction)signOut:(id)sender;
- (IBAction)lock:(id)sender;
@end @end

View File

@ -37,7 +37,7 @@ static EventHotKeyID MPLockHotKey = {.signature = 'lock', .id = 1};
[MPMacConfig get]; [MPMacConfig get];
#ifdef DEBUG #ifdef DEBUG
[PearlLogger get].printLevel = PearlLogLevelTrace; [PearlLogger get].printLevel = PearlLogLevelDebug;//Trace;
#endif #endif
}); });
} }
@ -60,7 +60,7 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven
return noErr; return noErr;
} }
if (hotKeyID.signature == MPLockHotKey.signature && hotKeyID.id == MPLockHotKey.id) { if (hotKeyID.signature == MPLockHotKey.signature && hotKeyID.id == MPLockHotKey.id) {
[((__bridge MPAppDelegate *)userData) signOut:nil]; [((__bridge MPAppDelegate *)userData) lock:nil];
return noErr; return noErr;
} }
@ -163,6 +163,11 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven
[self signOutAnimated:YES]; [self signOutAnimated:YES];
} }
- (IBAction)lock:(id)sender {
self.key = nil;
}
- (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)oldValue { - (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)oldValue {
if (configKey == @selector(rememberLogin)) if (configKey == @selector(rememberLogin))

View File

@ -11,22 +11,19 @@
#import "MPAppDelegate_Key.h" #import "MPAppDelegate_Key.h"
#import "MPAppDelegate_Store.h" #import "MPAppDelegate_Store.h"
#define MPAlertUnlockMP @"MPAlertUnlockMP"
#define MPAlertIncorrectMP @"MPAlertIncorrectMP"
@interface MPPasswordWindowController () @interface MPPasswordWindowController ()
@property (nonatomic, strong) NSString *oldSiteName;
@property (nonatomic, strong) NSArray /* MPElementEntity */ *siteResults; @property (nonatomic, strong) NSArray /* MPElementEntity */ *siteResults;
@property (nonatomic) BOOL inProgress; @property (nonatomic) BOOL inProgress;
@property (nonatomic) BOOL siteFieldPreventCompletion;
@end @end
@implementation MPPasswordWindowController @implementation MPPasswordWindowController
@synthesize oldSiteName, siteResults;
@synthesize siteField;
@synthesize contentField;
@synthesize tipField;
@synthesize inProgress = _inProgress;
- (void)windowDidLoad { - (void)windowDidLoad {
@ -37,6 +34,8 @@
[self.userLabel setStringValue:PearlString(@"%@'s password for:", [MPAppDelegate get].activeUser.name)]; [self.userLabel setStringValue:PearlString(@"%@'s password for:", [MPAppDelegate get].activeUser.name)];
} forKeyPath:@"activeUser" options:NSKeyValueObservingOptionInitial context:nil]; } forKeyPath:@"activeUser" options:NSKeyValueObservingOptionInitial context:nil];
[[MPAppDelegate get] addObserverBlock:^(NSString *keyPath, id object, NSDictionary *change, void *context) { [[MPAppDelegate get] addObserverBlock:^(NSString *keyPath, id object, NSDictionary *change, void *context) {
if (![MPAppDelegate get].key)
[self unlock];
if ([MPAppDelegate get].activeUser && [MPAppDelegate get].key) if ([MPAppDelegate get].activeUser && [MPAppDelegate get].key)
[MPAlgorithmDefault migrateUser:[MPAppDelegate get].activeUser completion:^(BOOL userRequiresNewMigration) { [MPAlgorithmDefault migrateUser:[MPAppDelegate get].activeUser completion:^(BOOL userRequiresNewMigration) {
if (userRequiresNewMigration) if (userRequiresNewMigration)
@ -56,18 +55,6 @@
usingBlock:^(NSNotification *note) { usingBlock:^(NSNotification *note) {
[[NSApplication sharedApplication] hide:self]; [[NSApplication sharedApplication] hide:self];
}]; }];
[[NSNotificationCenter defaultCenter] addObserverForName:NSControlTextDidChangeNotification object:self.siteField queue:nil
usingBlock:^(NSNotification *note) {
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];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:MPNotificationSignedOut object:nil queue:nil [[NSNotificationCenter defaultCenter] addObserverForName:MPNotificationSignedOut object:nil queue:nil
usingBlock:^(NSNotification *note) { usingBlock:^(NSNotification *note) {
[self.window close]; [self.window close];
@ -78,6 +65,12 @@
- (void)unlock { - (void)unlock {
if (![MPAppDelegate get].activeUser)
// No user to sign in with.
return;
if ([MPAppDelegate get].key)
// Already logged in.
return;
if ([[MPAppDelegate get] signInAsUser:[MPAppDelegate get].activeUser usingMasterPassword:nil]) if ([[MPAppDelegate get] signInAsUser:[MPAppDelegate get].activeUser usingMasterPassword:nil])
// Load the key from the keychain. // Load the key from the keychain.
return; return;
@ -88,6 +81,10 @@
if ([MPAppDelegate get].key) if ([MPAppDelegate get].key)
return; return;
self.content = @"";
[self.siteField setStringValue:@""];
[self.tipField setStringValue:@""];
NSAlert *alert = [NSAlert alertWithMessageText:@"Master Password is locked." NSAlert *alert = [NSAlert alertWithMessageText:@"Master Password is locked."
defaultButton:@"Unlock" alternateButton:@"Change" otherButton:@"Cancel" defaultButton:@"Unlock" alternateButton:@"Change" otherButton:@"Cancel"
informativeTextWithFormat:@"The master password is required to unlock the application for:\n\n%@", [MPAppDelegate get].activeUser.name]; informativeTextWithFormat:@"The master password is required to unlock the application for:\n\n%@", [MPAppDelegate get].activeUser.name];
@ -96,57 +93,71 @@
[alert layout]; [alert layout];
[passwordField becomeFirstResponder]; [passwordField becomeFirstResponder];
[alert beginSheetModalForWindow:self.window modalDelegate:self [alert beginSheetModalForWindow:self.window modalDelegate:self
didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:NULL]; 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 {
switch (returnCode) { if (contextInfo == MPAlertIncorrectMP) {
case NSAlertAlternateReturn: [self.window close];
// "Change" button. return;
if ([[NSAlert alertWithMessageText:@"Changing Master Password" }
defaultButton:nil alternateButton:[PearlStrings get].commonButtonCancel otherButton:nil if (contextInfo == MPAlertUnlockMP) {
informativeTextWithFormat: switch (returnCode) {
@"This will allow you to log in with a different master password.\n\n" case NSAlertAlternateReturn:
@"Note that you will only see the sites and passwords for the master password you log in with.\n" // "Change" button.
@"If you log in with a different master password, your current sites will be unavailable.\n\n" if ([[NSAlert alertWithMessageText:@"Changing Master Password"
@"You can always change back to your current master password later.\n" defaultButton:nil alternateButton:[PearlStrings get].commonButtonCancel otherButton:nil
@"Your current sites and passwords will then become available again."] runModal] informativeTextWithFormat:
== 1) { @"This will allow you to log in with a different master password.\n\n"
[MPAppDelegate get].activeUser.keyID = nil; @"Note that you will only see the sites and passwords for the master password you log in with.\n"
[[MPAppDelegate get] forgetSavedKeyFor:[MPAppDelegate get].activeUser]; @"If you log in with a different master password, your current sites will be unavailable.\n\n"
[[MPAppDelegate get] signOutAnimated:YES]; @"You can always change back to your current master password later.\n"
} @"Your current sites and passwords will then become available again."] runModal]
break; == 1) {
[MPAppDelegate get].activeUser.keyID = nil;
[[MPAppDelegate get] forgetSavedKeyFor:[MPAppDelegate get].activeUser];
[[MPAppDelegate get] signOutAnimated:YES];
}
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.
self.contentContainer.alphaValue = 0; self.contentContainer.alphaValue = 0;
[self.progressView startAnimation:nil]; [self.progressView startAnimation:nil];
self.inProgress = YES; self.inProgress = YES;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
BOOL success = [[MPAppDelegate get] signInAsUser:[MPAppDelegate get].activeUser usingMasterPassword:[(NSSecureTextField *)alert.accessoryView stringValue]]; BOOL success = [[MPAppDelegate get] signInAsUser:[MPAppDelegate get].activeUser
self.inProgress = NO; usingMasterPassword:[(NSSecureTextField *)alert.accessoryView stringValue]];
self.inProgress = NO;
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[self.progressView stopAnimation:nil]; [self.progressView stopAnimation:nil];
if (success) if (success)
self.contentContainer.alphaValue = 1; self.contentContainer.alphaValue = 1;
else else {
[self.window close]; [[NSAlert alertWithError:[NSError errorWithDomain:MPErrorDomain code:0 userInfo:@{
NSLocalizedDescriptionKey : PearlString(@"Incorrect master password for user %@",
[MPAppDelegate get].activeUser.name)
}]] beginSheetModalForWindow:self.window modalDelegate:self
didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:MPAlertIncorrectMP];
}
});
}); });
}); }
default:
break;
} }
default: return;
break;
} }
} }
@ -159,19 +170,27 @@
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])]; NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses_" ascending:NO]]; fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses_" ascending:NO]];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND user == %@", fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(name BEGINSWITH[cd] %@) AND user == %@",
query, query, [MPAppDelegate get].activeUser]; query, [MPAppDelegate get].activeUser];
NSError *error = nil; NSError *error = nil;
self.siteResults = [[MPAppDelegate managedObjectContextIfReady] executeFetchRequest:fetchRequest error:&error]; self.siteResults = [[MPAppDelegate managedObjectContextIfReady] executeFetchRequest:fetchRequest error:&error];
if (error) if (error)
err(@"Couldn't fetch elements: %@", error); err(@"While fetching elements for completion: %@", error);
if ([self.siteResults count] == 1) {
[textView setString:[(MPElementEntity *)[self.siteResults objectAtIndex:0] name]];
[textView setSelectedRange:NSMakeRange([query length], [[textView string] length] - [query length])];
if ([self trySite])
return nil;
}
NSMutableArray *mutableResults = [NSMutableArray arrayWithCapacity:[self.siteResults count] + 1]; NSMutableArray *mutableResults = [NSMutableArray arrayWithCapacity:[self.siteResults count] + 1];
if (self.siteResults) if (self.siteResults)
for (MPElementEntity *element in self.siteResults) for (MPElementEntity *element in self.siteResults)
[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.
return mutableResults; return mutableResults;
} }
@ -181,32 +200,70 @@
[self.window close]; [self.window close];
return YES; return YES;
} }
if ((self.siteFieldPreventCompletion = [NSStringFromSelector(commandSelector) hasPrefix:@"delete"]))
return NO;
if (commandSelector == @selector(insertNewline:) && [self.content length]) { if (commandSelector == @selector(insertNewline:) && [self.content length]) {
[[NSPasteboard generalPasteboard] declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil]; if ([self trySite])
if ([[NSPasteboard generalPasteboard] setString:self.content forType:NSPasteboardTypeString]) { [self copyContents];
self.tipField.alphaValue = 1; return YES;
[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(), ^{
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:0.2f];
[self.tipField.animator setAlphaValue:0];
[NSAnimationContext endGrouping];
});
[[self findElement] use];
return YES;
} else
wrn(@"Couldn't copy password to pasteboard.");
} }
return NO; return NO;
} }
- (void)controlTextDidEndEditing:(NSNotification *)obj { - (void)copyContents {
if (obj.object == self.siteField) [[NSPasteboard generalPasteboard] declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil];
[self trySite]; if (![[NSPasteboard generalPasteboard] setString:self.content forType:NSPasteboardTypeString]) {
wrn(@"Couldn't copy password to pasteboard.");
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
self.tipField.alphaValue = 1;
[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(), ^{
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:0.2f];
[self.tipField.animator setAlphaValue:0];
[NSAnimationContext endGrouping];
});
});
[[self findElement] use];
}
- (void)controlTextDidEndEditing:(NSNotification *)note {
if (note.object != self.siteField)
return;
[self trySite];
}
- (void)controlTextDidChange:(NSNotification *)note {
if (note.object != self.siteField)
return;
// Update the site content as the site name changes.
BOOL hasValidSite = [self trySite];
if ([[NSApp currentEvent] type] == NSKeyDown && [[[NSApp currentEvent] charactersIgnoringModifiers] isEqualToString:@"\r"]) {
if (hasValidSite)
[self copyContents];
return;
}
if (self.siteFieldPreventCompletion) {
self.siteFieldPreventCompletion = NO;
return;
}
self.siteFieldPreventCompletion = YES;
[(NSText *)[note.userInfo objectForKey:@"NSFieldEditor"] complete:self];
self.siteFieldPreventCompletion = NO;
} }
- (NSString *)content { - (NSString *)content {
@ -237,8 +294,6 @@
return NO; return NO;
} }
dbg(@"element:\n%@", [result debugDescription]);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSString *description = [result.content description]; NSString *description = [result.content description];
if (!description) if (!description)

View File

@ -3,12 +3,12 @@
<data> <data>
<int key="IBDocument.SystemTarget">1070</int> <int key="IBDocument.SystemTarget">1070</int>
<string key="IBDocument.SystemVersion">12C60</string> <string key="IBDocument.SystemVersion">12C60</string>
<string key="IBDocument.InterfaceBuilderVersion">2840</string> <string key="IBDocument.InterfaceBuilderVersion">2844</string>
<string key="IBDocument.AppKitVersion">1187.34</string> <string key="IBDocument.AppKitVersion">1187.34</string>
<string key="IBDocument.HIToolboxVersion">625.00</string> <string key="IBDocument.HIToolboxVersion">625.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions"> <object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string> <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="NS.object.0">2840</string> <string key="NS.object.0">2844</string>
</object> </object>
<array key="IBDocument.IntegratedClassDependencies"> <array key="IBDocument.IntegratedClassDependencies">
<string>NSCustomObject</string> <string>NSCustomObject</string>
@ -241,14 +241,6 @@
</object> </object>
<int key="connectionID">726</int> <int key="connectionID">726</int>
</object> </object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">signOut:</string>
<reference key="source" ref="976324537"/>
<reference key="destination" ref="229948989"/>
</object>
<int key="connectionID">727</int>
</object>
<object class="IBConnectionRecord"> <object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection"> <object class="IBOutletConnection" key="connection">
<string key="label">showItem</string> <string key="label">showItem</string>
@ -345,6 +337,14 @@
</object> </object>
<int key="connectionID">763</int> <int key="connectionID">763</int>
</object> </object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">lock:</string>
<reference key="source" ref="976324537"/>
<reference key="destination" ref="229948989"/>
</object>
<int key="connectionID">764</int>
</object>
</array> </array>
<object class="IBMutableOrderedSet" key="objectRecords"> <object class="IBMutableOrderedSet" key="objectRecords">
<array key="orderedObjects"> <array key="orderedObjects">
@ -538,96 +538,9 @@
<nil key="activeLocalization"/> <nil key="activeLocalization"/>
<dictionary class="NSMutableDictionary" key="localizations"/> <dictionary class="NSMutableDictionary" key="localizations"/>
<nil key="sourceID"/> <nil key="sourceID"/>
<int key="maxID">763</int> <int key="maxID">764</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes">
<array class="NSMutableArray" key="referencedPartialClassDescriptions">
<object class="IBPartialClassDescription">
<string key="className">MPAppDelegate</string>
<string key="superclassName">MPAppDelegate_Shared</string>
<dictionary class="NSMutableDictionary" key="actions">
<string key="activate:">id</string>
<string key="newUser:">NSMenuItem</string>
<string key="signOut:">id</string>
<string key="togglePreference:">NSMenuItem</string>
</dictionary>
<dictionary class="NSMutableDictionary" key="actionInfosByName">
<object class="IBActionInfo" key="activate:">
<string key="name">activate:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo" key="newUser:">
<string key="name">newUser:</string>
<string key="candidateClassName">NSMenuItem</string>
</object>
<object class="IBActionInfo" key="signOut:">
<string key="name">signOut:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo" key="togglePreference:">
<string key="name">togglePreference:</string>
<string key="candidateClassName">NSMenuItem</string>
</object>
</dictionary>
<dictionary class="NSMutableDictionary" key="outlets">
<string key="createUserItem">NSMenuItem</string>
<string key="lockItem">NSMenuItem</string>
<string key="rememberPasswordItem">NSMenuItem</string>
<string key="savePasswordItem">NSMenuItem</string>
<string key="showItem">NSMenuItem</string>
<string key="statusMenu">NSMenu</string>
<string key="useICloudItem">NSMenuItem</string>
<string key="usersItem">NSMenuItem</string>
</dictionary>
<dictionary class="NSMutableDictionary" key="toOneOutletInfosByName">
<object class="IBToOneOutletInfo" key="createUserItem">
<string key="name">createUserItem</string>
<string key="candidateClassName">NSMenuItem</string>
</object>
<object class="IBToOneOutletInfo" key="lockItem">
<string key="name">lockItem</string>
<string key="candidateClassName">NSMenuItem</string>
</object>
<object class="IBToOneOutletInfo" key="rememberPasswordItem">
<string key="name">rememberPasswordItem</string>
<string key="candidateClassName">NSMenuItem</string>
</object>
<object class="IBToOneOutletInfo" key="savePasswordItem">
<string key="name">savePasswordItem</string>
<string key="candidateClassName">NSMenuItem</string>
</object>
<object class="IBToOneOutletInfo" key="showItem">
<string key="name">showItem</string>
<string key="candidateClassName">NSMenuItem</string>
</object>
<object class="IBToOneOutletInfo" key="statusMenu">
<string key="name">statusMenu</string>
<string key="candidateClassName">NSMenu</string>
</object>
<object class="IBToOneOutletInfo" key="useICloudItem">
<string key="name">useICloudItem</string>
<string key="candidateClassName">NSMenuItem</string>
</object>
<object class="IBToOneOutletInfo" key="usersItem">
<string key="name">usersItem</string>
<string key="candidateClassName">NSMenuItem</string>
</object>
</dictionary>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">./Classes/MPAppDelegate.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">MPAppDelegate_Shared</string>
<string key="superclassName">PearlAppDelegate</string>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">./Classes/MPAppDelegate_Shared.h</string>
</object>
</object>
</array>
</object> </object>
<object class="IBClassDescriber" key="IBDocument.Classes"/>
<int key="IBDocument.localizationMode">0</int> <int key="IBDocument.localizationMode">0</int>
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string> <string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies"> <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">