// // P4MenuController.m // MBMenuExtra // // Created by Michael Bishop on 10/25/09. // Copyright 2009 Perforce Software. All rights reserved. // #import #import "P4MenuController.h" #import "P4MenuStatusViewController.h" #import "NGAActiveFileMonitor.h" #import "NGAAutoObserver.h" #import "P4MenuApplicationDelegate.h" #import "P4DiffTool.h" #import "P4ServerEntry.h" #import "P4Connection.h" #import "P4Port.h" #import "P4SpecManager.h" #import "P4FilePath.h" #import "P4Spec.h" #import "P4User.h" #import "P4Client.h" #import "Generic.h" #import "P4ServerStatusImageTransformer.h" #import "P4Response.h" #import "P4MenuLocalFile.h" #import "P4LocalFileManager.h" #import "NGAUtilities.h" #import "SCMSubmitDialog.h" #import "P4MenuStatusViewController.h" #import "NGAMailMessage.h" #include "P4ErrorCodes.h" #include "P4Connection.h" static NSString * const kCanAddFileStatusKey = @"P4MenuControllerCanAddFile"; static NSString * const kClientsAllRootsKey = @"arrangedObjects.roots"; static NSString * const kClientsAllClientViewsKey = @"arrangedObjects.view"; static BOOL hasFileCoordination = YES; //#define CALC_DIGESTS @interface P4MenuController () -(BOOL)P4_fileCanDiff; -(BOOL)P4_fileHasDifferences; -(void)P4_activeFileChanged:(NSNotification*)notification; -(void)P4_updateStatusMenuItem; -(void)P4_showError:(NSError*)error; -(NSImage*)P4_menuIconForState; -(NSImage*)P4_fileBadge; -(NSImage*)P4_imageNamed:(NSString*)name; -(NSImage*)P4_imageForStatus:(P4ServerStatus)status; -(int)P4_fileTypeForTypeString:(NSString*)typeString; -(NSDictionary*)P4_context; -(void)P4_updateMenuUI; -(void)P4_updateCurrentClient; -(void)P4_unobserveCurrentFile; -(void)P4_observeCurrentFile; -(void)P4_observeFileSafelyWhilePerformingBlock:(void(^)())block; -(BOOL)P4_runSimpleFileCommand:(NSString*)command arguments:(NSArray*)arguments completionBlock:(void(^)(P4Response *response))completion; -(void)P4_runCommands:(NSArray*)commands onCurrentFileWithCoordinatingOptions:(NSFileCoordinatorWritingOptions)options completionBlock:(void(^)(P4Response *response))completion; -(P4Client*)clientForLocalPath:(NSString*)localPath; @property (nonatomic, readwrite, copy) NSDictionary * currentLocalFileFStatInfo; @property (nonatomic, readwrite, retain) P4Spec * currentLocalFileChange; @property (nonatomic, readwrite, retain) P4Client * currentClient; @property (nonatomic, readwrite, retain) P4ServerEntry * currentServerEntry; @property (nonatomic, readwrite, retain) P4Connection * problemConnection; @property (nonatomic, readwrite, copy) NSDictionary * currentAdditionalFileInfo; @end @implementation P4MenuController @synthesize enabled = _enabled, currentLocalFile = _currentLocalFile, currentLocalFileFStatInfo = _currentFileFStatInfo, currentAdditionalFileInfo = _currentAdditionalFileInfo, currentLocalFileChange = _currentLocalFileChange, currentClient = _currentClient, currentServerEntry = _currentServerEntry, currentWindowTitle = _currentWindowTitle, problemConnection = _problemConnection, servers = _servers; +(void)initialize { [NSValueTransformer setValueTransformer:[[[P4ServerStatusImageTransformer alloc] init] autorelease] forName:NSStringFromClass([P4ServerStatusImageTransformer class])]; [NSValueTransformer setValueTransformer:[[[P4ServerStatusMessageTransformer alloc] init] autorelease] forName:NSStringFromClass([P4ServerStatusMessageTransformer class])]; hasFileCoordination = NSClassFromString(@"NSFileCoordinator") != nil; if ( !hasFileCoordination ) LOG_DEBUG(@"System does not have NSFileCoordinator"); } #pragma mark -SETTING_UP_OBSERVATIONS -(id)initWithMainController:(P4MenuApplicationDelegate*)mainController { if ((self = [super init]) == nil) return nil; _statusViewController = [[P4MenuStatusViewController alloc] init]; _statusViewController.menuController = self; if ([NSBundle loadNibForOwner:self] == NO) { [self release]; return NULL; } // Here, we watch all the roots in all the clients. If they change, it's possible // the front file will actually use another client the next time zfstat is run. [_allClientsController addObserver:self forKeyPath:kClientsAllRootsKey options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:0]; [_allClientsController addObserver:self forKeyPath:kClientsAllClientViewsKey options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:0]; _statusViewControllerObservationToken = [[_statusViewController addObserverForKeyPath:@"view" task:^(id obj, NSDictionary *change) { [_statusMenuItem setView:[obj view]]; }] retain]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(P4_activeFileChanged:) name:NGAActiveFileChangedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(P4_windowTitleChanged:) name:NGAWindowTitleChangedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidFinishLaunching:) name:NSApplicationDidFinishLaunchingNotification object:NSApp]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:NSApp]; _scmMenuExtraControllerWeakRef = mainController; [self registerAllAutoObservationMethods]; return self; } -(void)dealloc { [_statusViewController removeObserverWithBlockToken:_statusViewControllerObservationToken]; [_statusViewControllerObservationToken release]; [_allClientsController removeObserver:self forKeyPath:kClientsAllRootsKey]; [_allClientsController removeObserver:self forKeyPath:kClientsAllClientViewsKey]; [self unregisterAllAutoObservationMethods]; RELEASE(_currentObservationToken); [[NSNotificationCenter defaultCenter] removeObserver:self]; [[NSStatusBar systemStatusBar] removeStatusItem:_menuStatusItem]; RELEASE(_menuStatusItem); RELEASE(_servers); RELEASE(_currentFileFStatInfo); RELEASE(_currentAdditionalFileInfo); RELEASE(_currentLocalFileChange); RELEASE(_currentLocalFile); RELEASE(_currentWindowTitle); RELEASE(_currentClient); RELEASE(_submitDialogController); RELEASE(_statusViewController); RELEASE(_problemConnection); [super dealloc]; } /** Returns the support directory for the application, used to store the Core Data store file. This code uses a directory named "TrashCoreDataApp" for the content, either in the NSApplicationSupportDirectory location or (if the former cannot be found), the system's temporary directory. */ - (NSString *)applicationSupportDirectory { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); NSString *basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : NSTemporaryDirectory(); return [basePath stringByAppendingPathComponent:@"P4MenuExtra"]; } - (NSString*)applicationDataFilePath { return [[self applicationSupportDirectory] stringByAppendingPathComponent:@"Servers.data"]; } -(void)applicationDidFinishLaunching:(NSNotification*)notification { //#ifndef DEBUG // NSInteger itemIndex = [[_sendDebugLogMenuItem menu] indexOfItem:_sendDebugLogMenuItem]; // NSMenuItem * separatorItem = [[_sendDebugLogMenuItem menu] itemAtIndex:itemIndex+1]; // // [_sendDebugLogMenuItem setHidden:YES]; // [separatorItem setHidden:YES]; //#endif NSArray * newServers = nil; @try { newServers = [NSKeyedUnarchiver unarchiveObjectWithFile:[self applicationDataFilePath]]; } @catch (NSException *exception) { } @finally { } if (newServers && [newServers count] > 0) { self.servers = newServers; } else { self.servers = [NSArray arrayWithObject:[[[P4ServerEntry alloc] init] autorelease]]; } [self refreshAll:self]; } -(void)applicationWillTerminate:(NSNotification*)notification { NSError * error = nil; if ( ![[NSFileManager defaultManager] createDirectoryAtPath:[self applicationSupportDirectory] withIntermediateDirectories:YES attributes:nil error:&error] ) { [NSApp presentError:error]; return; } [NSKeyedArchiver archiveRootObject:self.servers toFile:[self applicationDataFilePath]]; } -(void)awakeFromNib { [_statusMenuItem setView:[_statusViewController view]]; } #pragma mark -OBSERVING - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { // LOG_DEBUG(@"KEYPATH changed: [%@]\nCHANGE: %@\nNEW_VALUE:%@", keyPath, change, [object valueForKeyPath:keyPath]); // if ( [keyPath isEqualToString:kClientsAllRootsKey] || [keyPath isEqualToString:kClientsAllClientViewsKey] ) { [self P4_updateCurrentClient]; return; } [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } -(void)valueDidChangeForCurrentClient { [self.currentFileManager fetchInformationForLocalFilePath:self.currentLocalFile.localFilepath]; [self P4_updateMenuUI]; } -(void)valueDidChangeForCurrentLocalFileFStatInfo { // There's no fstat info, maybe we can add the file... if ( self.currentLocalFileFStatInfo ) { self.currentAdditionalFileInfo = [self.currentAdditionalFileInfo dictionaryByRemovingObjectForKey:kCanAddFileStatusKey]; } else { [self.currentClient runArguments:NSARRAY(@"add", @"-n", self.currentLocalFile.localFilepath) withContext:[self P4_context] completionBlock:^(P4Response *response) { if (response.error || self.currentLocalFileFStatInfo) { self.currentAdditionalFileInfo = [self.currentAdditionalFileInfo dictionaryByRemovingObjectForKey:kCanAddFileStatusKey]; } else { NSMutableDictionary * newDict = [NSMutableDictionary dictionaryWithDictionary:self.currentAdditionalFileInfo]; [newDict setObject:[NSNumber numberWithBool:YES] forKey:kCanAddFileStatusKey]; self.currentAdditionalFileInfo = newDict; } [self P4_updateMenuUI]; }]; } [self P4_updateMenuUI]; NSString * changeName = [self.currentLocalFileFStatInfo objectForKey:@"haveChange"]; if ( !changeName ) changeName = [self.currentLocalFileFStatInfo objectForKey:@"headChange"]; if ( !changeName ) { self.currentLocalFileChange = nil; return; } self.currentLocalFileChange = [[self.currentClient manager] specOfType:@"change" identifier:changeName createIfNotFound:YES]; } - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { SEL selector = [menuItem action]; if ( selector == @selector(refreshAll:) ) return YES; if ( selector == @selector(sendDebugLog:) ) return YES; if ( selector == @selector(dummyStatusItem:) ) { [self P4_updateStatusMenuItem]; return NO; } if ( !self.currentLocalFile.localFilepath ) return NO; if ( selector == @selector(openForAdd:) ) { return [[self.currentAdditionalFileInfo valueForKey:kCanAddFileStatusKey] boolValue]; } NSDictionary * fstatInfo = self.currentLocalFileFStatInfo; if ( !fstatInfo ) return NO; int haveRev = [[fstatInfo objectForKey:@"haveRev"] intValue]; int headRev = [[fstatInfo objectForKey:@"headRev"] intValue]; NSString *action = [fstatInfo objectForKey:@"action"]; NSString *ourLock = [fstatInfo objectForKey:@"ourLock"]; NSString *otherLock = [fstatInfo objectForKey:@"otherLock"]; NSString *otherLock0 = [fstatInfo objectForKey:@"otherLock0"]; if ( selector == @selector(toggleLock:) ) { if ( otherLock ) { [menuItem setTitle:[NSString stringWithFormat:NSLocalizedString(@"Locked by (%@)", @"Locked menu item"), otherLock0]]; return NO; } if ( ourLock ) { [menuItem setTitle:NSLocalizedString(@"Unlock", @"Unlock menu item")]; return YES; } [menuItem setTitle:NSLocalizedString(@"Lock", @"Lock menu item")]; return action != nil; } if ( selector == @selector(diff:) ) { return [self P4_fileCanDiff]; } if ( selector == @selector(showSubmitDialog:) ) { return action != nil; } if ( selector == @selector(sync:) ) { return haveRev < headRev; } if ( selector == @selector(openForEdit:) ) { return action == nil || [action isEqualToString:@"integrate"]; } if ( selector == @selector(openForDelete:) ) { return action == nil; } if ( selector == @selector(revert:) ) { NSString * revertString = NSLocalizedString(@"Revert", @"Revert menu item"); // compare digests if the action is not an add if ( [self P4_fileCanDiff ] ) [menuItem setTitle:[revertString stringByAppendingString:@"..."]]; else [menuItem setTitle:revertString]; return action != nil; } return NO; } #pragma mark PRIVATE -(void)P4_updateMenuUI { [self P4_updateStatusMenuItem]; [_menuStatusItem setImage:[self P4_menuIconForState]]; } -(BOOL)P4_fileCanDiff { if (![self P4_fileHasDifferences]) return NO; NSDictionary * fstatInfo = self.currentLocalFileFStatInfo; if ( !fstatInfo ) return NO; NSString *action = [fstatInfo objectForKey:@"action"]; if ( [@"add" isEqualToString:action] ) return NO; if ( [@"delete" isEqualToString:action] ) return NO; return YES; } -(BOOL)P4_fileHasDifferences { NSString *digest = [self.currentLocalFileFStatInfo objectForKey:@"digest"]; // Assume there are differences if we cannot calculate them if ( !digest ) return YES; #ifdef CALC_DIGESTS // do a digest comparison here #endif return YES; } -(void)P4_updateStatusMenuItem { if( !self.currentLocalFile ) { [_statusMenuItem setTitle:NSLocalizedString(@"No Front File", @"Displayed when no file could be detected")]; return; } NSString * fileName = self.currentLocalFile.filename; if( !self.currentLocalFileFStatInfo ) { NSString * title = [NSString stringWithFormat:@"'%@' - %@", fileName, NSLocalizedString(@"No Information", @"Displayed when there is no information to be had from the file")]; [_statusMenuItem setTitle:title]; return; } NSString *haveRev = [self.currentLocalFileFStatInfo objectForKey:@"haveRev"]; NSString *headRev = [self.currentLocalFileFStatInfo objectForKey:@"headRev"]; NSString *action = [self.currentLocalFileFStatInfo objectForKey:@"action"]; NSString * title = nil; if ( action ) title = [NSString stringWithFormat:@"'%@' (%@/%@) - %@", fileName, haveRev, headRev, action]; else title = [NSString stringWithFormat:@"'%@' (%@/%@)", fileName, haveRev, headRev]; [_statusMenuItem setTitle:title]; } -(void)P4_activeFileChanged:(NSNotification*)notification { NSString * currentfile = [[notification userInfo] objectForKey:NGAActiveFileMonitorNewKey]; if ( [currentfile isEqual:[NSNull null]] ) currentfile = nil; if ( NSSTRING_ISEQUALTOSTRING(currentfile, self.currentLocalFile.localFilepath) ) return; BOOL isDirectory = NO; if ( [[NSFileManager defaultManager] fileExistsAtPath:currentfile isDirectory:&isDirectory] && !isDirectory ) self.currentLocalFile = [P4MenuLocalFile localFileWithLocalFilepath:currentfile]; else self.currentLocalFile = nil; } -(void)P4_windowTitleChanged:(NSNotification*)notification { id currentWindowTitle = [[notification userInfo] objectForKey:NGAActiveFileMonitorNewKey]; if ( [currentWindowTitle isEqual:[NSNull null]] ) currentWindowTitle = nil; if ( [currentWindowTitle length] <= 0 ) currentWindowTitle = nil; if ( NSSTRING_ISEQUALTOSTRING(currentWindowTitle, self.currentWindowTitle) ) return; self.currentWindowTitle = currentWindowTitle; } -(void)P4_showError:(NSError*)error { [NSApp presentError:error]; } -(NSDictionary*)P4_context { NSMutableDictionary * context = [NSMutableDictionary dictionary]; if ( self.currentLocalFile.localFilepath ) [context setObject:self.currentLocalFile.localFilepath forKey:kP4ConnectionCWDContextKey]; return [NSDictionary dictionaryWithDictionary:context]; } static GenericWindow * windowForDocument( GenericApplication * app, NSString * documentPath ) { if ( !app ) return nil; NSUInteger index = [app.windows indexOfObjectPassingTest:^BOOL(id window, NSUInteger idx, BOOL *stop) { @try { if ( NSSTRING_ISEQUALTOSTRING([[window document] path], documentPath) ) { *stop = YES; // assign and return in one statment return YES; } } @catch (NSException *exception) { LOG_DEBUG(@"Exception thrown while searching for window: %@", exception); } return NO; }]; if ( index == NSNotFound ) return nil; return [app.windows objectAtIndex:index]; } -(void)P4_openAndCloseDocumentAtPath:(NSString*)path whileExecutingBlock:(void(^)(NSString*))block { // Close document GenericApplication * currentApp = (GenericApplication *)[SBApplication applicationWithProcessIdentifier:NGAActiveFileMonitor.sharedMonitor.monitoredApplication.processIdentifier]; NSRect windowBounds = NSMakeRect(0, 0, 0, 0); // To reset the window when opening again GenericWindow * window = windowForDocument( currentApp, path ); if (window) { @try { windowBounds = window.bounds; } @catch (NSException *exception) { LOG_DEBUG(@"retrieving window bounds:'%@' EXCEPTION:%@", path, exception); } } if (window) { @try { [[window document] closeSaving:GenericSavoNo savingIn:[NSURL fileURLWithPath:path]]; } @catch (NSException *exception) { LOG_DEBUG(@"saving the document:'%@' EXCEPTION:%@", path, exception); } } block( path ); // Open document and reset window if ( currentApp && [[NSFileManager defaultManager] fileExistsAtPath:path] ) { @try { [currentApp open:[NSURL fileURLWithPath:path]]; } @catch (NSException *exception) { LOG_DEBUG(@"Exception thrown while opening the document:'%@' EXCEPTION:%@", path, exception); } @try { if (windowBounds.size.height != 0.0) { GenericWindow * window = windowForDocument( currentApp, path ); window.bounds = windowBounds; } } @catch (NSException *exception) { LOG_DEBUG(@"Exception thrown while resetting the window bounds for the document:'%@' EXCEPTION:%@", path, exception); } } } -(void)coordinateWritingToPath:(NSString*)path withOptions:(NSFileCoordinatorWritingOptions)options whileExecutingBlock:(void(^)(NSString*))block { // Only coordinate if coordination exists on this machine... if ( !hasFileCoordination ) { block(path); return; } // Run within file coordination NSURL * fileUrl = [NSURL fileURLWithPath:path]; NSFileCoordinator * fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; NSError * error = nil; [fileCoordinator coordinateWritingItemAtURL:fileUrl options:options error:&error byAccessor:^(NSURL *newURL) { block(newURL.path); }]; [fileCoordinator release]; } -(void)P4_runCommands:(NSArray*)commands onCurrentFileWithCoordinatingOptions:(NSFileCoordinatorWritingOptions)options completionBlock:(void(^)(P4Response *response))completionBlock; { NSString * localFilepath = self.currentLocalFile.localFilepath; P4Client * client = [self clientForLocalPath:localFilepath]; if ( !client ) return; NSDictionary * context = [self P4_context]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self P4_openAndCloseDocumentAtPath:localFilepath whileExecutingBlock:^(NSString *path) { [self coordinateWritingToPath:path withOptions:options whileExecutingBlock:^(NSString *path) { NSArray * completeArguments = [commands arrayByAddingObject:path]; P4Response * response = [client runArguments:completeArguments withContext:context content:nil]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ completionBlock( response ); [[self fileManagerForLocalPath:localFilepath] fetchInformationForLocalFilePath:localFilepath]; }]; }]; }]; }); } -(BOOL)P4_runSimpleFileCommand:(NSString*)command arguments:(NSArray*)arguments completionBlock:(void(^)(P4Response *response))completion { NSString * localFilepath = self.currentLocalFile.localFilepath; P4Client * client = [self clientForLocalPath:localFilepath]; if ( !client ) return NO; NSArray * completeArguments = [[NSArray arrayWithObject:command] arrayByAddingObjectsFromArray:arguments]; completeArguments = [completeArguments arrayByAddingObject:localFilepath]; return [client runArguments:completeArguments withContext:[self P4_context] completionBlock:^(P4Response *response) { [[self fileManagerForLocalPath:localFilepath] fetchInformationForLocalFilePath:localFilepath]; completion(response); }]; } -(BOOL)P4_runSimpleFileCommand:(NSString*)command arguments:(NSArray*)arguments { return [self P4_runSimpleFileCommand:command arguments:arguments completionBlock:^(P4Response *response) { if ( ![response isSuccess] ) { [self P4_showError:response.error]; return; } }]; } -(NSImage*)P4_menuIconForState { NSImage * baseImage = nil; if ( !self.currentLocalFileFStatInfo ) baseImage = [self P4_imageNamed:@"menu_bar_icon_empty"]; else baseImage = [self P4_imageNamed:@"menu_bar_icon"]; baseImage = [[baseImage copy] autorelease]; [baseImage lockFocus]; NSPoint origin = NSMakePoint(0,0); [baseImage compositeToPoint:origin operation:NSCompositeCopy]; [[self P4_fileBadge] compositeToPoint:origin operation:NSCompositeSourceOver]; [baseImage unlockFocus]; return baseImage; } -(NSImage*)P4_fileBadge { if ( !self.currentLocalFileFStatInfo ) return nil; if ( [self.currentLocalFileFStatInfo objectForKey:@"unresolved"] ) return [self P4_imageNamed:@"resolve.png"]; NSString * action = [self.currentLocalFileFStatInfo objectForKey:@"action"]; if ( action ) { if ( [action isEqualToString:@"edit"] ) return [self P4_imageNamed:@"edit.png"]; if ( [action isEqualToString:@"add"] ) return [self P4_imageNamed:@"add.png"]; if ( [action isEqualToString:@"del"] ) return [self P4_imageNamed:@"delete.png"]; } else { int haveRev = [[self.currentLocalFileFStatInfo objectForKey:@"haveRev"] intValue]; int headRev = [[self.currentLocalFileFStatInfo objectForKey:@"headRev"] intValue]; if ( headRev > haveRev ) return [self P4_imageNamed:@"dated.png"]; else return [self P4_imageNamed:@"head.png"]; } return nil; } -(NSImage*)P4_imageNamed:(NSString*)name { NSBundle *thisBundle = [NSBundle bundleForClass:[self class]]; NSString * imagePath = nil; if (!(imagePath = [thisBundle pathForImageResource:name])) return nil; return [[[NSImage alloc] initByReferencingFile:imagePath] autorelease]; } -(NSImage*)P4_imageForStatus:(P4ServerStatus)status { if ( status == P4ServerStatusOffline ) return [self P4_imageNamed:@"red"]; if ( status == P4ServerStatusError ) return [self P4_imageNamed:@"yellow"]; if ( status == P4ServerStatusOnline ) return [self P4_imageNamed:@"green"]; NSAssert( NO, @"Unrecognized Status" ); return nil; } -(int)P4_fileTypeForTypeString:(NSString*)typeString { if ( [typeString rangeOfString:@"text"].location != NSNotFound ) return kP4FileTypeText; if ( [typeString rangeOfString:@"binary"].location != NSNotFound ) return kP4FileTypeBinary; if ( [typeString rangeOfString:@"symlink"].location != NSNotFound ) return kP4FileTypeSymlink; if ( [typeString rangeOfString:@"apple"].location != NSNotFound ) return kP4FileTypeApple; if ( [typeString rangeOfString:@"unicode"].location != NSNotFound ) return kP4FileTypeUnicode; if ( [typeString rangeOfString:@"utf16"].location != NSNotFound ) return kP4FileTypeUTF16; if ( [typeString rangeOfString:@"resource"].location != NSNotFound ) return kP4FileTypeResource; return kP4FileTypeUnknown; } #pragma mark PUBLIC -(void)setEnabled:(BOOL)enabled { if ( enabled == _enabled ) return; _enabled = enabled; if ( enabled ) { _menuStatusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength]; [_menuStatusItem retain]; [_menuStatusItem setHighlightMode:YES]; [_menuStatusItem setMenu:_menu]; [_menuStatusItem setImage:[self P4_menuIconForState]]; } else { [[NSStatusBar systemStatusBar] removeStatusItem:_menuStatusItem]; [_menuStatusItem release]; _menuStatusItem = nil; } } -(IBAction)sync:(id)sender { [self P4_runCommands:NSARRAY(@"sync") onCurrentFileWithCoordinatingOptions:0 completionBlock:^(P4Response *response) { if ( response.error ) [NSApp presentError:response.error]; }]; } -(IBAction)toggleLock:(id)sender { NSString * ourLock = [self.currentLocalFileFStatInfo objectForKey:@"ourLock"]; NSString * command = ourLock ? @"unlock" : @"lock"; [self P4_runSimpleFileCommand:command arguments:nil]; } -(IBAction)diff:(id)sender { if (![self P4_fileCanDiff]) return; // First, print out the file NSString * currentFilePath = self.currentLocalFile.localFilepath; P4Client * client = self.currentClient; NSString * action = [self.currentLocalFileFStatInfo objectForKey:@"action"]; NSString * revisionToDiff = [self.currentLocalFileFStatInfo objectForKey:@"haveRev"]; // If the file is not open for edit, we will diff the previous version if (!action || ![action isEqualToString:@"edit"]) revisionToDiff = [[NSNumber numberWithInteger:([revisionToDiff integerValue] - 1)] stringValue]; NSString * utiHint = nil; NSString * type = [self.currentLocalFileFStatInfo objectForKey:@"type"]; if (!type) type = [self.currentLocalFileFStatInfo objectForKey:@"headType"]; // TODO: Need to handle 'apple','binary','resource','symlink' if ([type rangeOfString:@"binary"].location != NSNotFound) utiHint = (NSString*)kUTTypeData; if ([type rangeOfString:@"text"].location != NSNotFound) utiHint = (NSString*)kUTTypeText; if ([type rangeOfString:@"unicode"].location != NSNotFound) utiHint = (NSString*)kUTTypeText; if ([type rangeOfString:@"utf16"].location != NSNotFound) utiHint = (NSString*)kUTTypeUTF16PlainText; NSMutableDictionary * options = [NSMutableDictionary dictionaryWithCapacity:3]; [options setObject:@"" forKey:DiffOptionName1]; [options setObject:NSLocalizedString(@"Previous Revision", @"Previous Revision Diff File Name") forKey:DiffOptionName2]; [options setObjectIgnoringNil:utiHint forKey:DiffOptionUTIHint]; P4MutableFilePath * path = [P4MutableFilePath filePathParsedFromString:[self.currentLocalFileFStatInfo objectForKey:@"depotFile"]]; path.revision = revisionToDiff; [[P4LocalFileManager managerWithClient:client] contentForFilePath:path block:^(NSString *tempFile, NSError *error ) { if ( error ) { [NSApp presentError:error]; return; } [_scmMenuExtraControllerWeakRef diffFileAtPath:currentFilePath againstOlderFileAtPath:tempFile options:options]; }]; } -(IBAction)showSubmitDialog:(id)sender { if ( !_submitDialogController ) _submitDialogController = [[SCMSubmitDialog alloc] init]; _submitDialogController.submitter = self; _submitDialogController.filepath = _currentLocalFile.localFilepath; [_submitDialogController showWindow:self]; [NSApp activateIgnoringOtherApps:YES]; } -(IBAction)openForAdd:(id)sender { [self P4_runSimpleFileCommand:@"add" arguments:nil]; } -(IBAction)openForEdit:(id)sender { [self P4_runSimpleFileCommand:@"edit" arguments:nil]; } -(IBAction)openForDelete:(id)sender { [self P4_runCommands:NSARRAY(@"delete") onCurrentFileWithCoordinatingOptions:NSFileCoordinatorWritingForDeleting completionBlock:^(P4Response *response) { if ( response.error ) [NSApp presentError:response.error]; }]; } -(IBAction)revert:(id)sender { NSRunningApplication * monitoredApplication = [[NGAActiveFileMonitor sharedMonitor] monitoredApplication]; if ( [self P4_fileCanDiff ] ) { [NSApp activateIgnoringOtherApps:YES]; NSInteger chosenButton = NSRunAlertPanel( NSLocalizedString(@"Revert Confirmation", "Revert dialog title"), [NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to revert %@? (You cannot undo this)", @"Revert dialog message"), [self.currentLocalFile.localFilepath lastPathComponent]], NSLocalizedString(@"Cancel", @"Cancel Button"), NSLocalizedString(@"Show Differences", @"Button in Revert confirmation dialog"), NSLocalizedString(@"Revert All Changes", @"Button in Revert confirmation dialog"), nil); switch (chosenButton) { case NSAlertDefaultReturn: // LOG_DEBUG(@"activating %@", [monitoredApplication localizedName]); [monitoredApplication activateWithOptions:0]; return; break; case NSAlertAlternateReturn: // LOG_DEBUG(@"activating %@", [monitoredApplication localizedName]); [monitoredApplication activateWithOptions:0]; [self diff:self]; return; break; default: // Fall through... break; } } [self P4_runCommands:NSARRAY(@"revert") onCurrentFileWithCoordinatingOptions:0 completionBlock:^(P4Response *response) { if ( response.error ) [NSApp presentError:response.error]; }]; [monitoredApplication activateWithOptions:0]; } -(IBAction)refreshAll:(id)sender { for (P4ServerEntry * server in _servers) [server refreshAll:self]; } -(IBAction)dummyStatusItem:(id)sender { ; } -(IBAction)sendDebugLog:(id)sender { LOG_INFO(@"Current File: |%@|", self.currentLocalFile.localFilepath); LOG_INFO(@"Current Client: %@", self.currentClient); LOG_INFO(@"Current FStatInfo: %@", self.currentLocalFileFStatInfo); LOG_INFO(@"Current WindowTitle: |%@|", self.currentWindowTitle); LOG_INFO(@"Server Entries: %@", _servers); LOG_INFO(@"All Clients: %@", _allClientsController.arrangedObjects); NGAMailMessage * mailMessage = [[NGAMailMessage alloc] init]; mailMessage.recipients = NSARRAY(@"numericalgarden@me.com"); mailMessage.subject = @"Log Data for P4Menu"; mailMessage.contents = @"NOTES\n-----\n\n\n"; mailMessage.attachments = NSARRAY([NSURL fileURLWithPath:gLogFilePath]); [mailMessage openInMail:self]; [mailMessage release]; } #pragma mark - #pragma mark Servers Preference Pane - (NSView *)preferencePaneView { return _configurationPane; } - (NSImage *)preferencePaneIcon { return [self P4_imageNamed:@"P4PreferenceIcon"]; } - (NSString *)preferencePaneTitle { return @"Perforce"; } #pragma mark - #pragma mark LOCAL FILE DATA -(void)setCurrentLocalFile:(P4MenuLocalFile *)localFile { if ( [localFile isEqual:_currentLocalFile] ) return; [self P4_observeFileSafelyWhilePerformingBlock:^(){ [_currentLocalFile release]; _currentLocalFile = [localFile retain]; }]; [self P4_updateCurrentClient]; [self P4_updateMenuUI]; } +(NSSet *)keyPathsForValuesAffectingCurrentFileManager { return [NSSet setWithObjects:@"currentClient", nil]; } -(P4LocalFileManager*)currentFileManager { return [P4LocalFileManager managerWithClient:self.currentClient]; } -(P4Client*)clientForLocalPath:(NSString*)localPath serverEntry:(P4ServerEntry**)serverEntry { // slow implementation. Fast implementation would lookup from an optimized // clientRoot<->client map if ( !localPath ) return nil; P4Client * clientWithLongestRoot = nil; NSUInteger longestRootLength = 0; for (P4ServerEntry * server in self.servers) { for (P4Client * client in server.user.clients) { NSString * rootPath = nil; NSString * clientPath = [client clientPathForLocalFilePath:localPath root:&rootPath]; if ( ![client viewContainsClientPath:clientPath] ) continue; NSArray * clientRootPathComponents = [rootPath pathComponents]; if (clientRootPathComponents.count > longestRootLength) { longestRootLength = clientRootPathComponents.count; clientWithLongestRoot = client; if ( serverEntry ) *serverEntry = server; } } } // find the longest path and return that // if ( !clientWithLongestRoot ) // LOG_DEBUG( @"No client found for %@.", localPath ); // else // LOG_DEBUG( @"Using %@ for %@. Root: %@", clientWithLongestRoot.identifier, localPath, clientWithLongestRoot.root ); return clientWithLongestRoot; // One idea for a fast implementation: // When the clients refresh, make a tree of path components from all the // client roots and place client tags along it. This mimics where we'd put // .p4config files in a directory. This way we don't have to search through // all the client roots. /* / Users mbishop mbishopw dev work perforce_1666 computer_1666 [main] [main2] [main4] [main2] [dev2] [sub-client] */ } -(P4Client*)clientForLocalPath:(NSString*)localPath { return [self clientForLocalPath:localPath serverEntry:nil]; } -(P4LocalFileManager*)fileManagerForLocalPath:(NSString*)localPath { P4Client * client = [self clientForLocalPath:localPath]; if (!client) return nil; return [P4LocalFileManager managerWithPortString:client.connection.portString username:client.connection.username clientName:client.identifier]; } -(void)P4_unobserveCurrentFile { [_currentObservationToken unregister]; [_currentObservationToken release]; _currentObservationToken = nil; } -(void)P4_observeCurrentFile { P4LocalFileManager * fileManager = self.currentFileManager; [fileManager fetchInformationForLocalFilePath:self.currentLocalFile.localFilepath]; self.currentLocalFileFStatInfo = [fileManager informationForLocalFilePath:self.currentLocalFile.localFilepath]; __block P4MenuController * unretainedSelf = self; _currentObservationToken = [[P4FileObserverToken tokenRegisteringLocalFilePath:self.currentLocalFile.localFilepath withFileManager:fileManager observationBlock:^(P4LocalFileManager * fm, NSString *localFilePath, NSDictionary *fileInfo) { if ( ![unretainedSelf.currentLocalFile.localFilepath isEqualToString:localFilePath] || ![fm isEqual:unretainedSelf.currentFileManager]) return; unretainedSelf.currentLocalFileFStatInfo = fileInfo; }] retain]; } -(void)P4_observeFileSafelyWhilePerformingBlock:(void(^)())block { [self P4_unobserveCurrentFile]; block(); [self P4_observeCurrentFile]; } -(P4ServerEntry*)serverEntryWithPasswordError { for (P4ServerEntry * server in self.servers) { if (server.connection.needsNewPassword) { self.currentServerEntry = server; return server; } } return nil; } -(void)P4_updateCurrentClient { // For performance, only set it if it's different P4ServerEntry * problemServer = [self serverEntryWithPasswordError]; if ( problemServer ) { self.currentServerEntry = problemServer; self.currentClient = nil; return; } P4ServerEntry * serverEntry = nil; P4Client * newClient = [self clientForLocalPath:self.currentLocalFile.localFilepath serverEntry:&serverEntry]; if ( [self.currentClient isEqual:newClient] ) return; // Because the file manager is changing, we need to unobserve, then // re-observe [self P4_observeFileSafelyWhilePerformingBlock:^(){ self.currentClient = newClient; self.currentServerEntry = serverEntry; }]; } -(void)valueDidChangeForServersController_arrangedObjects_connection_needsNewPassword { P4ServerEntry * problemServer = [self serverEntryWithPasswordError]; if ( problemServer ) { self.currentClient = nil; self.currentServerEntry = problemServer; } else [self P4_updateCurrentClient]; } #pragma mark NSTextView Delegate Methods - (void)controlTextDidEndEditing:(NSNotification *)aNotification { P4ServerEntry * server = [[serversController selectedObjects] lastObject]; [server refreshAll:self]; } -(void)submitFile:(NSString*)filepath withDescription:(NSString*)description completionBlock:(void(^)(NSError*))completion { // If the file is a bundle, resolve all the files beneath // If the file is a normal file, submit it with the description P4Client * client = [self clientForLocalPath:filepath]; if ( !client ) return; [client runArguments:NSARRAY(@"submit", @"-d", description, filepath) withContext:[self P4_context] completionBlock:^(P4Response *response) { completion(response.error); [[self fileManagerForLocalPath:self.currentLocalFile.localFilepath] fetchInformationForLocalFilePath:self.currentLocalFile.localFilepath]; }]; } @end