Files
MacPass/MacPass/MPEntryViewController.m

748 lines
32 KiB
Objective-C

//
// MPEntryViewController.m
// MacPass
//
// Created by michael starke on 18.02.13.
// Copyright (c) 2013 HicknHack Software GmbH. All rights reserved.
//
#import "MPEntryViewController.h"
#import "MPAppDelegate.h"
#import "MPOutlineViewController.h"
#import "MPDocument.h"
#import "MPDocument+Search.h"
#import "MPDocument+Autotype.h"
#import "MPDocument+HistoryBrowsing.h"
#import "MPDocumentWindowController.h"
#import "MPPasteBoardController.h"
#import "MPOverlayWindowController.h"
#import "MPContextBarViewController.h"
#import "MPConstants.h"
#import "MPActionHelper.h"
#import "MPContextMenuHelper.h"
#import "MPIconHelper.h"
#import "MPSettingsHelper.h"
#import "MPEntryTableDataSource.h"
#import "MPStringLengthValueTransformer.h"
#import "MPValueTransformerHelper.h"
#import "MPEntryContextMenuDelegate.h"
#import "KPKUTIs.h"
#import "KPKGroup.h"
#import "KPKEntry.h"
#import "KPKNode+IconImage.h"
#import "KPKAttribute.h"
#import "KPKTimeInfo.h"
#import "KPKMetaData.h"
#import "HNHTableHeaderCell.h"
#import "HNHGradientView.h"
#import "MPNotifications.h"
#import "NSString+Commands.h"
#define STATUS_BAR_ANIMATION_TIME 0.15
#define EXPIRED_ENTRY_REFRESH_SECONDS 60
typedef NS_ENUM(NSUInteger,MPOVerlayInfoType) {
MPOverlayInfoPassword,
MPOverlayInfoUsername,
MPOverlayInfoURL,
MPOverlayInfoCustom,
};
NSString *const MPEntryTableUserNameColumnIdentifier = @"MPUserNameColumnIdentifier";
NSString *const MPEntryTableTitleColumnIdentifier = @"MPTitleColumnIdentifier";
NSString *const MPEntryTablePasswordColumnIdentifier = @"MPPasswordColumnIdentifier";
NSString *const MPEntryTableParentColumnIdentifier = @"MPParentColumnIdentifier";
NSString *const MPEntryTableURLColumnIdentifier = @"MPEntryTableURLColumnIdentifier";
NSString *const MPEntryTableNotesColumnIdentifier = @"MPEntryTableNotesColumnIdentifier";
NSString *const MPEntryTableAttachmentColumnIdentifier = @"MPEntryTableAttachmentColumnIdentifier";
NSString *const MPEntryTableModfiedColumnIdentifier = @"MPEntryTableModfiedColumnIdentifier";
NSString *const _MPTableImageCellView = @"ImageCell";
NSString *const _MPTableStringCellView = @"StringCell";
NSString *const _MPTableSecurCellView = @"PasswordCell";
@interface MPEntryViewController () {
/* TODO unify delegation */
MPEntryContextMenuDelegate *_menuDelegate;
BOOL _isDisplayingContextBar;
BOOL _didUnlock;
}
@property (strong) NSArrayController *entryArrayController;
@property (strong) MPContextBarViewController *contextBarViewController;
@property (strong) NSArray *filteredEntries;
@property (weak) IBOutlet NSTableView *entryTable;
/* Constraints */
@property (strong) IBOutlet NSLayoutConstraint *tableToTopConstraint;
@property (strong) NSLayoutConstraint *contextBarTopConstraint;
@property (weak) IBOutlet HNHGradientView *bottomBar;
@property (weak) IBOutlet NSButton *addEntryButton;
@property (weak) IBOutlet NSTextField *footerInfoText;
@property (nonatomic, strong) MPEntryTableDataSource *dataSource;
@end
@implementation MPEntryViewController
+ (NSString *)timeInfoModificationTimeKeyPath {
static NSString *timeInfoModificationTimeKeyPath;
if(nil == timeInfoModificationTimeKeyPath) {
timeInfoModificationTimeKeyPath = [[NSString alloc] initWithFormat:@"%@.%@", NSStringFromSelector(@selector(timeInfo)), NSStringFromSelector(@selector(lastModificationTime))];
}
return timeInfoModificationTimeKeyPath;
}
- (NSString *)nibName {
return @"EntryView";
}
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if(self) {
_isDisplayingContextBar = NO;
_entryArrayController = [[NSArrayController alloc] init];
_dataSource = [[MPEntryTableDataSource alloc] init];
_dataSource.viewController = self;
_menuDelegate = [[MPEntryContextMenuDelegate alloc] init];
_contextBarViewController = [[MPContextBarViewController alloc] init];
[self _updateExpirationDisplay];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)didLoadView {
[[self view] setWantsLayer:YES];
[_bottomBar setBorderType:HNHBorderTop|HNHBorderHighlight];
[self.addEntryButton setAction:[MPActionHelper actionOfType:MPActionAddEntry]];
[self.entryTable setDelegate:self];
[self.entryTable setDoubleAction:@selector(_columnDoubleClick:)];
[self.entryTable setTarget:self];
[self.entryTable setFloatsGroupRows:NO];
[self.entryTable registerForDraggedTypes:@[KPKEntryUTI]];
/* First responder notifications */
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(_didBecomFirstResponder:)
name:MPDidActivateViewNotification
object:_entryTable];
[self _setupEntryMenu];
NSTableColumn *parentColumn = [self.entryTable tableColumns][0];
NSTableColumn *titleColumn = [self.entryTable tableColumns][1];
NSTableColumn *userNameColumn = [self.entryTable tableColumns][2];
NSTableColumn *passwordColumn = [self.entryTable tableColumns][3];
NSTableColumn *urlColumn = [self.entryTable tableColumns][4];
NSTableColumn *attachmentsColumn = [[NSTableColumn alloc] initWithIdentifier:MPEntryTableAttachmentColumnIdentifier];
NSTableColumn *notesColumn = [[NSTableColumn alloc] initWithIdentifier:MPEntryTableNotesColumnIdentifier];
NSTableColumn *modifiedColumn = [[NSTableColumn alloc] initWithIdentifier:MPEntryTableModfiedColumnIdentifier];
[notesColumn setMinWidth:40.0];
[attachmentsColumn setMinWidth:40.0];
[modifiedColumn setMinWidth:40.0];
[self.entryTable addTableColumn:notesColumn];
[self.entryTable addTableColumn:attachmentsColumn];
[self.entryTable addTableColumn:modifiedColumn];
[parentColumn setIdentifier:MPEntryTableParentColumnIdentifier];
[titleColumn setIdentifier:MPEntryTableTitleColumnIdentifier];
[userNameColumn setIdentifier:MPEntryTableUserNameColumnIdentifier];
[passwordColumn setIdentifier:MPEntryTablePasswordColumnIdentifier];
[urlColumn setIdentifier:MPEntryTableURLColumnIdentifier];
[self.entryTable setAutosaveName:@"EntryTable"];
[self.entryTable setAutosaveTableColumns:YES];
NSString *parentNameKeyPath = [[NSString alloc] initWithFormat:@"%@.%@", NSStringFromSelector(@selector(parent)), NSStringFromSelector(@selector(name))];
NSSortDescriptor *titleColumSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:NSStringFromSelector(@selector(title))ascending:YES selector:@selector(localizedCaseInsensitiveCompare:)];
NSSortDescriptor *userNameSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:NSStringFromSelector(@selector(username)) ascending:YES selector:@selector(localizedCaseInsensitiveCompare:)];
NSSortDescriptor *urlSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:NSStringFromSelector(@selector(url)) ascending:YES selector:@selector(localizedCaseInsensitiveCompare:)];
NSSortDescriptor *groupnameSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:parentNameKeyPath ascending:YES selector:@selector(localizedCaseInsensitiveCompare:)];
NSSortDescriptor *dateSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:[MPEntryViewController timeInfoModificationTimeKeyPath] ascending:YES selector:@selector(compare:)];
[titleColumn setSortDescriptorPrototype:titleColumSortDescriptor];
[userNameColumn setSortDescriptorPrototype:userNameSortDescriptor];
[urlColumn setSortDescriptorPrototype:urlSortDescriptor];
[parentColumn setSortDescriptorPrototype:groupnameSortDescriptor];
[modifiedColumn setSortDescriptorPrototype:dateSortDescriptor];
[[parentColumn headerCell] setStringValue:NSLocalizedString(@"GROUP", "")];
[[titleColumn headerCell] setStringValue:NSLocalizedString(@"TITLE", "")];
[[userNameColumn headerCell] setStringValue:NSLocalizedString(@"USERNAME", "")];
[[passwordColumn headerCell] setStringValue:NSLocalizedString(@"PASSWORD", "")];
[[urlColumn headerCell] setStringValue:NSLocalizedString(@"URL", "")];
[[notesColumn headerCell] setStringValue:NSLocalizedString(@"NOTES", "")];
[[attachmentsColumn headerCell] setStringValue:NSLocalizedString(@"ATTACHMENTS", "")];
[[modifiedColumn headerCell] setStringValue:NSLocalizedString(@"MODIFIED", "")];
[self.entryTable bind:NSContentBinding toObject:self.entryArrayController withKeyPath:NSStringFromSelector(@selector(arrangedObjects)) options:nil];
[self.entryTable bind:NSSortDescriptorsBinding toObject:self.entryArrayController withKeyPath:NSStringFromSelector(@selector(sortDescriptors)) options:nil];
[self.entryTable setDataSource:_dataSource];
// bind NSArrayController sorting so that sort order gets auto-saved
// see: http://simx.me/technonova/software_development/sort_descriptors_nstableview_bindings_a.html
[self.entryArrayController bind:NSSortDescriptorsBinding
toObject:[NSUserDefaultsController sharedUserDefaultsController]
withKeyPath:[MPSettingsHelper defaultControllerPathForKey:kMPSettingsKeyEntryTableSortDescriptors]
options:@{ NSValueTransformerNameBindingOption: NSUnarchiveFromDataTransformerName }];
[self _setupHeaderMenu];
[parentColumn setHidden:YES];
}
- (NSResponder *)reconmendedFirstResponder {
return self.entryTable;
}
- (void)regsiterNotificationsForDocument:(MPDocument *)document {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(_didChangeCurrentItem:)
name:MPDocumentCurrentItemChangedNotification
object:document];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(_didAddItem:)
name:MPDocumentDidAddEntryNotification
object:document];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(_didEnterSearch:)
name:MPDocumentDidEnterSearchNotification
object:document];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(_didExitSearch:)
name:MPDocumentDidExitSearchNotification
object:document];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(_didUpdateSearchResults:)
name:MPDocumentDidChangeSearchResults
object:document];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(_didUnlockDatabase:)
name:MPDocumentDidUnlockDatabaseNotification
object:document];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_didEnterHistory:) name:MPDocumentDidEnterHistoryNotification object:document];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_didExitHistory:) name:MPDocumentDidExitHistoryNotification object:document];
[self.contextBarViewController registerNotificationsForDocument:document];
}
#pragma mark NSTableViewDelgate
- (void)tableView:(NSTableView *)tableView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row {
/*
bind bakground color to entry color
*/
}
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
KPKEntry *entry = [self.entryArrayController arrangedObjects][row];
BOOL isTitleColumn = [[tableColumn identifier] isEqualToString:MPEntryTableTitleColumnIdentifier];
BOOL isGroupColumn = [[tableColumn identifier] isEqualToString:MPEntryTableParentColumnIdentifier];
BOOL isPasswordColum = [[tableColumn identifier] isEqualToString:MPEntryTablePasswordColumnIdentifier];
BOOL isUsernameColumn = [[tableColumn identifier] isEqualToString:MPEntryTableUserNameColumnIdentifier];
BOOL isURLColumn = [[tableColumn identifier] isEqualToString:MPEntryTableURLColumnIdentifier];
BOOL isAttachmentColumn = [[tableColumn identifier] isEqualToString:MPEntryTableAttachmentColumnIdentifier];
BOOL isNotesColumn = [[tableColumn identifier] isEqualToString:MPEntryTableNotesColumnIdentifier];
BOOL isModifedColumn = [[tableColumn identifier] isEqualToString:MPEntryTableModfiedColumnIdentifier];
NSTableCellView *view = nil;
if(isTitleColumn || isGroupColumn) {
view = [tableView makeViewWithIdentifier:_MPTableImageCellView owner:self];
if( isTitleColumn ) {
[[view textField] bind:NSValueBinding toObject:entry withKeyPath:NSStringFromSelector(@selector(title)) options:nil];
[[view imageView] bind:NSValueBinding toObject:entry withKeyPath:NSStringFromSelector(@selector(iconImage)) options:nil];
}
else {
NSAssert(entry.parent != nil, @"Entry needs to have a parent");
NSString *parentNameKeyPath = [NSString stringWithFormat:@"%@.%@",NSStringFromSelector(@selector(parent)),NSStringFromSelector(@selector(name))];
NSString *parentIconImageKeyPath = [NSString stringWithFormat:@"%@.%@",NSStringFromSelector(@selector(parent)),NSStringFromSelector(@selector(iconImage))];
[[view textField] bind:NSValueBinding toObject:entry withKeyPath:parentNameKeyPath options:nil];
[[view imageView] bind:NSValueBinding toObject:entry withKeyPath:parentIconImageKeyPath options:nil];
}
}
else if(isPasswordColum) {
view = [tableView makeViewWithIdentifier:_MPTableSecurCellView owner:self];
NSDictionary *options = @{ NSValueTransformerBindingOption : [NSValueTransformer valueTransformerForName:MPStringLengthValueTransformerName] };
[[view textField] bind:NSValueBinding toObject:entry withKeyPath:NSStringFromSelector(@selector(password)) options:options];
}
else {
view = [tableView makeViewWithIdentifier:_MPTableStringCellView owner:self];
NSTextField *textField = [view textField];
if(!isModifedColumn) {
/* clean up old formatter that might be left */
[textField setFormatter:nil];
}
if(isModifedColumn) {
if(![[view textField] formatter]) {
/* Just use one formatter instance since it's expensive to create */
static NSDateFormatter *formatter = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
formatter = [[NSDateFormatter alloc] init];
[formatter setDateStyle:NSDateFormatterMediumStyle];
[formatter setTimeStyle:NSDateFormatterMediumStyle];
});
[textField setFormatter:formatter];
}
[textField bind:NSValueBinding toObject:entry.timeInfo withKeyPath:NSStringFromSelector(@selector(lastModificationTime)) options:nil];
return view;
}
else if(isURLColumn) {
[textField bind:NSValueBinding toObject:entry withKeyPath:NSStringFromSelector(@selector(url)) options:nil];
}
else if(isUsernameColumn) {
[textField bind:NSValueBinding toObject:entry withKeyPath:NSStringFromSelector(@selector(username)) options:nil];
}
else if(isNotesColumn) {
NSDictionary *options = @{ NSValueTransformerNameBindingOption : MPStripLineBreaksTransformerName };
[textField bind:NSValueBinding toObject:entry withKeyPath:NSStringFromSelector(@selector(notes)) options:options];
}
else if(isAttachmentColumn) {
[textField bind:NSValueBinding toObject:entry withKeyPath:@"binaries.@count" options:nil];
}
}
return view;
}
- (void)tableView:(NSTableView *)tableView didRemoveRowView:(NSTableRowView *)rowView forRow:(NSInteger)row {
/* Rows being removed for data change should be chekced here to clear selections */
if(row == -1) {
[self tableViewSelectionDidChange:nil];
}
}
- (void)tableViewSelectionDidChange:(NSNotification *)notification {
MPDocument *document = [[self windowController] document];
if([self.entryTable selectedRow] < 0 || [[_entryTable selectedRowIndexes] count] > 1) {
document.selectedEntry = nil;
}
else {
document.selectedEntry = [self.entryArrayController arrangedObjects][[self.entryTable selectedRow]];
}
}
#pragma mark MPTargetItemResolving
- (KPKEntry *)currentTargetEntry {
NSInteger activeRow = [self.entryTable clickedRow];
/* Fallback to selection e.g. for toolbar actions */
if(activeRow < 0 ) {
activeRow = [self.entryTable selectedRow];
}
if(activeRow >= 0 && activeRow <= [[self.entryArrayController arrangedObjects] count]) {
return [self.entryArrayController arrangedObjects][activeRow];
}
return nil;
}
- (KPKNode *)currentTargetNode {
KPKEntry *entry = [self currentTargetEntry];
if(entry) {
return entry;
}
MPDocument *document = [[self windowController] document];
return document.selectedItem;
}
#pragma mark MPDocument Notifications
- (void)_didChangeCurrentItem:(NSNotification *)notification {
MPDocument *document = [notification object];
if(!document.selectedGroup && !document.hasSearch) {
/* no group selection out of search is wrong */
[self.entryArrayController unbind:NSContentArrayBinding];
[self.entryArrayController setContent:nil];
return;
}
/*
If a group is the current item, see if we already show that group
also test if an element has been selected (issue #257)
*/
if(document.selectedItem == document.selectedGroup && document.selectedItem != nil) {
if(document.hasSearch) {
/* If search was active, stop it and exit */
[document exitSearch:self];
}
else if([[self.entryArrayController content] count] > 0) {
KPKEntry *entry = [[self.entryArrayController content] lastObject];
if(entry.parent == document.selectedGroup) {
return; // we are showing the correct object right now.
}
}
[self.entryArrayController bind:NSContentArrayBinding toObject:document.selectedGroup withKeyPath:NSStringFromSelector(@selector(entries)) options:nil];
}
[self _updateContextBar];
}
- (void)_didBecomFirstResponder:(NSNotification *)notification {
MPDocument *document = [[self windowController] document];
if(document.selectedEntry.parent == document.selectedGroup || document.hasSearch) {
document.selectedItem = document.selectedEntry;
}
else {
document.selectedEntry = nil;
}
}
- (void)_didAddItem:(NSNotification *)notification {
MPDocument *document = [[self windowController] document];
if(!document.selectedGroup) {
/* TODO: show group? */
return; // No group selected
}
KPKEntry *entry = document.selectedGroup.entries.lastObject;
if(!entry) {
return; // No Entry found, nothing to select.
}
NSUInteger row = [self.entryArrayController.arrangedObjects indexOfObject:entry];
[self.entryTable scrollRowToVisible:row];
[self.entryTable selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
}
- (void)_didUpdateSearchResults:(NSNotification *)notification {
[self _showContextBar];
NSArray *result = [notification userInfo][kMPDocumentSearchResultsKey];
NSAssert(result != nil, @"Resutls should never be nil");
self.filteredEntries = result;
[self.entryArrayController unbind:NSContentArrayBinding];
[self.entryArrayController setContent:self.filteredEntries];
[[self.entryTable tableColumnWithIdentifier:MPEntryTableParentColumnIdentifier] setHidden:NO];
}
- (void)_didExitSearch:(NSNotification *)notification {
[[self.entryTable tableColumnWithIdentifier:MPEntryTableParentColumnIdentifier] setHidden:YES];
MPDocument *document = [[self windowController] document];
document.selectedItem = document.selectedGroup;
if( nil == document.selectedItem && nil == document.selectedGroup ) {
[self.entryArrayController unbind:NSContentArrayBinding];
[self.entryArrayController setContent:nil];
}
[self _updateContextBar];
}
- (void)_didEnterSearch:(NSNotification *)notification {
[self _showContextBar];
}
- (void)_didUnlockDatabase:(NSNotification *)notificiation {
MPDocument *document = [[self windowController] document];
/* If the document was locked and unlocked we do not need to recheck */
if(document.unlockCount != 1) {
[self.footerInfoText setHidden:![document hasMalformedAutotypeItems]];
[self.footerInfoText setStringValue:NSLocalizedString(@"DOCUMENT_AUTOTYPE_CORRUPTION_WARNING", "")];
}
}
- (void)_didEnterHistory:(NSNotification *)notification {
[self _showContextBar];
/* TODO: Show modification date column if not present? */
MPDocument *document = [[self windowController] document];
[self.entryArrayController bind:NSContentArrayBinding toObject:document.selectedEntry withKeyPath:NSStringFromSelector(@selector(history)) options:nil];
}
- (void)_didExitHistory:(NSNotification *)notification {
[self _hideContextBar];
MPDocument *document = [[self windowController] document];
document.selectedItem = document.selectedEntry;
}
#pragma mark ContextBar
- (void)_updateContextBar {
MPDocument *document = [[self windowController] document];
if(!document.hasSearch) {
BOOL showTrash = document.useTrash && (document.selectedGroup == document.trash || [document isItemTrashed:document.selectedItem]);
if(showTrash) {
[self _showContextBar];
}
else {
[self _hideContextBar];
}
}
}
- (void)_showContextBar {
if(_isDisplayingContextBar) {
return;
}
_isDisplayingContextBar = YES;
if(![[self.contextBarViewController view] superview]) {
[[self view] addSubview:[self.contextBarViewController view]];
[self.contextBarViewController updateResponderChain];
NSView *contextBar = [self.contextBarViewController view];
NSView *scrollView = [_entryTable enclosingScrollView];
NSDictionary *views = NSDictionaryOfVariableBindings(scrollView, contextBar);
/* Pin to the left */
[[self view] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contextBar]|" options:0 metrics:nil views:views]];
/* Pin height and to top of entry table */
[[self view] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[contextBar(==30)]-0-[scrollView]" options:0 metrics:nil views:views]];
/* Create the top constraint for the filter bar where we can change the contanst instaed of removing/adding constraints all the time */
self.contextBarTopConstraint = [NSLayoutConstraint constraintWithItem:contextBar
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:[self view]
attribute:NSLayoutAttributeTop
multiplier:1
constant:-31];
}
/* Add the view for the first time */
[[self view] removeConstraint:self.tableToTopConstraint];
[[self view] addConstraint:self.contextBarTopConstraint];
[[self view] layout];
self.contextBarTopConstraint.constant = 0;
[NSAnimationContext runAnimationGroup:^(NSAnimationContext* context) {
context.duration = STATUS_BAR_ANIMATION_TIME;
context.allowsImplicitAnimation = YES;
[self.view layoutSubtreeIfNeeded];
} completionHandler:nil];
}
- (void)_hideContextBar {
if(!_isDisplayingContextBar) {
return; // nothing to do;
}
self.contextBarTopConstraint.constant = -31;
[[self view] addConstraint:self.tableToTopConstraint];
[NSAnimationContext runAnimationGroup:^(NSAnimationContext* context) {
context.duration = STATUS_BAR_ANIMATION_TIME;
context.allowsImplicitAnimation = YES;
[self.view layoutSubtreeIfNeeded];
} completionHandler:^{
_isDisplayingContextBar = NO;
}];
}
#pragma mark Copy/Paste Overlays
- (void)_copyToPasteboard:(NSString *)data overlayInfo:(MPOVerlayInfoType)overlayInfoType name:(NSString *)name{
if(data) {
[[MPPasteBoardController defaultController] copyObjects:@[ data ]];
}
NSImage *infoImage = nil;
NSString *infoText = nil;
switch (overlayInfoType) {
case MPOverlayInfoPassword:
infoImage = [[NSBundle mainBundle] imageForResource:@"00_PasswordTemplate"];
infoText = NSLocalizedString(@"COPIED_PASSWORD", @"Password was copied to the pasteboard");
break;
case MPOverlayInfoURL:
infoImage = [[NSBundle mainBundle] imageForResource:@"01_PackageNetworkTemplate"];
infoText = NSLocalizedString(@"COPIED_URL", @"URL was copied to the pasteboard");
break;
case MPOverlayInfoUsername:
infoImage = [[NSBundle mainBundle] imageForResource:@"09_IdentityTemplate"];
infoText = NSLocalizedString(@"COPIED_USERNAME", @"Username was copied to the pasteboard");
break;
case MPOverlayInfoCustom:
infoImage = [[NSBundle mainBundle] imageForResource:@"00_PasswordTemplate"];
infoText = [NSString stringWithFormat:NSLocalizedString(@"COPIED_FIELD_%@", "Field nam that was copied to the pasteboard"), name];
break;
}
[[MPOverlayWindowController sharedController] displayOverlayImage:infoImage label:infoText atView:self.view];
}
#pragma mark Validation
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem {
/* Validation is soley handeld in the document */
MPDocument *document = [[self windowController] document];
return [document validateMenuItem:menuItem];
}
#pragma mark ContextMenu
- (void)_setupEntryMenu {
NSMenu *menu = [[NSMenu alloc] init];
NSArray *items = [MPContextMenuHelper contextMenuItemsWithItems:MPContextMenuFull];
for(NSMenuItem *item in items) {
[menu addItem:item];
}
[menu setDelegate:_menuDelegate];
[self.entryTable setMenu:menu];
}
- (void)_setupHeaderMenu {
NSMenu *headerMenu = [[NSMenu allocWithZone:[NSMenu menuZone]] init];
[headerMenu addItemWithTitle:NSLocalizedString(@"TITLE", "") action:NULL keyEquivalent:@""];
[headerMenu addItemWithTitle:NSLocalizedString(@"USERNAME", "") action:NULL keyEquivalent:@""];
[headerMenu addItemWithTitle:NSLocalizedString(@"PASSWORD", "") action:NULL keyEquivalent:@""];
[headerMenu addItemWithTitle:NSLocalizedString(@"URL", "") action:NULL keyEquivalent:@""];
[headerMenu addItemWithTitle:NSLocalizedString(@"NOTES", "") action:NULL keyEquivalent:@""];
[headerMenu addItemWithTitle:NSLocalizedString(@"ATTACHMENTS", "") action:NULL keyEquivalent:@""];
[headerMenu addItemWithTitle:NSLocalizedString(@"MODIFIED", "") action:NULL keyEquivalent:@""];
NSArray *identifier = @[ MPEntryTableTitleColumnIdentifier,
MPEntryTableUserNameColumnIdentifier,
MPEntryTablePasswordColumnIdentifier,
MPEntryTableURLColumnIdentifier,
MPEntryTableNotesColumnIdentifier,
MPEntryTableAttachmentColumnIdentifier,
MPEntryTableModfiedColumnIdentifier ];
NSDictionary *options = @{ NSValueTransformerNameBindingOption : NSNegateBooleanTransformerName };
for(NSMenuItem *item in [headerMenu itemArray]) {
NSUInteger index = [headerMenu indexOfItem:item];
NSTableColumn *column= [self.entryTable tableColumnWithIdentifier:identifier[index]];
[item bind:NSValueBinding toObject:column withKeyPath:NSHiddenBinding options:options];
}
[[self.entryTable headerView] setMenu:headerMenu];
}
#pragma mark Actions
- (void)copyPassword:(id)sender {
KPKEntry *selectedEntry = [[self currentTargetNode] asEntry];
if(selectedEntry) {
[self _copyToPasteboard:[selectedEntry.password finalValueForEntry:selectedEntry] overlayInfo:MPOverlayInfoPassword name:nil];
}
}
- (void)copyUsername:(id)sender {
KPKEntry *selectedEntry = [[self currentTargetNode] asEntry];
if(selectedEntry) {
[self _copyToPasteboard:[selectedEntry.username finalValueForEntry:selectedEntry] overlayInfo:MPOverlayInfoUsername name:nil];
}
}
- (void)copyCustomAttribute:(id)sender {
KPKEntry *selectedEntry = [[self currentTargetNode] asEntry];
if(selectedEntry && [selectedEntry isKindOfClass:[KPKEntry class]]) {
NSUInteger index = [sender tag];
NSAssert((index >= 0) && (index < [selectedEntry.customAttributes count]), @"Index for custom field needs to be valid");
KPKAttribute *attribute = selectedEntry.customAttributes[index];
[self _copyToPasteboard:attribute.evaluatedValue overlayInfo:MPOverlayInfoCustom name:attribute.key];
}
}
- (void)copyURL:(id)sender {
KPKEntry *selectedEntry = [[self currentTargetNode] asEntry];
if(selectedEntry) {
[self _copyToPasteboard:[selectedEntry.url finalValueForEntry:selectedEntry] overlayInfo:MPOverlayInfoURL name:nil];
}
}
- (void)openURL:(id)sender {
KPKEntry *selectedEntry = [[self currentTargetNode] asEntry];
NSString *expandedURL = [selectedEntry.url finalValueForEntry:selectedEntry];
if(expandedURL.length > 0) {
NSURL *webURL = [NSURL URLWithString:[expandedURL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSString *scheme = [webURL scheme];
if(!scheme) {
webURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@", [expandedURL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]];
}
NSString *browserBundleID = [[NSUserDefaults standardUserDefaults] objectForKey:kMPSettingsKeyBrowserBundleId];
BOOL openedURL = NO;
if(browserBundleID) {
openedURL = [[NSWorkspace sharedWorkspace] openURLs:@[webURL] withAppBundleIdentifier:browserBundleID options:NSWorkspaceLaunchAsync additionalEventParamDescriptor:nil launchIdentifiers:NULL];
}
if(!openedURL) {
openedURL = [[NSWorkspace sharedWorkspace] openURL:webURL];
}
if(!openedURL) {
NSLog(@"Unable to open URL %@", webURL);
}
}
}
- (void)delete:(id)sender {
KPKEntry *entry = [[self currentTargetNode] asEntry];
if(!entry) {
return;
}
MPDocument *document = [[self windowController] document];
[document deleteEntry:entry];
}
- (void)_columnDoubleClick:(id)sender {
if(0 == [[self.entryArrayController arrangedObjects] count]) {
return; // No data available
}
NSInteger columnIndex = [self.entryTable clickedColumn];
if(columnIndex < 0 || columnIndex >= [[self.entryTable tableColumns] count]) {
return; // No Colum to use
}
NSTableColumn *column = [self.entryTable tableColumns][[self.entryTable clickedColumn]];
NSString *identifier = [column identifier];
if([identifier isEqualToString:MPEntryTableTitleColumnIdentifier]) {
[self _executeTitleColumnDoubleClick];
}
else if([identifier isEqualToString:MPEntryTablePasswordColumnIdentifier]) {
[self copyPassword:nil];
}
else if([identifier isEqualToString:MPEntryTableUserNameColumnIdentifier]) {
[self copyUsername:nil];
}
else if([identifier isEqualToString:MPEntryTableURLColumnIdentifier]) {
[self _executeURLColumnDoubleClick];
}
// TODO: Add more actions for new columns
}
- (void)_executeTitleColumnDoubleClick {
MPDoubleClickTitleAction action = [[NSUserDefaults standardUserDefaults] integerForKey:kMPSettingsKeyDoubleClickTitleAction];
switch(action) {
case MPDoubleClickTitleActionInspect:
[(MPDocumentWindowController *)self.windowController showInspector:nil];
break;
case MPDoubleClickTitleActionIgnore:
break;
default:
NSLog(@"Unknown double click title action");
break;
}
}
- (void)_executeURLColumnDoubleClick {
MPDoubleClickURLAction action = [[NSUserDefaults standardUserDefaults] integerForKey:kMPSettingsKeyDoubleClickURLAction];
switch (action) {
case MPDoubleClickURLActionOpen:
[self openURL:nil];
break;
case MPDoubleClickURLActionCopy:
[self copyURL:nil];
break;
default:
NSLog(@"Unknown double click URL action");
break;
}
}
#pragma mark periodic UI Update
- (void)_updateExpirationDisplay {
/* items are all entries */
[[self.entryArrayController arrangedObjects] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
[[obj timeInfo] isExpired];
}];
[self performSelector:@selector(_updateExpirationDisplay) withObject:nil afterDelay:EXPIRED_ENTRY_REFRESH_SECONDS];
}
@end