diff --git a/Cartfile b/Cartfile index 7c311fea..63b7ddba 100644 --- a/Cartfile +++ b/Cartfile @@ -1,3 +1,3 @@ github "sparkle-project/Sparkle" ~> 1.18.1 -github "MacPass/KeePassKit" ~> 2.2.1 +github "MacPass/KeePassKit" ~> 2.3 github "mstarke/HNHUi" ~> 3.0 diff --git a/Cartfile.resolved b/Cartfile.resolved index e016c6bc..497a0b6a 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,4 +1,4 @@ -github "MacPass/KeePassKit" "2.2.1" +github "MacPass/KeePassKit" "2.3.1" github "mstarke/HNHUi" "3.0" github "robbiehanson/KissXML" "5.2.3" github "sparkle-project/Sparkle" "1.20.0" diff --git a/MacPass.xcodeproj/project.pbxproj b/MacPass.xcodeproj/project.pbxproj index 0ba57f72..2ed47040 100644 --- a/MacPass.xcodeproj/project.pbxproj +++ b/MacPass.xcodeproj/project.pbxproj @@ -130,6 +130,8 @@ 4C586F9E16D07ABD00E7DB57 /* 00_PasswordTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 4C586F9D16D07ABD00E7DB57 /* 00_PasswordTemplate.pdf */; }; 4C586FA016D07D7200E7DB57 /* 01_PackageNetworkTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 4C586F9F16D07D7200E7DB57 /* 01_PackageNetworkTemplate.pdf */; }; 4C586FA216D07F6A00E7DB57 /* 02_MessageBoxWarningTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 4C586FA116D07F6A00E7DB57 /* 02_MessageBoxWarningTemplate.pdf */; }; + 4C58A4A32192EC1600B13370 /* NSIndexPath+MPAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C58A4A22192EC1600B13370 /* NSIndexPath+MPAdditions.m */; }; + 4C58A4A82192EEBE00B13370 /* MPTestIndexPathAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C58A4A72192EEBE00B13370 /* MPTestIndexPathAdditions.m */; }; 4C5A11FE1708DE8700223D8A /* MPPasswordCreatorViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C5A11FC1708DE8700223D8A /* MPPasswordCreatorViewController.m */; }; 4C5EF816218CA03F0003C00E /* MPAutotypeParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C5EF815218CA03F0003C00E /* MPAutotypeParser.m */; }; 4C5FE9AE17843CE20001D5A8 /* MPSelectedAttachmentTableCellView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C5FE9AD17843CE20001D5A8 /* MPSelectedAttachmentTableCellView.m */; }; @@ -526,6 +528,9 @@ 4C586F9D16D07ABD00E7DB57 /* 00_PasswordTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = 00_PasswordTemplate.pdf; sourceTree = ""; }; 4C586F9F16D07D7200E7DB57 /* 01_PackageNetworkTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = 01_PackageNetworkTemplate.pdf; sourceTree = ""; }; 4C586FA116D07F6A00E7DB57 /* 02_MessageBoxWarningTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = 02_MessageBoxWarningTemplate.pdf; sourceTree = ""; }; + 4C58A4A12192EC1600B13370 /* NSIndexPath+MPAdditions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSIndexPath+MPAdditions.h"; sourceTree = ""; }; + 4C58A4A22192EC1600B13370 /* NSIndexPath+MPAdditions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSIndexPath+MPAdditions.m"; sourceTree = ""; }; + 4C58A4A72192EEBE00B13370 /* MPTestIndexPathAdditions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPTestIndexPathAdditions.m; sourceTree = ""; }; 4C5A11FB1708DE8700223D8A /* MPPasswordCreatorViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPPasswordCreatorViewController.h; sourceTree = ""; }; 4C5A11FC1708DE8700223D8A /* MPPasswordCreatorViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPPasswordCreatorViewController.m; sourceTree = ""; }; 4C5ADC3017830B09004E1E8D /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InspectorView.strings; sourceTree = ""; }; @@ -1085,6 +1090,8 @@ 4C0949581FD6B89B004F2971 /* NSUserNotification+MPAdditions.m */, 4C769CA7213D59BF00A3F60A /* KPKEntry+MPCustomAttributeProperties.h */, 4C769CA8213D59BF00A3F60A /* KPKEntry+MPCustomAttributeProperties.m */, + 4C58A4A12192EC1600B13370 /* NSIndexPath+MPAdditions.h */, + 4C58A4A22192EC1600B13370 /* NSIndexPath+MPAdditions.m */, ); name = Categories; sourceTree = ""; @@ -1213,6 +1220,7 @@ 4C8F0C721FCF1B7A00BE157F /* MPTestPickcharsParser.m */, 4C71BCB32167B75900B4CBDA /* MPTestPluginVersionComparator.m */, 4C45FB1F178E09ED0010007D /* Supporting Files */, + 4C58A4A72192EEBE00B13370 /* MPTestIndexPathAdditions.m */, ); path = MacPassTests; sourceTree = ""; @@ -1939,6 +1947,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4C58A4A82192EEBE00B13370 /* MPTestIndexPathAdditions.m in Sources */, 4C45FB2D178E0BCB0010007D /* MPDatabaseLoading.m in Sources */, 4C8DEAA21C314D2C00D24C32 /* MPTestAutotypeDelay.m in Sources */, 4C71BCB42167B75900B4CBDA /* MPTestPluginVersionComparator.m in Sources */, @@ -2039,6 +2048,7 @@ 4C3666411787327E00B249F1 /* MPDocument+Attachments.m in Sources */, 4CF6C3021FBF39BF0055AD03 /* MPPluginTabelCellView.m in Sources */, 4C10412C178CDD44001B5239 /* NSDate+Humanized.m in Sources */, + 4C58A4A32192EC1600B13370 /* NSIndexPath+MPAdditions.m in Sources */, 4C2F17A21FD69BCA0097418D /* MPUserNotificationCenterDelegate.m in Sources */, 4CC663E7216F7A7100E33965 /* MPPluginRepositoryBrowserViewController.m in Sources */, 4C0C59F118B17F10009C7B76 /* DDHotKeyUtilities.m in Sources */, diff --git a/MacPass/MPEntryTableDataSource.m b/MacPass/MPEntryTableDataSource.m index f45c477b..4298564f 100644 --- a/MacPass/MPEntryTableDataSource.m +++ b/MacPass/MPEntryTableDataSource.m @@ -39,4 +39,8 @@ return nil; } +- (void)tableView:(NSTableView *)tableView draggingSession:(NSDraggingSession *)session willBeginAtPoint:(NSPoint)screenPoint forRowIndexes:(nonnull NSIndexSet *)rowIndexes { + session.draggingFormation = NSDraggingFormationList; +} + @end diff --git a/MacPass/MPEntryViewController.m b/MacPass/MPEntryViewController.m index df4bdf86..fc783ec7 100644 --- a/MacPass/MPEntryViewController.m +++ b/MacPass/MPEntryViewController.m @@ -123,7 +123,7 @@ NSString *const _MPTableSecurCellView = @"PasswordCell"; self.entryTable.doubleAction = @selector(_columnDoubleClick:); self.entryTable.target = self; self.entryTable.floatsGroupRows = NO; - [self.entryTable registerForDraggedTypes:@[KPKEntryUTI]]; + [self.entryTable registerForDraggedTypes:@[KPKEntryUTI, KPKEntryUUDIUTI]]; /* First responder notifications */ [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(_didBecomFirstResponder:) diff --git a/MacPass/MPOutlineDataSource.m b/MacPass/MPOutlineDataSource.m index 7f3c7bae..497a85cd 100644 --- a/MacPass/MPOutlineDataSource.m +++ b/MacPass/MPOutlineDataSource.m @@ -23,16 +23,13 @@ #import "MPOutlineDataSource.h" #import "MPDocument.h" #import "MPConstants.h" +#import "NSIndexPath+MPAdditions.h" #import "KeePassKit/KeePassKit.h" @interface MPOutlineDataSource () -@property (weak) KPKGroup *localDraggedGroup; -@property (weak) KPKEntry *localDraggedEntry; - @property (strong) NSArray *draggedGroups; -@property (strong) NSArray *draggedEntries; @end @@ -47,74 +44,88 @@ return nil; } -- (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id)info proposedItem:(id)item proposedChildIndex:(NSInteger)index { - - /* Clean up our local search */ - self.localDraggedEntry = nil; - self.localDraggedGroup = nil; - - info.animatesToDestination = YES; - NSDragOperation operationMask = NSDragOperationMove; - /* - If we can support copy on drag, this can be used - to obtain the dragging modifier mask the user presses - */ - BOOL localCopy = NO; - if([info draggingSourceOperationMask] == NSDragOperationCopy) { - operationMask = NSDragOperationCopy; - localCopy = YES; +- (void)outlineView:(NSOutlineView *)outlineView draggingSession:(NSDraggingSession *)session willBeginAtPoint:(NSPoint)screenPoint forItems:(NSArray *)draggedItems { + session.draggingFormation = NSDraggingFormationList; + self.draggedGroups = @[]; + NSMutableArray *localDraggedGroups = [[NSMutableArray alloc] init]; + for(NSTreeNode *node in draggedItems) { + BOOL addNode = YES; + for(NSTreeNode *otherNode in draggedItems) { + if(node == otherNode) { + continue; + } + addNode &= ![otherNode.indexPath containsIndexPath:node.indexPath]; + } + if(addNode) { + KPKGroup *group = node.representedObject; + [localDraggedGroups addObject:group]; + } } + self.draggedGroups = [localDraggedGroups copy]; +} + +- (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id)info proposedItem:(id)item proposedChildIndex:(NSInteger)index { + info.animatesToDestination = YES; - - /* Check if the Target is the root group */ id targetItem = [item representedObject]; if( ![targetItem isKindOfClass:KPKGroup.class] ) { return NSDragOperationNone; // Block all unknown types } - NSPasteboard *pasteBoard = [info draggingPasteboard]; - KPKGroup *draggedGroup = nil; - KPKEntry *draggedEntry = nil; - BOOL couldReadPasteboard = [self _readDataFromPasteboard:pasteBoard group:&draggedGroup entry:&draggedEntry]; - if(!couldReadPasteboard) { - return NSDragOperationNone; - } + BOOL copyDrag = (info.draggingSourceOperationMask == NSDragOperationCopy); KPKGroup *targetGroup = targetItem; - BOOL validTarget = YES; - MPDocument *document = outlineView.window.windowController.document; - /* Dragging Groups */ - if(draggedGroup) { - self.localDraggedGroup = [document findGroup:draggedGroup.uuid]; - if( [draggedGroup.uuid isEqual:targetGroup.uuid] ) { - return NSDragOperationNone; // Groups cannot be moved inside themselves - } - if(self.localDraggedGroup) { - if( self.localDraggedGroup.parent == targetGroup ) { - validTarget &= index != NSOutlineViewDropOnItemIndex; - validTarget &= index != [self.localDraggedGroup.parent.groups indexOfObject:self.localDraggedGroup]; + BOOL isGroupDrop = (info.draggingSource == outlineView); + BOOL isEntryDrop = (!isGroupDrop && [info.draggingSource window] == outlineView.window); + if(isGroupDrop) { + /* local group drop */ + for(KPKGroup *draggedGroup in self.draggedGroups) { + BOOL validTarget = YES; + if(targetGroup == draggedGroup) { + return copyDrag ? NSDragOperationCopy : NSDragOperationNone; } - BOOL isAnchesor = [self.localDraggedGroup isAnchestorOf:targetGroup]; + + if(draggedGroup.parent == targetGroup) { + if(index == NSOutlineViewDropOnItemIndex) { + return copyDrag ? NSDragOperationCopy : NSDragOperationNone; + } + validTarget &= index != draggedGroup.index; + } + BOOL isAnchesor = [draggedGroup isAnchestorOf:targetGroup]; validTarget &= !isAnchesor; + if(!validTarget) { + return NSDragOperationNone; + } + } + return copyDrag ? NSDragOperationCopy : NSDragOperationMove; + } + else if(isEntryDrop) { + /* local entry drop */ + MPDocument *document = outlineView.window.windowController.document; + NSUUID *entryUUID = [self _entryUUIDsFromPasteboard:info.draggingPasteboard].firstObject; + KPKEntry *entry = [document findEntry:entryUUID]; + // FIXME: set correct item when drop is proposed between items + if(index != NSOutlineViewDropOnItemIndex) { + NSTreeNode *node = item; + NSUInteger dropIndex = MIN(node.childNodes.count-1, index); + id dropItem = node.childNodes[dropIndex]; + [outlineView setDropItem:dropItem dropChildIndex:NSOutlineViewDropOnItemIndex]; } else { - /* Copy can always work in this case */ - operationMask = NSDragOperationCopy; - } - } - else if(draggedEntry) { - self.localDraggedEntry = [document findEntry:draggedEntry.uuid]; - if(self.localDraggedEntry) { - /* local Copy is always valid regardless of parent */ - validTarget = localCopy ? YES : self.localDraggedEntry.parent != targetGroup; [outlineView setDropItem:item dropChildIndex:NSOutlineViewDropOnItemIndex]; } - else { - /* Entry copy is always valid */ - operationMask = NSDragOperationCopy; + if(entry.parent == targetItem) { + return copyDrag ? NSDragOperationCopy : NSDragOperationNone; } + return copyDrag ? NSDragOperationCopy : NSDragOperationMove; + } + else { + /* extern drop */ + if([info.draggingPasteboard.types containsObject:KPKEntryUUDIUTI]) { + [outlineView setDropItem:item dropChildIndex:NSOutlineViewDropOnItemIndex]; + } + return NSDragOperationCopy; } - return validTarget ? operationMask : NSDragOperationNone; } - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id)info item:(id)item childIndex:(NSInteger)index { @@ -125,80 +136,99 @@ return NO; // Wrong } - NSPasteboard *pasteBoard = [info draggingPasteboard]; - KPKGroup *draggedGroup = nil; - KPKEntry *draggedEntry = nil; - BOOL validPateboard = [self _readDataFromPasteboard:pasteBoard group:&draggedGroup entry:&draggedEntry]; - if(!validPateboard) { - return NO; - } + BOOL copyItem = (info.draggingSourceOperationMask == NSDragOperationCopy); - BOOL copyItem = ([info draggingSourceOperationMask] == NSDragOperationCopy); - - KPKGroup *targetGroup = (KPKGroup *)targetItem; - if(draggedGroup) { - if(copyItem || (nil == self.localDraggedGroup) ) { - draggedGroup = [draggedGroup copyWithTitle:nil options:kKPKCopyOptionNone]; - [draggedGroup addToGroup:targetGroup atIndex:index]; - [draggedGroup.undoManager setActionName:NSLocalizedString(@"COPY_GROUP", "Action title for copying a group via drag and drop")]; + KPKGroup *targetGroup = targetItem; + MPDocument *document = outlineView.window.windowController.document; + if(info.draggingSource == outlineView) { + if(copyItem) { + NSUInteger insertIndex = index; + for(KPKGroup *group in self.draggedGroups.reverseObjectEnumerator) { + KPKGroup *groupCopy = [group copyWithTitle:nil options:kKPKCopyOptionNone]; + [groupCopy addToGroup:targetGroup atIndex:insertIndex]; + insertIndex = groupCopy.index; + [groupCopy.undoManager setActionName:NSLocalizedString(@"COPY_GROUP", "Action title for copying a group via drag and drop")]; + } return YES; } - else if(self.localDraggedGroup) { - /* Simple move */ - [self.localDraggedGroup moveToGroup:targetGroup atIndex:index]; - [self.localDraggedGroup.undoManager setActionName:NSLocalizedString(@"MOVE_GROUP", "Action title for moving a group via drag and drop")]; - return YES; - } - /* Nothing valid */ - return NO; - } - else if(draggedEntry) { - if(copyItem || (nil == self.localDraggedEntry)) { - draggedEntry = [draggedEntry copyWithTitle:nil options:kKPKCopyOptionNone]; - [draggedEntry addToGroup:targetGroup]; - [draggedEntry.undoManager setActionName:NSLocalizedString(@"COPY_ENTRY", "Action title for copying an entry via drag and drop")]; - return YES; - } - else if(self.localDraggedEntry) { - [self.localDraggedEntry moveToGroup:targetGroup]; - [self.localDraggedEntry.undoManager setActionName:NSLocalizedString(@"MOVE_ENTRY", "Action title for moving an entry via drag and drop")]; + else { + NSUInteger insertIndex = index; + for(KPKGroup *group in self.draggedGroups.reverseObjectEnumerator) { + [group moveToGroup:targetGroup atIndex:insertIndex]; + insertIndex = group.index; + [group.undoManager setActionName:NSLocalizedString(@"MOVE_GROUP", "Action title for moving a group via drag and drop")]; + } return YES; } } - return NO; + else if([info.draggingSource window] == outlineView.window) { + NSArray *entryUUIDs = [self _entryUUIDsFromPasteboard:info.draggingPasteboard]; + if(copyItem) { + for(NSUUID *entryUUID in entryUUIDs) { + KPKEntry *draggedEntry = [[document findEntry:entryUUID] copyWithTitle:nil options:kKPKCopyOptionNone]; + [draggedEntry addToGroup:targetGroup]; + [draggedEntry.undoManager setActionName:NSLocalizedString(@"COPY_ENTRY", "Action title for copying an entry via drag and drop")]; + } + } + else { + for(NSUUID *entryUUID in entryUUIDs) { + KPKEntry *draggedEntry = [document findEntry:entryUUID]; + [draggedEntry moveToGroup:targetGroup]; + [draggedEntry.undoManager setActionName:NSLocalizedString(@"MOVE_ENTRY", "Action title for moving an entry via drag and drop")]; + } + } + return YES; + } + for(KPKEntry *draggedEntry in [self _entriesFromPasteboard:info.draggingPasteboard]) { + [draggedEntry addToGroup:targetGroup]; + [draggedEntry.undoManager setActionName:NSLocalizedString(@"DRAG_ENTRY", "Action title for copying an entry via drag and drop to another database")]; + } + for(KPKGroup *draggedGroup in [self _normalizedGroupsFromPasterboard:info.draggingPasteboard]) { + [draggedGroup addToGroup:targetGroup]; + [draggedGroup.undoManager setActionName:NSLocalizedString(@"DRAG_GROUP", "Actiontitle for copying groups via drag and drop to antother database")]; + } + return YES; } -- (BOOL)_readDataFromPasteboard:(NSPasteboard *)pasteboard group:(KPKGroup **)group entry:(KPKEntry **)entry;{ - - if(entry == NULL || group == NULL) { - return NO; // Need valid pointers - } - /* Cleanup old stuff */ - - NSArray *types = pasteboard.types; - if(types.count > 1 || types.count == 0) { - return NO; - } - - NSString *draggedType = types.lastObject; - if([draggedType isEqualToString:KPKGroupUTI]) { - // dragging group - NSArray *groups = [pasteboard readObjectsForClasses:@[KPKGroup.class] options:nil]; - if(groups.count != 1) { - return NO; +- (NSArray *)_entryUUIDsFromPasteboard:(NSPasteboard *)pBoard { + if([pBoard.types containsObject:KPKEntryUUDIUTI]) { + if([pBoard canReadObjectForClasses:@[NSUUID.class] options:nil]) { + return [pBoard readObjectsForClasses:@[NSUUID.class] options:nil]; } - *group = groups.lastObject; - return YES; } - else if([draggedType isEqualToString:KPKEntryUTI]) { - NSArray *entries = [pasteboard readObjectsForClasses:@[KPKEntry.class] options:nil]; - if([entries count] != 1) { - return NO; // NO entry readable + return @[]; +} + +- (NSArray *)_normalizedGroupsFromPasterboard:(NSPasteboard *)pBoard { + if([pBoard.types containsObject:KPKGroupUTI]) { + if([pBoard canReadObjectForClasses:@[KPKGroup.class] options:nil]) { + NSArray *groups = [pBoard readObjectsForClasses:@[KPKGroup.class] options:nil]; + return [groups filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary * _Nullable bindings) { + KPKGroup *group = evaluatedObject; + BOOL isValid = YES; + for(KPKGroup *otherGroup in groups) { + if(otherGroup == group) { + continue; + } + if(nil != [otherGroup groupForUUID:group.uuid]) { + isValid = NO; + break; + } + } + return isValid; + }]]; } - *entry = entries.lastObject; - return YES; } - return NO; + return @[]; +} + +- (NSArray *)_entriesFromPasteboard:(NSPasteboard *)pBoard { + if([pBoard.types containsObject:KPKEntryUTI]) { + if([pBoard canReadObjectForClasses:@[KPKEntry.class] options:nil]) { + return [pBoard readObjectsForClasses:@[KPKEntry.class] options:nil]; + } + } + return @[]; } @end diff --git a/MacPass/MPOutlineViewController.m b/MacPass/MPOutlineViewController.m index 1b27e5bf..cfc56e6f 100644 --- a/MacPass/MPOutlineViewController.m +++ b/MacPass/MPOutlineViewController.m @@ -92,7 +92,7 @@ NSString *const _MPOutlinveViewHeaderViewIdentifier = @"HeaderCell"; self.outlineView.doubleAction = @selector(_doubleClickedGroup:); self.outlineView.allowsMultipleSelection = YES; self.outlineView.delegate = self; - [self.outlineView registerForDraggedTypes:@[ KPKGroupUTI, KPKEntryUTI ]]; + [self.outlineView registerForDraggedTypes:@[ KPKGroupUTI, KPKGroupUUIDUTI, KPKEntryUTI, KPKEntryUUDIUTI ]]; [self.outlineView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:YES]; [NSNotificationCenter.defaultCenter addObserver:self diff --git a/MacPass/NSIndexPath+MPAdditions.h b/MacPass/NSIndexPath+MPAdditions.h new file mode 100644 index 00000000..8d691701 --- /dev/null +++ b/MacPass/NSIndexPath+MPAdditions.h @@ -0,0 +1,19 @@ +// +// NSIndexPath+MPAdditions.h +// MacPass +// +// Created by Michael Starke on 07.11.18. +// Copyright © 2018 HicknHack Software GmbH. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSIndexPath (MPAdditions) + +- (BOOL)containsIndexPath:(NSIndexPath *)path; + +@end + +NS_ASSUME_NONNULL_END diff --git a/MacPass/NSIndexPath+MPAdditions.m b/MacPass/NSIndexPath+MPAdditions.m new file mode 100644 index 00000000..e72de9eb --- /dev/null +++ b/MacPass/NSIndexPath+MPAdditions.m @@ -0,0 +1,34 @@ +// +// NSIndexPath+MPAdditions.m +// MacPass +// +// Created by Michael Starke on 07.11.18. +// Copyright © 2018 HicknHack Software GmbH. All rights reserved. +// + +#import "NSIndexPath+MPAdditions.h" + +@implementation NSIndexPath (MPAdditions) + +- (BOOL)containsIndexPath:(NSIndexPath *)path { + NSComparisonResult result = [self compare:path]; + if(result == NSOrderedSame) { + return YES; + } + if(result == NSOrderedDescending) { + return NO; + } + if(self.length == path.length) { + return NO; + } + + NSUInteger commonLength = MIN(self.length, path.length); + for(NSUInteger position = 0; position < commonLength; position++) { + if([self indexAtPosition:position] != [path indexAtPosition:position]) { + return NO; + } + } + return YES; +} + +@end diff --git a/MacPassTests/MPTestIndexPathAdditions.m b/MacPassTests/MPTestIndexPathAdditions.m new file mode 100644 index 00000000..bfdeae4d --- /dev/null +++ b/MacPassTests/MPTestIndexPathAdditions.m @@ -0,0 +1,53 @@ +// +// MPTestIndexPathAdditions.m +// MacPassTests +// +// Created by Michael Starke on 07.11.18. +// Copyright © 2018 HicknHack Software GmbH. All rights reserved. +// + +#import +#import "NSIndexPath+MPAdditions.h" + +@interface MPTestIndexPathAdditions : XCTestCase + +@end + +@implementation MPTestIndexPathAdditions + +- (void)testContainsA { + NSUInteger indexes1[] = {0,1,2,3}; + NSUInteger indexes2[] = {0,1,3}; + + NSIndexPath *path1 = [[NSIndexPath alloc] initWithIndexes:indexes1 length:sizeof(indexes1)/sizeof(NSUInteger)]; + NSIndexPath *path2 = [[NSIndexPath alloc] initWithIndexes:indexes2 length:sizeof(indexes2)/sizeof(NSUInteger)]; + + XCTAssertFalse([path1 containsIndexPath:path2]); + XCTAssertFalse([path2 containsIndexPath:path1]); +} + +- (void)testContainsB { + NSUInteger indexes1[] = {0,2,3}; + NSUInteger indexes2[] = {0,2}; + + NSIndexPath *path1 = [[NSIndexPath alloc] initWithIndexes:indexes1 length:sizeof(indexes1)/sizeof(NSUInteger)]; + NSIndexPath *path2 = [[NSIndexPath alloc] initWithIndexes:indexes2 length:sizeof(indexes2)/sizeof(NSUInteger)]; + + XCTAssertFalse([path1 containsIndexPath:path2]); + XCTAssertTrue([path2 containsIndexPath:path1]); +} + +- (void)testContainsC { + NSUInteger indexes1[] = {10,1,3}; + NSUInteger indexes2[] = {10,1,3}; + + NSIndexPath *path1 = [[NSIndexPath alloc] initWithIndexes:indexes1 length:sizeof(indexes1)/sizeof(NSUInteger)]; + NSIndexPath *path2 = [[NSIndexPath alloc] initWithIndexes:indexes2 length:sizeof(indexes2)/sizeof(NSUInteger)]; + + XCTAssertTrue([path1 containsIndexPath:path2]); + XCTAssertTrue([path2 containsIndexPath:path1]); +} + + + +@end