// // MainWindowController.m // Perforce // // Created by Adam Czubernat on 09.05.2013. // Copyright (c) 2013 Perforce Software, Inc. All rights reserved. // #import "MainWindowController.h" #import "DebugWindowController.h" #import "LoginPanelController.h" #import "WorkspacePanelController.h" #import "SubmitPanelController.h" #import "SyncPanelController.h" #import "LoadingPanelController.h" #import "ErrorPanelController.h" #import "InfoPanelController.h" #import "SidebarViewController.h" #import "BrowserViewController.h" #import "ColumnViewController.h" #import "IconViewController.h" #import "P4Workspace.h" #import "P4Reachability.h" #import "P4Defaults.h" #define PANE_DEFAULT_WIDTH 200.0f #define PANE_MIN_WIDTH 150.0f #define PATH_DEFAULT_HEIGHT 40.0f @interface MainWindowController () <NSMenuDelegate, NSSplitViewDelegate, NSPathControlDelegate, LoginPanelControllerDelegate, WorkspacePanelControllerDelegate, SidebarViewControllerDelegate, BrowserViewDelegate, P4ItemActionDelegate> { NSMutableArray *redoStack; NSMutableArray *undoStack; SidebarViewController *sidebarController; BrowserViewController *browserController; LoginPanelController *loginPanel; LoadingPanelController *loadingPanel; __weak ErrorPanelController *errorPanel; __weak SubmitPanelController *submitPanel; InfoPanelController *infoPanel; DebugWindowController *debugController; P4ItemAction *pendingAction; // Subviews __weak IBOutlet PSView *_topBar; __weak IBOutlet PSSplitView *_mainSplitView; __weak IBOutlet NSView *_leftPane; __weak IBOutlet PSSplitView *_rightSplitView; __weak IBOutlet NSView *_rightTopPane; __weak IBOutlet NSBox *_rightBottomPane; __weak IBOutlet PSView *_pathSeparator; __weak IBOutlet NSPathControl *_pathControl; __weak IBOutlet NSView *_networkWarning; // Top Bar buttons __weak IBOutlet NSButton *_backButton; __weak IBOutlet NSButton *_nextButton; __weak IBOutlet NSButton *_addButton; __weak IBOutlet NSButton *_createDirectoryButton; __weak IBOutlet NSButton *_checkInAllButton; __weak IBOutlet NSButton *_viewColumnButton; __weak IBOutlet NSButton *_viewIconButton; __weak IBOutlet NSButton *_syncButton; __weak IBOutlet PSActivityIndicator *_syncButtonIndicator; __weak IBOutlet NSSearchField *_searchField; // Search __weak NSView *_searchBar; } - (void)changePath:(NSString *)path; - (void)next; - (void)back; - (void)clear; - (void)updateNavigationButtons; - (void)setPathControlItem:(P4Item *)item; - (void)setBrowserController:(BrowserViewController *)browser; - (void)syncWorkspace; - (void)addFiles; - (void)createDirectory; - (NSArray *)filterWorkspacePaths:(NSArray *)paths; - (void)showNetworkWarning:(BOOL)show animated:(BOOL)animated; // Notifications - (void)syncStartedNotification:(NSNotification *)notification; - (void)syncFinishedNotification:(NSNotification *)notification; - (void)submitFinishedNotification:(NSNotification *)notification; - (void)mappingUpdatedNotification:(NSNotification *)notification; - (void)sessionExpiredNotification:(NSNotification *)notification; - (void)networkReachabilityNotification:(NSNotification *)notification; // Actions - (void)pathControlDoubleClick:(id)sender; - (IBAction)backButtonPressed:(id)sender; - (IBAction)nextButtonPressed:(id)sender; - (IBAction)addButtonPressed:(id)sender; - (IBAction)createDirectoryButtonPressed:(id)sender; - (IBAction)checkInAllButtonPressed:(id)sender; - (IBAction)viewColumnButtonPressed:(id)sender; - (IBAction)viewIconButtonPressed:(id)sender; - (IBAction)syncButtonPressed:(id)sender; - (IBAction)searchAction:(id)sender; @end @implementation MainWindowController - (id)init { return self = [super initWithWindowNibName:NSStringFromClass([self class])]; } - (void)awakeFromNib { [super awakeFromNib]; [self.window setFrameUsingName:@"MainWindow"]; [self.window setFrameAutosaveName:@"MainWindow"]; sidebarController = [[SidebarViewController alloc] init]; sidebarController.delegate = self; [_mainSplitView setDelegate:self]; [_mainSplitView replaceSubview:_leftPane with:sidebarController.view]; [_mainSplitView adjustSubviews]; [_mainSplitView setPosition:PANE_DEFAULT_WIDTH ofDividerAtIndex:0]; // Custom appearance of vertical divider [_mainSplitView setDividerWidth:0.0f]; [_mainSplitView setHidesDivider:YES]; [_pathSeparator setImage:[NSImage imageNamed:@"SeparatorVerticalDark.png"]]; [_pathSeparator setContentMode:PSViewContentModeCenter]; [_rightSplitView setDelegate:self]; [_rightSplitView setPosition:(_rightSplitView.bounds.size.height - 1.0f - PATH_DEFAULT_HEIGHT) ofDividerAtIndex:0]; [_rightSplitView adjustSubviews]; [_pathControl setFocusRingType:NSFocusRingTypeNone]; [_pathControl setPathComponentCells:@[]]; [_pathControl setDoubleAction:@selector(pathControlDoubleClick:)]; [_pathControl setTarget:self]; [_pathControl setDelegate:self]; // Config [self.window setBackgroundColor:[NSUserDefaults colorForKey:kColorBackgroundBrowser]]; _topBar.backgroundColor = [NSUserDefaults colorForKey:kColorBackgroundTopBar]; _rightBottomPane.fillColor = [NSUserDefaults colorForKey:kColorBackgroundBottomBar]; [self showNetworkWarning:![P4Reachability isConnected] animated:NO]; } - (void)windowDidLoad { [super windowDidLoad]; [P4Reachability startNotifier]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(syncStartedNotification:) name:P4SyncStartedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(syncFinishedNotification:) name:P4SyncFinishedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(submitFinishedNotification:) name:P4SubmitFinishedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sessionExpiredNotification:) name:P4SessionExpiredNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkReachabilityNotification:) name:P4ReachabilityNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mappingUpdatedNotification:) name:P4MappingUpdatedNotification object:nil]; } #pragma mark - Public - (void)openFile:(NSString *)path { NSString *root = [[P4Workspace sharedInstance] root]; NSString *localPath; // Prepare local path if ([path hasPrefix:@"p4://"]) { path = [path substringFromIndex:3]; path = [path stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSString *relativePath = [path relativePath:@"//"]; localPath = [root stringByAppendingString:relativePath]; } else { // Check if local path exists BOOL dir = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&dir] && dir; localPath = dir ? path = [path directoryPath] : path; NSString *relativePath = [path relativePath:root]; if (!relativePath) { [self showErrors:@[ [NSError errorWithFormat:@"Files not in user's workspace"] ]]; return; } path = [@"//" stringByAppendingString:relativePath]; } if (![self.window isVisible]) [self showWindow:nil]; // Load depot path [browserController loadPath:path]; } - (void)editFiles:(NSArray *)paths { paths = [self filterWorkspacePaths:paths]; if (paths.count) { [[browserController rootItem] performAction:@selector(checkoutFiles:) object:paths delegate:self]; } } - (void)editAllFiles:(NSArray *)paths { paths = [self filterWorkspacePaths:paths]; if (paths.count) { [[browserController rootItem] performAction:@selector(checkoutAllFiles:) object:paths delegate:self]; } } - (void)submitFiles:(NSArray *)paths { if (![self.window isVisible]) [self showWindow:nil]; // Map files NSMutableArray *submitPaths = [NSMutableArray array]; for (__strong NSString *path in paths) { if (![path hasPrefix:@"//"]) path = [[P4Workspace sharedInstance] mappingForPath:path]; if (path) [submitPaths addObject:path]; } // None was mapped if (paths.count && !submitPaths.count) { [self showErrors:@[ [NSError errorWithFormat:@"Files not in user's workspace"] ]]; return; } [[P4Workspace sharedInstance] checkOpenedFiles:^(P4Operation *operation, NSArray *response) { [submitPanel dismiss]; SubmitPanelController *panel = [[SubmitPanelController alloc] init]; submitPanel = panel; [submitPanel presentWithWindow:self.window]; [submitPanel selectPaths:submitPaths]; }]; } - (void)submitAllFiles:(NSArray *)paths { paths = [self filterWorkspacePaths:paths]; if (paths.count) { [[browserController rootItem] performAction:@selector(checkInAllFiles:) object:paths delegate:self]; } } - (void)reload { [self clear]; [self setBrowserController: [[P4WorkspaceDefaults sharedInstance] iconView] ? [[IconViewController alloc] init] : [[ColumnViewController alloc] init]]; [sidebarController reload]; [browserController setRootItem:nil]; #ifdef DEBUG [[P4Workspace sharedInstance] setAutosyncInterval:5*60]; #else [[P4Workspace sharedInstance] setAutosyncInterval:2*60]; #endif } - (void)loadPath:(NSString *)path { [browserController loadPath:path]; } - (void)showErrors:(NSArray *)errors { NSMutableArray *message = [NSMutableArray array]; // Parse errors NSMutableDictionary *errGroups = [NSMutableDictionary dictionary]; for (NSError *error in errors) { // Don't show alert for session expiration if (error.code == P4ErrorSessionExpired) continue; // Don't show details for generic errors if (error.domain != P4ErrorDomain) { [message addObject:error.localizedDescription]; continue; } id key = @(error.code); NSString *errorMessage = nil; switch (error.code) { case P4ErrorAlreadyOpened: errorMessage = @"File is already opened for edit"; break; case P4ErrorAlreadyEdited: errorMessage = @"File is already opened for edit"; break; case P4ErrorFileExclusiveLock: errorMessage = @"File is LOCKED"; break; case P4ErrorOpenedBy: { NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"also opened by (.*)@" options:NSRegularExpressionCaseInsensitive error:NULL]; NSArray *matches = [regex matchesInString:error.localizedDescription options:0 range:NSMakeRange(0, [error.localizedDescription length])]; NSString *parsedUser = nil; for (NSTextCheckingResult *match in matches) { NSRange matchRange = [match rangeAtIndex:1]; parsedUser = [error.localizedDescription substringWithRange:matchRange]; } errorMessage = @"It looks like <username> is currently using this file. Please email the user to submit the file."; if(parsedUser != nil) { errorMessage = [errorMessage stringByReplacingOccurrencesOfString:@"<username>" withString:parsedUser]; } break; } case P4ErrorFilesWithWildcards: errorMessage = @"Please remove @#%* characters from the filename to add this file."; break; case P4ErrorRenameWithWildcards: errorMessage = @"Please remove @#%* characters from the filename to rename this file."; break; case P4ErrorReconcileWithWildcards: errorMessage = @"Please remove @#%* characters from the filename to reconcile this file."; break; case P4ErrorFileNotInView: errorMessage = @"File location is not synced with workspace"; break; case P4ErrorFileNotOnClient: errorMessage = @"File is not tracked"; break; case P4ErrorCannotRollback: errorMessage = @"You have to submit the file before reverting"; break; case P4ErrorEditReadOnlyFile: errorMessage = @"You cannot edit this file. The file is read-only."; break; case P4ErrorRevertMoveDeleteFile: errorMessage = @"Please revert the file in the pending changelist."; break; default: errorMessage = @"Action failed with errors"; key = [NSNull null]; break; } NSMutableArray *errGroup = [errGroups objectForKey:key]; if (!errGroup) { errGroup = [NSMutableArray arrayWithObject:errorMessage]; [errGroups setObject:errGroup forKey:key]; [message addObject:errorMessage]; } [errGroup addObject:error.localizedDescription]; } if (!message.count) return; // Nothing to display NSMutableArray *details = [NSMutableArray array]; [errGroups enumerateKeysAndObjectsUsingBlock:^(id key, id errGroup, BOOL *stop) { [details addObjectsFromArray:errGroup]; }]; // if (![self.window isVisible]) // [self showWindow:nil]; [errorPanel dismiss]; ErrorPanelController *panel = [[ErrorPanelController alloc] init]; errorPanel = panel; [errorPanel setTitle:@"\nAction failed"]; [errorPanel setMessage:[message componentsJoinedByString:@"\n"]]; if (details.count) { [errorPanel setDetails:[details componentsJoinedByString:@"\n"]]; [errorPanel setDetailsTitle:errors.count > 1 ? [NSString stringWithFormat:@"Error details (%ld)", errors.count] : @"Error details"]; } [errorPanel presentWithWindow:self.window]; } - (void)showLoading:(NSString *)message { if (!loadingPanel) { loadingPanel = [[LoadingPanelController alloc] initWithStyle:NSProgressIndicatorSpinningStyle]; } loadingPanel.title = message; if (![loadingPanel.window isVisible]) [loadingPanel presentWithWindow:self.window]; } - (void)dismissLoading { [loadingPanel dismiss]; loadingPanel = nil; } #pragma mark - Private - (void)changePath:(NSString *)path { [sidebarController setWorkingPath:path]; if (!path || [path isEqualCaseInsensitive:undoStack.lastObject]) return; [undoStack addObject:path]; redoStack = [NSMutableArray array]; [self updateNavigationButtons]; } - (void)next { [undoStack addObject:redoStack.lastObject]; [redoStack removeLastObject]; [self updateNavigationButtons]; [browserController loadPath:undoStack.lastObject]; } - (void)back { [redoStack addObject:undoStack.lastObject]; [undoStack removeLastObject]; [self updateNavigationButtons]; [browserController loadPath:undoStack.lastObject]; } - (void)clear { undoStack = [NSMutableArray array]; redoStack = [NSMutableArray array]; [self updateNavigationButtons]; } - (void)updateNavigationButtons { [_backButton setEnabled:undoStack.count > 1]; [_nextButton setEnabled:redoStack.count != 0]; } - (void)setPathControlItem:(P4Item *)item { NSColor *color = [NSUserDefaults colorForKey:kColorBackgroundBottomBar]; NSPathComponentCell *(^createCellBlock)(NSString *, NSString *) = ^NSPathComponentCell *(NSString *name, NSString *path) { PSPathComponentCell *pathCell = [[PSPathComponentCell alloc] init]; pathCell.font = [NSFont fontWithName:@"Helvetica-Bold" size:12]; pathCell.textColor = [NSColor darkGrayColor]; [pathCell setBackgroundColor:color]; [pathCell setImage:[NSImage imageNamed:@"IconFolder"]]; [pathCell setTitle:name]; [pathCell setRepresentedObject:path]; return pathCell; }; NSString *rootName = nil; NSString *rootPath = nil; NSString *relative = item.remotePath; if ([relative hasPrefix:@"//"]) { rootPath = @"//"; rootName = @"All Files"; } else { rootPath = browserController.rootItem.path; rootName = browserController.rootItem.name; relative = nil; } relative = [relative relativePath:rootPath]; relative = [relative stringByRemovingSuffix:@"/"]; NSMutableArray *cells = [NSMutableArray array]; if (rootName) [cells addObject:createCellBlock(rootName, rootPath)]; NSString *path = rootPath; for (NSString *component in [relative pathComponents]) { path = [path stringByAppendingFormat:@"%@/", component]; [cells addObject:createCellBlock(component, path)]; } PSPathComponentCell *lastCell = [cells lastObject]; if (item && !item.isDirectory) lastCell.image = item.icon; [lastCell setLastItem:YES]; [lastCell setRepresentedObject:nil]; [_pathControl setPathComponentCells:cells]; } - (void)setBrowserController:(BrowserViewController *)browser { if (browserController == browser) return; // Swap views [_rightSplitView replaceSubview:_rightTopPane with:browser.view]; _rightTopPane = browser.view; // Swap next responders if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_9) { [browser setNextResponder:browser.view.nextResponder]; [browser.view setNextResponder:browser]; [self.window makeFirstResponder:browser.view]; } [browser setRootItem:browserController.rootItem]; [browser setWorkingItem:browserController.workingItem]; [browser setSelectedItems:browserController.selectedItems]; [browser setDelegate:self]; browserController = browser; PSLogStore(@"BrowserController", @"%@", browser); BOOL column = [browser isKindOfClass:[ColumnViewController class]]; [_viewColumnButton setEnabled:!column]; [_viewIconButton setEnabled:column]; // Swap search bars if (_searchBar) { NSView *view = browser.searchBar; CGFloat height = view.frame.size.height; [_rightSplitView replaceSubview:_searchBar with:view]; _searchBar = view; CGRect frame = view.frame; frame.size.height = height; view.frame = frame; } } - (void)syncWorkspace { SyncPanelController *syncPanel = [[SyncPanelController alloc] init]; [syncPanel presentWithWindow:self.window]; } - (void)addFiles { NSOpenPanel *openPanel = [NSOpenPanel openPanel]; [openPanel setCanChooseFiles:YES]; [openPanel setCanChooseDirectories:YES]; [openPanel setAllowsMultipleSelection:YES]; [openPanel setCanCreateDirectories:YES]; [openPanel setPrompt:@"Select"]; NSURL *directory = [NSURL fileURLWithPath:NSHomeDirectory() isDirectory:YES]; [openPanel setDirectoryURL:directory]; [openPanel beginSheetModalForWindow:self.window completionHandler:^(NSInteger result) { if (result != NSOKButton) return; NSArray *selectedPaths = [[openPanel URLs] valueForKey:@"path"]; [[browserController workingItem] performAction:@selector(insertFiles:) object:selectedPaths delegate:self]; }]; } - (void)createDirectory { [[browserController workingItem] performAction:@selector(createDirectory) object:nil delegate:self]; } - (NSArray *)filterWorkspacePaths:(NSArray *)paths { NSString *root = [[P4Workspace sharedInstance] root]; paths = [paths filteredArrayUsingBlock:^BOOL(NSString *path, NSUInteger idx) { return [path isSubpath:root]; }]; if (!paths.count) { [self showErrors:@[ [NSError errorWithFormat:@"Files not in user's workspace"] ]]; return nil; } return paths; } - (void)showNetworkWarning:(BOOL)show animated:(BOOL)animated { if (show == [_networkWarning isHidden]) { CGRect frame = _pathControl.frame; frame.size.width = _networkWarning.frame.origin.x; if (!show) frame.size.width += _networkWarning.frame.size.width; if (animated) { [_networkWarning setAlphaValue:!show]; [_networkWarning setHidden:NO]; [NSView animateWithDuration:0.25f animations:^{ [[_networkWarning animator] setAlphaValue:show]; [[_pathControl animator] setFrame:frame]; } completion:^{ [_networkWarning setHidden:!show]; }]; } else { [_pathControl setFrame:frame]; [_networkWarning setHidden:!show]; } } } #pragma mark - SplitView delegate - (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex { if (splitView == _mainSplitView) return proposedMaximumPosition * 0.5f; // Half of window size return proposedMaximumPosition; } - (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex { if (splitView == _mainSplitView) return PANE_MIN_WIDTH; return proposedMinimumPosition; } - (BOOL)splitView:(NSSplitView *)splitView canCollapseSubview:(NSView *)subview { if (splitView == _mainSplitView) return subview == sidebarController.view; // Collapsable side pane return NO; } - (BOOL)splitView:(NSSplitView *)splitView shouldAdjustSizeOfSubview:(NSView *)view { if (view == sidebarController.view) return (sidebarController.view.bounds.size.width > // Don't resize side pane / unless it's larger than half of window size _mainSplitView.bounds.size.width * 0.5f); if (view == _rightBottomPane) return NO; if (view == _searchBar) return NO; return YES; } - (NSRect)splitView:(NSSplitView *)splitView effectiveRect:(NSRect)proposedEffectiveRect forDrawnRect:(NSRect)drawnRect ofDividerAtIndex:(NSInteger)dividerIndex { if (splitView == _rightSplitView) return CGRectZero; return proposedEffectiveRect; } #pragma mark - LoginPanel Delegate - (void)loginPanelDidLogin { WorkspacePanelController *workspacePanel = [[WorkspacePanelController alloc] init]; workspacePanel.delegate = self; [workspacePanel presentWithWindow:self.window]; } - (void)loginPanelDidCancel { [loginPanel dismiss]; loginPanel = nil; } - (void)loginPanelDidLoginWithWorkspace:(NSString *)workspace { [self workspacePanelDidSelectWorkspace:workspace]; } - (void)loginPanelDidRelogin { [loginPanel dismiss]; loginPanel = nil; [browserController reload]; } #pragma mark - WorkspacePanel Delegate - (void)workspacePanelDidSelectWorkspace:(NSString *)workspace { [loginPanel dismiss]; loginPanel = nil; [self reload]; [self loadPath:@"//"]; } - (void)workspacePanelDidCreateWorkspace:(NSString *)workspace { [self workspacePanelDidSelectWorkspace:workspace]; } - (void)workspacePanelDidCancel { } #pragma mark - Sidebar Delegate - (void)sidebarDidSelectPath:(NSString *)path { [browserController loadRootPath:path]; } #pragma mark - BrowserView Delegate - (void)browserView:(BrowserViewController *)browser didChangeWorkingItem:(P4Item *)item { BOOL editable = item.isEditable; [_addButton setEnabled:editable]; [_createDirectoryButton setEnabled:editable]; [self changePath:item.path]; } - (void)browserView:(BrowserViewController *)browser didChangeSelectedItems:(NSArray *)items { [self setPathControlItem:items.firstObject ?: browser.workingItem]; if ([infoPanel.window isVisible]) [infoPanel setItems:items]; } - (void)browserView:(id)browser didChangeSearchQuery:(NSString *)query { if (query) { if (!_searchBar.superview) { _searchBar = browserController.searchBar; [_rightSplitView insertFirstSubview:_searchBar animated:YES]; } // Update undo stack if ([[undoStack lastObject] hasPrefix:@"search://"]) { [undoStack removeLastObject]; NSString *path = browserController.workingItem.path ?: @"search://"; // Dirty way to ensure non-nil [undoStack addObject:path]; } } else { _searchField.stringValue = @""; if (_searchBar.superview) { [_rightSplitView removeFirstSubviewAnimated:YES]; _searchBar = nil; } // Remove history stack entries if ([[undoStack lastObject] hasPrefix:@"search://"]) [undoStack removeLastObject]; if ([[redoStack lastObject] hasPrefix:@"search://"]) [redoStack removeLastObject]; [self updateNavigationButtons]; } } #pragma mark - P4ItemAction delegate - (void)action:(P4ItemAction *)action loadingMessage:(NSString *)message { if (!pendingAction) { pendingAction = action; [self showLoading:message]; } if (pendingAction == action) [self showLoading:message]; } - (void)action:(P4ItemAction *)action didFinish:(NSArray *)response errors:(NSArray *)errors { if (pendingAction == action) { [self dismissLoading]; pendingAction = nil; } if (errors) { [self showErrors:errors]; return; } if (action.selector == @selector(createDirectory)) [browserController editItemName:[action.item itemAtPath:response.lastObject]]; else if (action.selector == @selector(checkIn)) [self submitFiles:@[ action.item.remotePath ]]; else if (action.selector == @selector(checkInFiles:)) [self submitFiles:action.object]; else if (action.selector == @selector(checkInAll)) [self submitFiles:response]; else if (action.selector == @selector(checkInAllFiles:)) [self submitFiles:response]; else if (action.selector == @selector(addFiles:)) [self submitFiles:response]; else if (action.selector == @selector(reconcile)) [self submitFiles:response]; else if (action.selector == @selector(revertToVersion:)) [self submitFiles:response]; else if (action.selector == @selector(revertToChangelist:)) [self submitFiles:response]; else if (action.selector == @selector(mapToWorkspace)) [self syncWorkspace]; else if (action.selector == @selector(checkoutFiles:)) [self openFile:[action.object firstObject]]; else if (action.selector == @selector(showVersions)) [browserController showVersions:action.item]; else if (action.selector == @selector(showFolderHistory)) [browserController showFolderHistory:action.item]; } - (BOOL)action:(id)action prompt:(NSString *)message { return [[NSAlert alertWithMessageText:@"Confirm" defaultButton:@"Yes" alternateButton:@"No" otherButton:nil informativeTextWithFormat:@"%@", message] runModal] == NSAlertDefaultReturn; } #pragma mark - P4Workspace notifications - (void)syncStartedNotification:(NSNotification *)notification { [_checkInAllButton setEnabled:NO]; [_syncButtonIndicator setHidden:NO]; [_syncButtonIndicator setAlphaValue:0.0f]; [NSView animateWithDuration:0.25f animations:^{ [_syncButton.animator setAlphaValue:0.0f]; [_syncButtonIndicator.animator setAlphaValue:1.0f]; } completion:^{ if ([[P4Workspace sharedInstance] isSynchronizing]) [_syncButton setHidden:YES]; }]; } - (void)syncFinishedNotification:(NSNotification *)notification { [_checkInAllButton setEnabled:YES]; [_syncButton setHidden:NO]; [_syncButton setAlphaValue:0.0f]; [NSView animateWithDuration:0.25f animations:^{ [_syncButton.animator setAlphaValue:1.0f]; [_syncButtonIndicator.animator setAlphaValue:0.0f]; } completion:^{ [_syncButton setAlphaValue:1.0f]; [_syncButton setHidden:NO]; // Make sure it's not hidden [_syncButtonIndicator setHidden:YES]; }]; [browserController reload]; } - (void)submitFinishedNotification:(NSNotification *)notification { [browserController reload]; } - (void)mappingUpdatedNotification:(NSNotification *)notification { [[P4WorkspaceDefaults sharedInstance] updateMapping]; } #pragma mark - Network notifications - (void)sessionExpiredNotification:(NSNotification *)notification { if (loginPanel) return; loginPanel = [LoginPanelController reloginPanel]; loginPanel.delegate = self; [loginPanel presentExclusiveWithWindow:self.window]; } - (void)networkReachabilityNotification:(NSNotification *)notification { [self showNetworkWarning:![P4Reachability isConnected] animated:YES]; } #pragma mark - PathControl - (NSDragOperation)pathControl:(NSPathControl *)pathControl validateDrop:(id <NSDraggingInfo>)info { return NSDragOperationNone; } - (void)pathControlDoubleClick:(id)sender { NSPathComponentCell *cell = [_pathControl clickedPathComponentCell]; NSString *path = cell.representedObject; if (path) [browserController loadPath:path]; } #pragma mark - NSMenu delegate - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { SEL action = menuItem.action; BOOL connected = [[P4Workspace sharedInstance] isLoggedIn]; BOOL columnView = [browserController isKindOfClass:[ColumnViewController class]]; if (action == @selector(newWindow:)) { return YES; } else if (action == @selector(connect:)) { return YES; } else if (action == @selector(changeConnectionDetails:)) { return connected; } else if (action == @selector(showWorkspaceSelection:)) { return connected; } else if (action == @selector(cleanWorkspace:)) { return connected; } else if (action == @selector(disconnect:)) { return connected; } else if (action == @selector(getInfo:)) { return YES; } else if (action == @selector(toggleColumnView:)) { [menuItem setState:columnView]; return !menuItem.state; } else if (action == @selector(toggleIconView:)) { [menuItem setState:!columnView]; return !menuItem.state; } else if (action == @selector(showHelp:)) { return YES; } return NO; } - (void)menuNeedsUpdate:(NSMenu *)menu { [menu setAllowsContextMenuPlugIns:NO]; NSArray *selectedItems = [browserController selectedItems]; if (selectedItems.count > 1) [menu setItems:selectedItems delegate:self]; else if (selectedItems.count) [menu setItem:selectedItems.lastObject delegate:self]; else { [menu removeAllItems]; [[menu addItemWithTitle:@"No selection" action:nil keyEquivalent:@""] setEnabled:NO]; } } #pragma mark - Menu Actions - (void)newWindow:(id)sender { NSURL *url = [[NSRunningApplication currentApplication] executableURL]; NSError *error; if (![[NSWorkspace sharedWorkspace] launchApplicationAtURL:url options:NSWorkspaceLaunchNewInstance configuration:nil error:&error]) { [self showErrors:@[ error ]]; } } - (void)connect:(id)sender { if (loginPanel) return; loginPanel = [LoginPanelController loginPanel]; loginPanel.delegate = self; loginPanel.allowsLastWorkspace = YES; [loginPanel presentWithWindow:self.window]; } - (void)changeConnectionDetails:(id)sender { if (loginPanel) return; loginPanel = [LoginPanelController connectionPanel]; loginPanel.delegate = self; loginPanel.allowsLastWorkspace = YES; [loginPanel presentWithWindow:self.window]; } - (void)showWorkspaceSelection:(id)sender { WorkspacePanelController *workspacePanel = [[WorkspacePanelController alloc] init]; workspacePanel.delegate = self; [workspacePanel presentWithWindow:self.window]; } - (void)cleanWorkspace:(id)sender { // this method removes any explicit file mappings from the client workspace. [[P4Workspace sharedInstance] listPendingFiles:nil response:^(P4Operation *operation, NSArray *response) { NSArray *paths = [response valueForKey:@"depotFile"]; NSArray *errors = operation.errors; if (!errors && paths.count) errors = @[ [NSError errorWithFormat:@"You cannot clean your workspace with files checked out. Submit or revert the files"] ]; if (errors) { [self showErrors:errors]; return; } NSMutableArray *mappedFiles = [NSMutableArray array]; for (NSString *mappingPath in [[P4Workspace sharedInstance] mapping]) { if(![mappingPath hasSuffix:@"/"] && ![mappingPath hasPrefix:@"-"]) [mappedFiles addObject:mappingPath]; } if (mappedFiles.count) { [[P4Workspace sharedInstance] mappingSet:NO files:mappedFiles response:nil]; [[P4WorkspaceDefaults sharedInstance] updateMapping]; [[P4Workspace sharedInstance] syncWorkspace:nil response:nil]; } }]; } - (void)disconnect:(id)sender { [self showLoading:@"Logging out..."]; NSString *username = [P4Workspace sharedInstance].username; [[P4Workspace sharedInstance] logout:^(P4Operation *operation, NSArray *response) { // Remove last workspace settings P4Defaults *defaults = [P4Defaults sharedInstance]; NSMutableDictionary *users = [defaults.users mutableCopy]; [users removeObjectForKey:username]; defaults.users = users; defaults.workspace = nil; [P4WorkspaceDefaults setWorkspace:nil]; [sidebarController reload]; [browserController setRootItem:nil]; [self dismissLoading]; [self connect:nil]; }]; } - (void)getInfo:(id)sender { NSArray *items = [browserController selectedItems]; if (!infoPanel) infoPanel = [[InfoPanelController alloc] init]; [infoPanel setItems:items]; [infoPanel showWindow:nil]; } - (void)toggleColumnView:(id)sender { [[P4WorkspaceDefaults sharedInstance] setIconView:NO]; [self setBrowserController:[[ColumnViewController alloc] init]]; } - (void)toggleIconView:(id)sender { [[P4WorkspaceDefaults sharedInstance] setIconView:YES]; [self setBrowserController:[[IconViewController alloc] init]]; } - (IBAction)debugMenuRunCommand:(id)sender { debugController = [[DebugWindowController alloc] init]; [debugController showWindow:self]; } - (IBAction)debugMenuSaveLog:(id)sender { PSDebugLogSave(); } - (void)showHelp:(id)sender { NSString *url = [NSUserDefaults objectForKey:kHelpUrl]; [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]]; } #pragma mark - Toolbar actions - (IBAction)backButtonPressed:(id)sender { [self back]; } - (IBAction)nextButtonPressed:(id)sender { [self next]; } - (IBAction)syncButtonPressed:(id)sender { [self syncWorkspace]; } - (IBAction)addButtonPressed:(id)sender { [self addFiles]; } - (IBAction)checkInAllButtonPressed:(id)sender { [self submitFiles:nil]; } - (IBAction)viewColumnButtonPressed:(id)sender { [self toggleColumnView:sender]; } - (IBAction)viewIconButtonPressed:(id)sender { [self toggleIconView:sender]; } - (IBAction)createDirectoryButtonPressed:(id)sender { [self createDirectory]; } #pragma mark - Search action - (IBAction)searchAction:(id)sender { [browserController search:[_searchField stringValue]]; } @end
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#1 | 16817 | christoph_leithner | "Forking branch Main of perforce-software-piper to christoph_leithner-piper." | ||
//guest/perforce_software/piper/main/mac/R2.0/Perforce/Classes/WindowControllers/MainWindowController.m | |||||
#1 | 16507 | perforce_software | Move to main branch. | ||
//guest/perforce_software/piper/mac/R2.0/Perforce/Classes/WindowControllers/MainWindowController.m | |||||
#1 | 12962 | alan_petersen |
Populate -o //guest/perforce_software/piper/mac/main/... //guest/perforce_software/piper/mac/R2.0/.... |
||
//guest/perforce_software/piper/mac/main/Perforce/Classes/WindowControllers/MainWindowController.m | |||||
#2 | 12961 | alan_petersen |
Piper 2.0 Mega Update New Features/Functionality - Added help menu redirecting to URL. - Added readonly property for creating new workspaces. - Added html hyperlinks for Copy link functionality. - Added functionality for managing Finder Favorite items in sidebar. - Redesigned the way mapping is stored in Piper. - First version of syncing finder sidebar items with workspace mapping. - Small sorting improvements. - Creating Projects directory inside users home folder. - Adding Projects folder to finder sidebar item. - Creating and removing symbolic links accordingly to mapped folders. - Preventing duplicate names in symbolic links. - Refreshing symbolic links on mapping change inside application. - Storing workspace and server details in p4 configuration for other applications to use. - Added contextual menu items for Finder integration. - Added services menu for Adobe Illustrator integration. - Keyboard shortcuts for Illustrator integration. - Code refactoring and fixes for mapping issues. - Added Finder functionality to edit all files in folder. - Added user friendly message when editing a file using Finder outside the workspace. - Implemented hidden automatic login when opening application using Finder integration. - Logging to file in ~/Library/Logs - Unified workspace and all files views to show both local and depot files and folders. - Removed my workspace view references and logic. - Editing unmapped files on server. - First version of adding file to unmapped folders. - Showing opened by and edit actions in column details for all depot files. - Improved mappings functionality. - Enabled same feature options for mapped and unmapped folders and files. - Redesigned from scratch mapping and unmapping procedures for adding and removing files. - Implemented cleaning workspace using new mapping functionality. Removed debug overlay coloring. - Automated workspace creation - Improvements in editing files already mapped to workspace. - Implemented deleting remote files. - Implemented first version of move operation for remote files. - Removing last workspace information when disconnecting from workspace using app menu. - Implemented editing and submitting using symbolic links in project folder. New finder menu service for symbolic links Show in Piper which acts like share link functionality. - New icons for files and folders not tracked in the filesystem. - Improvements in showing file using share link. - Switched to new way of retrieving files in order to show user changes. - Redesigned and implemented new functionality for chaining operations with mapping. - Improvements and redesign of Edit/add actions to use new chaining logic . Fixed issue with file edit. - Improvements in window showing when using services. - Simplified file loading so the local files appears only when remote are also loaded. - Improved deleting of untracked files to avoid mapping and marking for delete. - Enabling simple copy paste and moving of remote and local files. - Added abort for exception handling in order to force crashing application on critical failures - Added custom exception handling for catching runtime errors to log and crash instead of continuing in unstable state. - Changed file copying to use mark for add . - Simplified and fixed responding file representations to mapping changes. Bug Fixes - Fixed crash when synchronizing. - Fixed sync issue when downloading directory without file size information. - Fixed issue with unread list crashing when file is not existing on disk. - Fixed incorrect sync progress calculation. - Removed relative path issues. - Fixed many of case-sensitivity problems. - Fixed deprecated methods and related issues in OS X 10.10. - Fixed folder rename not updating in column view. Revised and fixed many potential problems from implicit casting. - Fixed missing sync button on fast sync completion. - Refreshing mapping on synchronization. Fixed symbolic links not appearing until app is restarted. - Fixed latest crashing of autosync. - Fixed loading indicator issues. - Fixed and redesigned submit dialog to work correctly with Submit All Files option in Finder. - Fixed multiple error messages on network outage. Redesigned showing errors in main window. - Fixed opening random locations when using Finder integration. - Fixed issue when panel was detached from parent window. - Fixed bug when creating new workspace wouldn't store default settings. - Fixed memory issues with network operations. - Fixes in relogging mappings and file listing. - Improvements in editing unmapped files. - Fixed crash when adding file outside workspace. - Fixed breadcrumbs control issue. - Fixed issue with double parent folders when opening unmapped files. - Fixed crashes on sync after mapping new files. - Fixed issue with editing file using Finder -- Merging code and additional fixes in add button functionality. - Fixed unsync not working - Fixed submit panel issue not selecting files with different name case. - Fixed missing revert and sync to workspace actions in some cases. - Fixed issue with Submit and Edit finder actions. Improvements in stability of finder integration. - Fixed issue with unsubmitted folders breaking status of files inside. - Fixed issue with added files not showing correct icon and status. - Fixed bug with file edit resulting in a new directory named exactly like a file. - Fixed issue with reloading of subpath resulting in untracked folders. - Fixed mapping issue when result was always view mapping not relative. - Fixed submit panel showing more than once. - Fixed illustrator services not working. - Fixed userdefaults preferences problem with workspace name being null. - Fixed userdefaults keypath problem of dot-containing workspace names. - Forcing recreating of browser to possibly prevent pre-10.10 errors with automatic workspace selection. - Fixed adding file to depot not presenting correct icon. - Fixed issues with reverting a file that was marked for add. - Presenting error when trying to submit untracked files. - Fixed issue when submit files service crashed when using unmapped files. - Fixed file representation disappearing when removing file. - Fixed issue with symlinks resolving working on 10.10 only. Issue related to workspace selection not showing. - Fixed error panel method calls unavailable in Mac OS versions before 10.10. Issue related to hanging error panels. - Fixed removing a local file resulting in action progress freezing. - Fixed open file not working after edit. - Fixing crash when mapping changed. Issue related to moving local file to unmapped folder and other similar cases. |
||
#1 | 11252 | alan_petersen | Rename/move file(s) | ||
//guest/perforce_software/piper/mac/Perforce/Classes/WindowControllers/MainWindowController.m | |||||
#1 | 10744 | alan_petersen | Rename/move file(s) | ||
//guest/perforce_software/piper/Perforce/Classes/WindowControllers/MainWindowController.m | |||||
#1 | 8919 | Matt Attaway | Initial add of Piper, a lightweight Perforce client for artists and designers. |