// // P4Item.m // Perforce // // Created by Adam Czubernat on 25.07.2013. // Copyright (c) 2013 Perforce Software, Inc. All rights reserved. // #import "P4Item.h" #import <objc/objc-runtime.h> @interface P4Item () <P4WorkspaceDelegate> { NSArray *tags, *lowercaseTags; } @property (nonatomic, retain) P4ItemAction *lockedAction; + (NSMutableArray *)targets; - (void)insertFiles:(NSArray *)paths copy:(BOOL)copy; - (void)loadingAction:(P4ItemAction *)action message:(NSString *)message; - (void)finishAction:(P4ItemAction *)action response:(NSArray *)response error:(NSError *)error; - (BOOL)promptAction:(P4ItemAction *)action message:(NSString *)message; @end @implementation P4Item @synthesize lockedAction; - (id)init { self = [super init]; PSInstanceCreated([self class]); return self; } - (void)dealloc { if (!parent) [[P4Workspace sharedInstance] removeObserver:self]; PSInstanceDeallocated([self class]); } - (NSString *)description { return [NSString stringWithFormat:@"%@ : %@", NSStringFromClass([self class]), remotePath ?: localPath]; } #pragma mark - Public - (P4Item *)parent { return parent; } - (NSString *)name { return name; } - (NSString *)path { return localPath ?: remotePath; } - (NSString *)localPath { return localPath; } - (NSString *)remotePath { return remotePath; } - (BOOL)isDirectory { return flags.directory; } - (NSArray *)children { if (!children && !flags.loading && !flags.failure) { flags.loading = YES; [self loadPath:self.path]; } return children; } - (BOOL)isLoading { return flags.loading; } - (BOOL)hasError { return flags.failure; } - (BOOL)isTracked { return flags.tracked; } - (BOOL)hasMapped { return flags.hasMapped; } - (BOOL)isMapped { return flags.mapped; } - (BOOL)isIgnored { return flags.ignored; } - (BOOL)isUnread { return flags.unread; } - (BOOL)isShelved { return flags.shelved; } - (NSString *)status { return status; } - (NSString *)statusOwner { return statusOwner; } - (NSArray *)tags { return tags; }; - (BOOL)hasTag:(NSString *)tag { return [lowercaseTags containsObject:[tag lowercaseString]]; } - (NSDictionary *)metadata { return metadata; } - (NSString *)iconName { NSString *file; if (flags.directory) { if (flags.ignored) file = @"IconFolderIgnored"; else if (flags.mapped) file = @"IconFolderMapped"; else if (flags.tracked || !parent) file = @"IconFolder"; else if (flags.hasMapped) file = @"IconFolderMixed"; else file = @"IconFolderUntracked"; } else { NSString *action = status ?: [metadata objectForKey:@"otherAction0"]; if (action) { if ([action isEqualToString:@"add"]) file = @"IconFileAdd"; else if ([action isEqualToString:@"edit"]) file = @"IconFileCheckout"; else if ([action isEqualToString:@"delete"]) file = @"IconFileDelete"; else if ([action isEqualToString:@"move/add"]) file = @"IconFileMoveAdd"; else if ([action isEqualToString:@"move/delete"]) file = @"IconFileMoveDelete"; if (!status && statusOwner) file = [file stringByAppendingString:@"Other"]; } else if (flags.tracked) file = @"IconFile"; else file = @"IconFileUntracked"; } return file; } - (NSImage *)icon { return [NSImage imageNamed:[[self iconName] stringByAppendingString:@".png"]]; } - (NSImage *)iconHighlighted { return [NSImage imageNamed:[[self iconName] stringByAppendingString:@"_alt.png"]]; } - (NSColor *)overlay { return nil; // UNUSED CGFloat COLOR_H = 0.9f; CGFloat COLOR_L = 0.6f; BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:localPath]; if (flags.ignored) return [NSColor colorWithDeviceRed:COLOR_H green:COLOR_L blue:COLOR_L alpha:1.0f]; if (flags.tracked || flags.mapped) { if (exists) return [NSColor colorWithDeviceRed:COLOR_L green:COLOR_H blue:COLOR_L alpha:1.0f]; return [NSColor colorWithDeviceRed:COLOR_L green:0.7 blue:COLOR_L alpha:1.0f]; } return [NSColor colorWithDeviceRed:COLOR_L green:COLOR_L blue:COLOR_L alpha:1.0f]; } - (NSImage *)previewWithSize:(CGSize)size { CGFloat minSize = fminf(size.width, size.height); if (localPath) { // Generate real preview NSImage *image = [NSImage imageWithFilePreview:localPath size:CGSizeMake(minSize, minSize) icon:YES]; if (image) return image; } // File not found generate preview from generic icon NSString *fileType; if (flags.directory) fileType = NSFileTypeForHFSTypeCode(kGenericFolderIcon); else fileType = remotePath.pathExtension; NSImage *image = [[NSWorkspace sharedWorkspace] iconForFileType:fileType]; [image setSize:CGSizeMake(minSize, minSize)]; return image; } #pragma mark - Actions - (void)open { if (flags.directory) return; // Open using default app [[NSWorkspace sharedWorkspace] openFile:localPath]; // Mark as read if (flags.unread) [self markAsRead]; } - (void)openFromDepot { if (flags.directory) return; P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Opening file from depot..."]; NSString *tmpPath = [NSString stringWithFormat:@"/tmp/p4/%@/%@", [[P4Workspace sharedInstance] workspace], name]; [[P4Workspace sharedInstance] runCommand:[NSString stringWithFormat:@"print -o \"%@\" \"%@\"", tmpPath, remotePath] response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response error:operation.error]; if (!operation.errors) [[NSWorkspace sharedWorkspace] openFile:tmpPath]; }]; } - (void)openWithCheckout { [self checkout]; [self open]; } - (void)showInFinder { if (localPath.length) [[NSWorkspace sharedWorkspace] selectFile:localPath inFileViewerRootedAtPath:@""]; } - (void)checkout { if (status) return; // Prompt for checking out folder if (flags.directory && [self promptAction:lockedAction message: [NSString stringWithFormat:@"Would you like to checkout '%@' " "directory with all of its contents?", name]] == NO) return; [self checkoutItems:@[ self ]]; } - (void)checkoutItems:(NSArray *)items { P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Checking out..."]; NSMutableArray *paths = [NSMutableArray arrayWithCapacity:items.count]; for (P4Item *item in items) [paths addObject:item.localPath]; [[P4Workspace sharedInstance] editFiles:paths response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response error:operation.error]; }]; } - (void)addItem { [self addItems:@[ self ]]; } - (void)addItems:(NSArray *)items { P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Adding files..."]; NSMutableArray *paths = [NSMutableArray arrayWithCapacity:items.count]; for (P4Item *item in items) [paths addObject:item.localPath]; [[P4Workspace sharedInstance] addFiles:paths response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response error:operation.error]; }]; } - (void)checkIn { [self checkInItems:@[ self ]]; } - (void)checkInItems:(NSArray *)items { P4ItemAction *action = lockedAction; [self finishAction:action response:[items valueForKey:@"localPath"] error:nil]; } - (void)checkInAll { if (!flags.directory) return; P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Check-in all files..."]; [[P4Workspace sharedInstance] listPendingFiles:@[ localPath ] response:^(P4Operation *operation, NSArray *response) { NSArray *paths = [response valueForKey:@"depotFile"]; NSError *error = (operation.error ?: paths.count ? nil : [NSError errorWithFormat:@"No files to check-in"]); [self finishAction:action response:paths error:error]; }]; } - (void)deleteItem { [self deleteItems:@[ self ]]; } - (void)deleteItems:(NSArray *)items { P4ItemAction *action = lockedAction; NSMutableArray *paths = [NSMutableArray arrayWithCapacity:items.count]; for (P4Item *item in items) [paths addObject:item.localPath]; NSString *prompt = paths.count > 4 ? [NSString stringWithFormat:@"Would you like to delete %ld items?", paths.count] : [NSString stringWithFormat:@"Would you like to delete\n%@ ?", [[items valueForKey:@"path"] componentsJoinedByString:@"\n"]]; if (![self promptAction:action message:prompt]) return; [self loadingAction:action message:@"Deleting files..."]; // Remove files NSError *error; NSFileManager *filemanager = [NSFileManager defaultManager]; for (NSString *path in paths) { if (![filemanager removeItemAtPath:path error:&error]) { [self finishAction:action response:nil error:error]; return; } } [[P4Workspace sharedInstance] removeFiles:paths response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response error:operation.error]; }]; } - (void)revert { [self revertItems:@[ self ]]; } - (void)revertItems:(NSArray *)items { P4ItemAction *action = lockedAction; NSMutableArray *paths = [NSMutableArray arrayWithCapacity:items.count]; for (P4Item *item in items) [paths addObject:item.localPath]; NSString *prompt = paths.count > 4 ? [NSString stringWithFormat:@"Would you like to revert %ld items?", paths.count] : [NSString stringWithFormat:@"Would you like to revert\n%@ ?", [[items valueForKey:@"path"] componentsJoinedByString:@"\n"]]; if (![self promptAction:action message:prompt]) return; [self loadingAction:action message:@"Reverting files..."]; [[P4Workspace sharedInstance] revertFiles:paths response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response error:operation.error]; }]; } - (void)revertIfUnchanged { P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Undo checkout..."]; [[P4Workspace sharedInstance] revertUnchangedFiles:@[ localPath ] response:^(P4Operation *operation, NSArray *response) { NSDictionary *revert = [response lastObject]; NSString *clientAction = [revert objectForKey:@"action"]; NSString *clientFile = [revert objectForKey:@"clientFile"]; if ([clientAction isEqualToString:@"reverted"] && [clientFile isEqualToString:localPath]) { [self finishAction:action response:response error:operation.error]; } else { NSError *error = [NSError errorWithFormat: @"Action couldn't complete - file was changed.\n" "Please revert or submit pending changes"]; [self finishAction:action response:response error:error]; } }]; } - (void)mapToWorkspace { P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Setting workspace mapping..."]; [[P4Workspace sharedInstance] mappingSet:YES path:remotePath response:^(P4Operation *operation, NSArray *response) { PSLog(@"Map response %@", response); [self finishAction:action response:response error:operation.error]; }]; } - (void)unmapFromWorkspace { P4ItemAction *action = lockedAction; // Prompt for unmapping existing folder if ([[NSFileManager defaultManager] fileExistsAtPath:localPath] && [self promptAction:lockedAction message: [NSString stringWithFormat:@"Would you like to unmap '%@' " "directory with all of its contents?", name]] == NO) return; [self loadingAction:action message:@"Setting workspace mapping..."]; [[P4Workspace sharedInstance] mappingSet:NO path:remotePath response:^(P4Operation *operation, NSArray *response) { PSLog(@"Unmap response %@", response); [self finishAction:action response:response error:operation.error]; }]; } - (void)markAsRead { if (flags.directory || !flags.unread) return; [[P4Workspace sharedInstance] markFilesRead:@[ localPath ]]; flags.unread = NO; [self finishLoading]; // Propagate read change through parents P4Item *parentItem = self; while ((parentItem = parentItem->parent) && parentItem->parent) { BOOL hasUpdated = NO; for (P4Item *parentChild in parentItem->children) if (parentChild->flags.unread) { hasUpdated = YES; break; } if (hasUpdated) break; parentItem->flags.unread = NO; [parentItem finishLoading]; } } - (void)markItemsAsRead:(NSArray *)items { for (P4Item *item in items) { [item markAsRead]; } } - (void)markAllAsRead { if (!flags.directory || !flags.unread) return; [[P4Workspace sharedInstance] markFilesRead:@[ localPath ]]; flags.unread = NO; [self finishLoading]; // Propagate read change through parents P4Item *parentItem = self; while ((parentItem = parentItem->parent) && parentItem->parent) { BOOL hasUpdated = NO; for (P4Item *parentChild in parentItem->children) if (parentChild->flags.unread) { hasUpdated = YES; break; } if (hasUpdated) break; parentItem->flags.unread = NO; [parentItem finishLoading]; } // Propagate read change through children if (!children) // Not loaded yet return; NSMutableArray *queue = [NSMutableArray array]; if (children.count) [queue addObjectsFromArray:children]; while (queue.count) { P4Item *child = [queue lastObject]; [queue removeLastObject]; if (!child->flags.unread) continue; // Has updated items child->flags.unread = NO; if (child->children.count) [queue addObjectsFromArray:child->children]; [child finishLoading]; } } - (void)shelve { if (flags.directory || !status) return; if (flags.shelved && [self promptAction:lockedAction message: [NSString stringWithFormat: @"Would you like to overwrite currently shelved file version " "of '%@' ?", name]] == NO) return; P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Shelving file..."]; [[P4Workspace sharedInstance] shelveFile:remotePath ?: localPath response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response error:operation.error]; }]; } - (void)unshelve { P4ItemAction *action = lockedAction; // Prompt for overwriting if (![self promptAction:lockedAction message: [NSString stringWithFormat: @"Would you like to unshelve and overwrite current" " workspace file '%@' ?", name]]) return; [self loadingAction:action message:@"Unshelving..."]; [[P4Workspace sharedInstance] unshelveFile:remotePath ?: localPath response:^(P4Operation *operation, NSArray *response) { [self finishLoading]; [self finishAction:action response:response error:operation.error]; }]; } - (void)discardShelve { P4ItemAction *action = lockedAction; // Prompt for discarding if (![self promptAction:lockedAction message: [NSString stringWithFormat: @"Would you like to discard shelved version of" " workspace file '%@' ?", name]]) return; [self loadingAction:action message:@"Discarding shelved version..."]; [[P4Workspace sharedInstance] discardShelvedFiles:@[ remotePath ?: localPath ] response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response error:operation.error]; }]; } - (void)openShelve { if (flags.directory) return; P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Opening shelved version..."]; [[P4Workspace sharedInstance] openShelvedFile:remotePath response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response error:operation.error]; }]; } - (void)showVersions { P4ItemAction *action = lockedAction; [self finishAction:action response:[self valueForKey:@"path"] error:nil]; } - (void)copyShareLink { NSString *path = [@"p4:" stringByAppendingString:remotePath]; path = [path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; // NSURL *url = [[NSURL alloc] initWithString:path]; NSPasteboard *pboard = [NSPasteboard generalPasteboard]; [pboard clearContents]; [pboard setString:path forType:NSPasteboardTypeString]; // [pboard writeObjects:@[ path ]]; // Didn't work on apps like MS Outlook } - (void)openVersion:(NSString *)versionPath { if (flags.directory) return; NSRange range = [versionPath rangeOfString:@"#" options:NSBackwardsSearch]; NSString *version = [versionPath substringFromIndex:range.location]; NSString *filePath = [versionPath substringToIndex:range.location]; NSString *filename = [[filePath lastPathComponent] stringByDeletingPathExtension]; filename = [filename stringByAppendingString:version]; filename = [filename stringByAppendingPathExtension:[filePath pathExtension]]; P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Opening previous file version..."]; NSString *tmp = [NSString stringWithFormat:@"/tmp/p4/%@/%@", [[P4Workspace sharedInstance] workspace], filename]; [[P4Workspace sharedInstance] runCommand:[NSString stringWithFormat:@"print -o \"%@\" \"%@\"", tmp, versionPath] response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response error:operation.error]; if (!operation.errors) [[NSWorkspace sharedWorkspace] openFile:tmp]; }]; } - (void)revertToVersion:(NSString *)versionPath { P4ItemAction *action = lockedAction; NSString *prompt = [NSString stringWithFormat:@"Would you like to promote " "version to latest?\n%@", versionPath]; if (![self promptAction:action message:prompt]) return; [self loadingAction:action message:@"Promoting file version..."]; // Sync file with specified revision [[P4Workspace sharedInstance] runCommand:[NSString stringWithFormat:@"sync -f \"%@\"", versionPath] response:^(P4Operation *operation, NSArray *response) { if (operation.errors) { NSArray *openedErrors = [operation errorsWithCode:P4ErrorOpenedLaterRevision]; NSString *openedPath = [[openedErrors lastObject] localizedFailureReason]; // Revert by syncing latest revision [[P4Workspace sharedInstance] runCommand:[NSString stringWithFormat:@"sync \"%@\"", openedPath] response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response error:operation.error]; }]; return; } NSDictionary *record = response.count ? [response objectAtIndex:0] : nil; NSString *syncPath = [record objectForKey:@"depotFile"]; NSString *syncAction = [record objectForKey:@"action"]; PSLog(@"Reverting %@\n\tPath %@\n\tAction %@", versionPath, syncPath, syncAction); if ([syncAction isEqualToString:@"added"]) { [[P4Workspace sharedInstance] addFiles:@[ syncPath ] response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response error:operation.error]; }]; } else if ([syncAction isEqualToString:@"updated"]) { [[P4Workspace sharedInstance] editFiles:@[ syncPath ] response:^(P4Operation *operation, NSArray *response) { NSArray *resolveErrors = [operation errorsWithCode:P4ErrorMustSyncResolve]; NSArray *resolvePaths = [resolveErrors valueForKeyPath:@"localizedFailureReason"]; [operation ignoreErrors:resolveErrors]; // Finish if there are errors or there's no path to resolve if (operation.errors || !resolvePaths.count) { [self finishAction:action response:response error:operation.error]; return; } [[P4Workspace sharedInstance] resolveFiles:resolvePaths response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response error:operation.error]; PSLog(@"Resolved %@", response); }]; }]; } else { [self finishAction:action response:response error: [NSError errorWithFormat:@"Unknown promote response %@", syncAction]]; } }]; } - (BOOL)isFavoriteFolder { return [[[P4WorkspaceDefaults sharedInstance] favoriteFolders] containsObject:self.path]; } - (void)addFavoriteFolder { if (flags.directory && !self.isFavoriteFolder) [[P4WorkspaceDefaults sharedInstance] addFavoriteFolder:self.path]; } - (void)removeFavoriteFolder { if (flags.directory && self.isFavoriteFolder) [[P4WorkspaceDefaults sharedInstance] removeFavoriteFolder:self.path]; } #pragma mark - Edit Actions - (BOOL)isEditable { return NO; } - (void)createDirectory { P4ItemAction *action = lockedAction; if (!action) action = [P4ItemAction actionForItem:self name:@"Create directory" selector:@selector(createDirectory)]; if (!localPath) { [self finishAction:action response:nil error: [NSError errorWithFormat: @"Couldn't create directory in item without local path"]]; return; } NSFileManager *filemanager = [NSFileManager defaultManager]; NSString *untitledPath = [localPath stringByAppendingPath:@"untitled folder"]; NSString *destinationPath = untitledPath; NSInteger number = 1; while ([filemanager fileExistsAtPath:destinationPath]) destinationPath = [untitledPath stringByAppendingFormat:@" %ld", number++]; destinationPath = [destinationPath stringByAppendingString:@"/"]; NSError *error; if (![filemanager createDirectoryAtPath:destinationPath withIntermediateDirectories:NO attributes:nil error:&error]) { [self finishAction:action response:nil error:error]; return; } [self fileCreated:destinationPath]; [self finishAction:action response:@[ destinationPath ] error:error]; } - (void)rename:(NSString *)newName { P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Renaming..."]; NSFileManager *filemanager = [NSFileManager defaultManager]; NSString *src = localPath; NSString *dst = [[localPath stringByDeletingPath] stringByAppendingPath:newName]; // Append '/' slash to directories BOOL dir = [filemanager fileExistsAtPath:src isDirectory:&dir] && dir; dst = dir ? [dst stringByAppendingString:@"/"] : dst; // Move a file NSError *error; if (![filemanager moveItemAtPath:src toPath:dst error:&error]) { [self finishAction:action response:nil error:error]; return; } [[P4Workspace sharedInstance] moveFiles:@[ src ] toPaths:@[ dst ] response:^(P4Operation *operation, NSArray *response) { if (!flags.tracked) [operation ignoreErrorsWithCode:P4ErrorFileNotOpened]; //rename isn't fully tested [self finishAction:action response:response error:operation.error]; }]; } - (void)addFiles:(NSArray *)paths { [self loadingAction:lockedAction message:@"Adding files..."]; [self insertFiles:paths copy:YES]; } - (void)copyFiles:(NSArray *)paths { [self loadingAction:lockedAction message:@"Copying files..."]; [self insertFiles:paths copy:YES]; } - (void)moveFiles:(NSArray *)paths { [self loadingAction:lockedAction message:@"Moving files..."]; [self insertFiles:paths copy:NO]; } - (void)addTag:(NSString *)tag { P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Adding tag..."]; tag = [tag stringByReplacingOccurrencesOfString:@"," withString:@"-"]; tag = [[tag componentsSeparatedByCharactersInSet: [NSCharacterSet whitespaceCharacterSet]] componentsJoinedByString:@"-"]; NSString *attr = [metadata objectForKey:@"openattr-tags"]; attr = attr.length ? [attr stringByAppendingFormat:@",%@", tag] : tag; [[P4Workspace sharedInstance] setAttribute:@"tags" value:attr paths:@[ remotePath ?: localPath ] response:^(P4Operation *operation, NSArray *response) { NSDictionary *dict = [response lastObject]; if ([[dict objectForKey:@"status"] isEqualToString:@"set"]) { NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:metadata]; [dict setObject:attr forKey:@"openattr-tags"]; metadata = dict; [self refreshTags]; } [self finishAction:action response:response error:operation.error]; [self finishLoading]; }]; } - (void)removeTag:(NSString *)tag { P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Removing tag..."]; NSString *attr = [metadata objectForKey:@"openattr-tags"]; if (!attr.length) { [self finishAction:action response:nil error:[NSError errorWithFormat:@"There's no pending tags to remove"]]; return; } NSMutableArray *tagsArr = [[attr componentsSeparatedByString:@","] mutableCopy]; [tagsArr removeObject:tag]; attr = tagsArr.count ? [tagsArr componentsJoinedByString:@","] : nil; [[P4Workspace sharedInstance] setAttribute:@"tags" value:attr paths:@[ remotePath ] response:^(P4Operation *operation, NSArray *response) { if (!operation.errors) { NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:metadata]; if (attr) [dict setObject:attr forKey:@"openattr-tags"]; else [dict removeObjectForKey:@"openattr-tags"]; metadata = dict; [self refreshTags]; } [self finishAction:action response:response error:operation.error]; [self finishLoading]; }]; } #pragma mark - Contextual actions - (NSArray *)actions { // Default actions P4ItemAction *action; NSMutableArray *actions = [NSMutableArray array]; BOOL dir = NO; BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:localPath isDirectory:&dir]; if (localPath.length && !flags.directory) { action = [P4ItemAction actionForItem:self name:@"Open" selector:@selector(openWithCheckout)]; action.disabled = !exists || dir; [actions addObject:action]; action = [P4ItemAction actionForItem:self name:@"Open read only" selector:@selector(open)]; action.disabled = !exists || dir; [actions addObject:action]; } if (flags.directory) { [actions addObject:[P4ItemAction actionForItem:self name:@"Check out all files" selector:@selector(checkout)]]; [actions addObject:[P4ItemAction actionForItem:self name:@"Check-in all files..." selector:@selector(checkInAll)]]; } if (flags.directory && parent) { if ([self isFavoriteFolder]) [actions addObject:[P4ItemAction actionForItem:self name:@"Remove from favorites" selector:@selector(removeFavoriteFolder)]]; else [actions addObject:[P4ItemAction actionForItem:self name:@"Add to favorites" selector:@selector(addFavoriteFolder)]]; } if (!flags.directory && !status) { if (flags.tracked) [actions addObject:[P4ItemAction actionForItem:self name:@"Check out" selector:@selector(checkout)]]; else [actions addObject:[P4ItemAction actionForItem:self name:@"Mark for add" selector:@selector(addItem)]]; } if (status) { [actions addObject:[P4ItemAction actionForItem:self name:@"Check-in..." selector:@selector(checkIn)]]; } if (flags.unread) { if (flags.directory) [actions addObject:[P4ItemAction actionForItem:self name:@"Mark all as read" selector:@selector(markAllAsRead)]]; else [actions addObject:[P4ItemAction actionForItem:self name:@"Mark as read" selector:@selector(markAsRead)]]; } if (localPath.length) { action = [P4ItemAction actionForItem:self name:@"Show in Finder" selector:@selector(showInFinder)]; action.disabled = !exists || dir != flags.directory; [actions addObject:action]; } if (!flags.directory && flags.tracked) { [actions addObject:[P4ItemAction actionForItem:self name:@"Show Versions" selector:@selector(showVersions)]]; } if (flags.tracked && parent) [actions addObject:[P4ItemAction actionForItem:self name:@"Copy link" selector:@selector(copyShareLink)]]; if (status && // ![status isEqualToString:@"delete"] && // Shelve only edited but not deleted files // ![status isEqualToString:@"move/delete"] [status isEqualToString:@"edit"]) { // Shelve only when edited if (flags.shelved) [actions addObject:[P4ItemAction actionForItem:self name:@"Shelve new version..." selector:@selector(shelve)]]; else [actions addObject:[P4ItemAction actionForItem:self name:@"Shelve" selector:@selector(shelve)]]; } else if (flags.shelved) { [actions addObject:[P4ItemAction actionForItem:self name:@"Unshelve..." selector:@selector(unshelve)]]; } // Manage shelved version if (flags.shelved) { [actions addObject:[P4ItemAction actionForItem:self name:@"Open shelved version" selector:@selector(openShelve)]]; [actions addObject:[P4ItemAction actionForItem:self name:@"Discard shelved version" selector:@selector(discardShelve)]]; } if (status) { if ([status isEqualToString:@"edit"]) [actions addObject:[P4ItemAction actionForItem:self name:@"Undo checkout" selector:@selector(revertIfUnchanged)]]; if ([status isEqualToString:@"delete"]) [actions addObject:[P4ItemAction actionForItem:self name:@"Undo delete" selector:@selector(revert)]]; else [actions addObject:[P4ItemAction actionForItem:self name:@"Revert changes" selector:@selector(revert)]]; } if (flags.directory && self.isEditable) { [actions addObject:[P4ItemAction actionForItem:self name:@"New directory" selector:@selector(createDirectory)]]; } if (flags.directory && parent && flags.tracked) { NSDictionary *mapping = [[[P4Workspace sharedInstance] mappingForPaths:@[ localPath ]] lastObject]; NSString *map = [mapping objectForKey:@"action"]; action = [P4ItemAction actionForItem:self name:nil selector:@selector(unmapFromWorkspace)]; if ([map isEqualToString:@"tracked"]) { action.name = @"Ignore folder in Workspace"; } else if ([map isEqualToString:@"mapped"]) { action.name = @"Unmap from Workspace"; } else { action.name = @"Not mapped into Workspace"; action.disabled = YES; } [actions addObject:action]; } if (flags.directory && parent) { [actions addObject:[P4ItemAction actionForItem:self name:@"Delete all files" selector:@selector(deleteItem)]]; } else if (flags.directory) { // Don't delete root directory } else if ([status isEqualToString:@"delete"]) { // Don't delete } else if ([status isEqualToString:@"move/delete"]) { // Don't delete } else if (flags.tracked || status) { [actions addObject:[P4ItemAction actionForItem:self name:@"Mark for delete" selector:@selector(deleteItem)]]; } else { [actions addObject:[P4ItemAction actionForItem:self name:@"Delete local file" selector:@selector(deleteItem)]]; } return actions; } - (NSArray *)actionsForItems:(NSArray *)items { // Default actions return [NSMutableArray array]; } - (id)defaultAction { return nil; } - (void)performAction:(SEL)selector object:(id)object delegate:(id)delegate { P4ItemAction *action = [P4ItemAction actionForItem:self name:nil selector:selector]; action.object = object; action.delegate = delegate; [action performAction]; } - (void)performAction:(SEL)selector items:(NSArray *)items delegate:(id)delegate { P4ItemAction *action = [P4ItemAction actionForItems:items name:nil selector:selector]; action.delegate = delegate; [action performAction]; } #pragma mark - Utils + (void)addObserver:(id<P4ItemDelegate>)observer { NSMutableArray *targets = [[self class] targets]; if (![targets containsObject:observer]) [targets addObject:observer]; } + (void)removeObserver:(id <P4ItemDelegate>)observer { NSMutableArray *targets = [[self class] targets]; if ([targets containsObject:observer]) [targets removeObject:observer]; } - (void)loadPath:(NSString *)path PS_ABSTRACT_METHOD - (P4Item *)cachedItemForPath:(NSString *)filePath { BOOL remote = [filePath hasPrefix:@"//"]; NSString *parentPath = remote ? remotePath : localPath; if (!filePath.length) return self; if ([filePath isEqualToString:parentPath]) return self; // Children for whole path because it is not under parent's path if (!parentPath || ![filePath hasPrefix:parentPath]) { for (P4Item *child in self->children) if ([remote ? child->remotePath : child->localPath isEqualToString:filePath]) return child; return nil; } NSString *relative = [filePath substringFromIndex:parentPath.length]; relative = [relative stringByRemovingSuffix:@"/"]; NSArray *components = [relative pathComponents]; // Traversing P4Item *item = self; for (NSString *component in components) { if (!item->children.count) // Not loaded return nil; for (P4Item *child in item->children) { if ([child->name isEqualToString:component]) { item = child; break; } } } NSString *itemPath = remote ? item->remotePath : item->localPath; if (itemPath && ![itemPath isEqualToString:filePath]) return nil; return item; } - (void)reload { self->children = nil; self->metadata = nil; [self refreshTags]; [self finishLoading]; self->flags.loading = YES; } #pragma mark - Protected - (void)sortChildren { // Sort files alphanumerically children = [children sortedArrayUsingComparator: ^NSComparisonResult(P4Item *obj1, P4Item *obj2) { return [obj1.name localizedStandardCompare:obj2.name]; }]; } - (void)finishLoading { flags.loading = NO; flags.failure = NO; for (id <P4ItemDelegate> target in [[self class] targets]) { if ([target respondsToSelector:@selector(itemDidLoad:)]) [target itemDidLoad:self]; } } - (void)failWithError:(NSError *)error { flags.loading = NO; flags.failure = YES; for (id <P4ItemDelegate> target in [[self class] targets]) { if ([target respondsToSelector:@selector(item:didFailWithError:)]) [target item:self didFailWithError:error]; } } - (void)invalidate { flags.loading = NO; flags.failure = NO; for (id <P4ItemDelegate> target in [[self class] targets]) { if ([target respondsToSelector:@selector(itemDidInvalidate:)]) [target itemDidInvalidate:self]; } } - (void)refreshTags { NSString *attr = ([metadata objectForKey:@"openattr-tags"] ?: [metadata objectForKey:@"attr-tags"]); tags = [attr componentsSeparatedByString:@","]; lowercaseTags = [tags valueForKeyPath:@"lowercaseString"]; } #pragma mark - Private + (NSMutableArray *)targets { static char targetsKey; NSMutableArray *array = objc_getAssociatedObject(self, &targetsKey); if (!array) { CFArrayCallBacks callbacks = { 0, NULL, NULL, CFCopyDescription, CFEqual }; array = CFBridgingRelease(CFArrayCreateMutable(NULL, 0, &callbacks)); objc_setAssociatedObject(self, &targetsKey, array, OBJC_ASSOCIATION_RETAIN); } return array; } - (void)insertFiles:(NSArray *)paths copy:(BOOL)copy { P4ItemAction *action = lockedAction; NSFileManager *filemanager = [NSFileManager defaultManager]; NSMutableArray *sources = [NSMutableArray arrayWithCapacity:paths.count]; NSMutableArray *destinations = [NSMutableArray arrayWithCapacity:paths.count]; for (NSString *src in paths) { // Remove files to overwrite NSString *filename = src.lastPathComponent; NSString *dest = [localPath stringByAppendingPath:filename]; // Inserting into same location if ([src isEqualToString:dest]) { if (copy) { // Make duplicate NSString *ext = [dest pathExtension]; NSString *copyPath = [[dest stringByDeletingPathExtension] stringByAppendingString:@" copy"]; dest = [copyPath stringByAppendingPathExtension:ext]; NSInteger number = 1; while ([filemanager fileExistsAtPath:dest]) dest = [copyPath stringByAppendingFormat:@" %ld.%@", number++, ext]; } else { // Can't move into same location [self finishAction:action response:nil error: [NSError errorWithFormat:@"Can't move '%@' into the same location", filename]]; return; } } if ([filemanager fileExistsAtPath:dest]) { // Ask if overwrite NSString *prompt = [NSString stringWithFormat: @"An item named '%@' already exists in this location. " "Do you want to replace it with the one you're moving ?", filename]; if (![self promptAction:action message:prompt]) continue; // Overwrite [filemanager removeItemAtPath:dest error:NULL]; } // Append '/' slash to directories BOOL dir = [filemanager fileExistsAtPath:src isDirectory:&dir] && dir; [destinations addObject:dir ? [dest directoryPath] : dest]; [sources addObject:dir ? [src directoryPath] : src]; // Move / copy a file NSError *error; if (copy ? ![filemanager copyItemAtPath:src toPath:dest error:&error] : ![filemanager moveItemAtPath:src toPath:dest error:&error]) { // Filemanager error [self finishAction:action response:nil error:error]; return; } } if (!destinations.count) { // Nothing to move [self finishAction:action response:nil error:nil]; return; } P4ResponseBlock_t block = ^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response error:operation.error]; }; if (copy) [[P4Workspace sharedInstance] addFiles:destinations response:block]; else [[P4Workspace sharedInstance] moveFiles:sources toPaths:destinations response:block]; } - (void)loadingAction:(P4ItemAction *)action message:(NSString *)message { if ([action.delegate respondsToSelector:@selector(action:loadingMessage:)]) [action.delegate action:action loadingMessage:message]; } - (void)finishAction:(P4ItemAction *)action response:(NSArray *)response error:(NSError *)error { if ([action.delegate respondsToSelector:@selector(action:didFinish:error:)]) [action.delegate action:action didFinish:response error:error]; } - (BOOL)promptAction:(P4ItemAction *)action message:(NSString *)message { if ([action.delegate respondsToSelector:@selector(action:prompt:)]) return [action.delegate action:action prompt:message]; return YES; } #pragma mark - P4Workspace delegate #pragma mark Filesystem Events - (void)fileCreated:(NSString *)filePath { P4Item *item = [self cachedItemForPath:filePath]; [item finishLoading]; } - (void)fileRemoved:(NSString *)filePath { P4Item *item = [self cachedItemForPath:filePath]; [item finishLoading]; } - (void)fileMoved:(NSString *)oldPath toPath:(NSString *)newPath { P4Item *item = [self cachedItemForPath:oldPath]; [item finishLoading]; item = [self cachedItemForPath:newPath]; [item finishLoading]; } - (void)fileRenamed:(NSString *)oldPath toPath:(NSString *)newPath { P4Item *item = [self cachedItemForPath:oldPath]; [item finishLoading]; item = [self cachedItemForPath:newPath]; [item finishLoading]; } - (void)fileModified:(NSString *)filePath { P4Item *item = [self cachedItemForPath:filePath]; [item finishLoading]; } #pragma mark P4 Events - (void)file:(NSString *)filePath actionChanged:(NSDictionary *)info { NSString *action = [info objectForKey:@"action"]; P4Item *item = [self cachedItemForPath:filePath]; if (item) { item->remotePath = [info objectForKey:@"depotFile"]; item->status = action; item->flags.tracked = YES; } [item finishLoading]; } - (void)file:(NSString *)filePath revertedAction:(NSDictionary *)info { // NSString *oldAction = [info objectForKey:@"oldAction"]; P4Item *item = [self cachedItemForPath:filePath]; if (item) { NSString *action = [info objectForKey:@"action"]; NSNumber *revision = [info objectForKey:@"rev"]; BOOL reverted = [action isEqualToString:@"reverted"]; item->status = nil; item->flags.tracked = reverted || revision; NSString *pendingTags = [item->metadata objectForKey:@"openattr-tags"]; if (pendingTags) { NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:item->metadata]; [dict removeObjectForKey:@"openattr-tags"]; if (!reverted) [dict setObject:pendingTags forKey:@"attr-tags"]; item->metadata = dict; [item refreshTags]; } } [item finishLoading]; } - (void)file:(NSString *)filePath updated:(NSDictionary *)info { // Empty } - (void)file:(NSString *)filePath mappingChanged:(NSDictionary *)info { P4Item *item = [self cachedItemForPath:filePath]; if (!item) return; // Isn't loaded NSString *action = [info objectForKey:@"action"]; BOOL ignored = [action isEqualToString:@"ignored"]; BOOL mapped = [action isEqualToString:@"mapped"]; if (info && item->flags.ignored == ignored && item->flags.mapped == mapped) return; // No change if (info) { item->flags.ignored = ignored; item->flags.tracked = !ignored; item->flags.mapped = mapped; } else { // Take into account inherited mapping from parent item->flags.ignored = NO; item->flags.mapped = NO; if (!item->parent) item->flags.tracked = NO; else if (item->parent->flags.mapped || item->parent->flags.tracked) item->flags.tracked = YES; else item->flags.tracked = NO; } // Propagate mapping change through parents P4Item *parentItem = item; while ((parentItem = parentItem->parent) && parentItem->parent) { if (item->flags.mapped || item->flags.hasMapped) { // Mapping if (parentItem->flags.hasMapped) break; parentItem->flags.hasMapped = YES; } else { // Unmapping BOOL hasMapped = NO; for (P4Item *parentChild in parentItem->children) if (parentChild->flags.mapped || parentChild->flags.hasMapped) { hasMapped = YES; break; } if (hasMapped) break; parentItem->flags.hasMapped = NO; } [parentItem finishLoading]; } [item finishLoading]; // Propagate mapping change through children if (!item->children) // Not loaded yet return; NSMutableArray *queue = [NSMutableArray array]; if (item->children.count) [queue addObjectsFromArray:item->children]; while (queue.count) { P4Item *child = [queue lastObject]; [queue removeLastObject]; if (child->flags.mapped || child->flags.ignored) continue; // Has own mapping child->flags.tracked = item->flags.tracked; if (child->children.count) [queue addObjectsFromArray:child->children]; [child finishLoading]; } } - (void)file:(NSString *)filePath shelved:(NSDictionary *)info { P4Item *item = [self cachedItemForPath:filePath]; if (!item) return; item->flags.shelved = info != nil; [item finishLoading]; } #pragma mark - QuickLook Preview Item - (NSURL *)previewItemURL { return localPath ? [NSURL fileURLWithPath:localPath] : nil; } - (NSString *)previewItemTitle { return name; } @end #pragma mark - P4ItemAction implementation @implementation P4ItemAction @synthesize delegate, item, items, name, selector, object, disabled; - (void)setItems:(NSArray *)anItems { // Create non retaining array of items CFArrayCallBacks callbacks = { 0, NULL, NULL, CFCopyDescription, CFEqual }; items = CFBridgingRelease(CFArrayCreateMutable(NULL, 0, &callbacks)); [(NSMutableArray *)items addObjectsFromArray:anItems]; } + (id)actionForItem:(P4Item *)item name:(NSString *)name selector:(SEL)selector { P4ItemAction *action = [[P4ItemAction alloc] init]; action.item = item; action.name = name; action.selector = selector; return action; } + (id)actionForItems:(NSArray *)items name:(NSString *)name selector:(SEL)selector { P4ItemAction *action = [[P4ItemAction alloc] init]; action.items = items; action.item = [items lastObject]; action.name = name; action.selector = selector; return action; } - (void)performAction { NSAssert(item, @"P4ItemAction should have an item"); NSAssert(!(items && object), @"Received Action with object and items"); @synchronized(item) { item.lockedAction = self; if (![item respondsToSelector:selector]) return; if (items) objc_msgSend(item, selector, items); else if (object) objc_msgSend(item, selector, object); else if (item) objc_msgSend(item, selector); item.lockedAction = nil; } } @end