// // ColumnViewController.m // Perforce // // Created by Adam Czubernat on 12.05.2013. // Copyright (c) 2013 Perforce Software, Inc. All rights reserved. // #import "ColumnViewController.h" #import "ColumnCell.h" #import "PSBrowser.h" #import "ColumnViewDetails.h" #import "ColumnViewHeader.h" @interface ColumnViewController () { __weak IBOutlet PSBrowser *browserView; IBOutlet NSView *loadingOverlay; __weak IBOutlet NSProgressIndicator *loadingOverlayIndicator; NSProgressIndicator *progressIndicator; PSView *warningIndicator; id loadingItem; BOOL isLoading; BOOL isReloading; P4Item *detailsItem; // Item selected for details NSMutableArray *columnHeaders; // Will Load CGFloat reloadScroll; // QuickLook QLPreviewPanel *quickLookPanel; NSArray *quickLookItems; } - (void)clickAction; - (void)doubleClickAction; - (void)showLoadingOverlay; - (void)showLoadingForItem:(P4Item *)item; - (void)showErrorForItem:(P4Item *)item; - (void)updateFavoritesNotification:(NSNotification *)notification; @end @implementation ColumnViewController - (void)loadView { [super loadView]; [browserView setBackgroundColor:[NSColor clearColor]]; [browserView setCellClass:[ColumnCell class]]; [browserView setRowHeight:24.0f]; [browserView setFocusRingType:NSFocusRingTypeNone]; [(id)browserView setBorderType:NSNoBorder]; [browserView setSendsActionOnArrowKeys:YES]; [browserView setAllowsMultipleSelection:YES]; [browserView setAllowsBranchSelection:YES]; [browserView registerForDraggedTypes:@[ NSFilenamesPboardType, P4PasteboardTypeFile ]]; [browserView setTarget:self]; [browserView setAction:@selector(clickAction)]; [browserView setDoubleAction:@selector(doubleClickAction)]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateFavoritesNotification:) name:P4WorkspaceDefaultsChangedNotification object:nil]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self name:P4WorkspaceDefaultsChangedNotification object:nil]; } #pragma mark - BrowserViewController Overrides - (NSArray *)selectedItems { NSMutableArray *array = [NSMutableArray array]; for (NSIndexPath *indexPath in [browserView selectionIndexPaths]) { P4Item *item = [browserView itemAtIndexPath:indexPath]; [array addObject:item]; } return array; } - (void)setWorkingItem:(P4Item *)item { [super setWorkingItem:item]; NSMutableArray *components = [NSMutableArray array]; P4Item *root = [self rootItemForBrowser:browserView]; for (P4Item *parent = item; parent && parent != root; parent = parent.parent) [components insertObject:parent.name atIndex:0]; NSString *path = [components componentsJoinedByString:@"/"]; [browserView loadColumnZero]; [browserView setPath:path]; } - (void)setSelectedIndexes:(NSIndexSet *)indexes { [browserView selectRowIndexes:indexes inColumn:browserView.lastColumn]; [self clickAction]; } - (void)refresh { [browserView setNeedsDisplay]; } - (void)willLoadPath:(NSString *)path { isLoading = YES; [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(showLoadingOverlay) object:nil]; [self performSelector:@selector(showLoadingOverlay) withObject:nil afterDelay:0.25f]; if (!workingItem) { isReloading = YES; // Get scrollview position NSScrollView *scrollView = nil; for (NSView *subview in [browserView subviews]) if ([subview isKindOfClass:NSClassFromString(@"_NSBrowserScrollView")]) scrollView = (id)subview; reloadScroll = [scrollView documentVisibleRect].origin.x; } } - (void)showVersions:(P4Item *)item { if (item != detailsItem) { [browserView selectRow:[browserView clickedRow] inColumn:[browserView clickedColumn]]; [self clickAction]; } ColumnViewDetails *details = (id)[browserView detailViewController]; [details showVersions]; } - (void)showFolderHistory:(P4Item *)item { if (item != detailsItem) { NSInteger row = [browserView clickedRow]; NSInteger column = [browserView columnOfItem:item]; if (column < 0) // Item isn't opened, user clicked directly on row column = [browserView clickedColumn]; if (workingItem != item) { [browserView selectRow:row inColumn:column]; [self clickAction]; } ColumnViewHeader *header = [columnHeaders objectAtIndex:column]; [header setSelected:YES]; [self columnViewHeader:header clickedWithItem:item]; } ColumnViewDetails *details = (id)[browserView detailViewController]; [details showFolderHistory]; } - (void)editItemName:(P4Item *)item { NSInteger row = [[item.parent children] indexOfObjectIdenticalTo:item]; NSInteger column = [browserView columnOfItem:item.parent]; NSIndexPath *indexPath = [browserView indexPathForColumn:column]; indexPath = [indexPath indexPathByAddingIndex:row]; [browserView setSelectionIndexPath:indexPath]; [browserView editItemAtIndexPath:indexPath withEvent:nil select:NO]; [self selectionChanged:@[ item ]]; } - (void)setBackgroundColor:(NSColor *)color { [browserView setColumnBackgroundColor:color]; } - (void)setDraggingSourceOperationMask:(NSDragOperation)mask forLocal:(BOOL)isLocal { [browserView setDraggingSourceOperationMask:mask forLocal:isLocal]; } - (void)selectionChanged:(NSArray *)items { // Determine if selection changed working item P4Item *firstItem = items.firstObject; P4Item *parentItem; if (!firstItem) parentItem = [self rootItemForBrowser:browserView]; else if (items.count > 1 || ![firstItem isDirectory]) parentItem = [firstItem parent]; else parentItem = firstItem; if (workingItem != parentItem) [super setWorkingItem:parentItem]; [super selectionChanged:items]; } #pragma mark - Private - (void)clickAction { NSArray *selection = [self selectedItems]; if (!quickLookPanel && [QLPreviewPanel sharedPreviewPanelExists]) [[QLPreviewPanel sharedPreviewPanel] updateController]; if (quickLookPanel) { quickLookItems = selection; [quickLookPanel reloadData]; } detailsItem = nil; [columnHeaders makeObjectsPerformSelector:@selector(deselect)]; [self selectionChanged:selection]; if (selection.count > 1) return; // Don't show details for multiple items P4Item *item = selection.firstObject; // Show detail pane for files if (item && ![item isDirectory]) { ColumnViewDetails *detailsController; detailsController = [[ColumnViewDetails alloc] initWithItem:item actionDelegate:[self.view.window windowController]]; [browserView setDetailViewController:detailsController]; detailsItem = item; } } - (void)doubleClickAction { if (browserView.selectionIndexPaths.count > 1) return; // Don't open multiple items P4Item *item = [browserView itemAtIndexPath:browserView.selectionIndexPath]; if (!item) return; [self selectionChanged:@[ item ]]; // Perform default item action P4ItemAction *action = [item defaultAction]; action.delegate = [self.view.window windowController]; [action performAction]; } - (void)showLoadingOverlay { [loadingOverlay setFrame:self.view.bounds]; [self.view addSubview:loadingOverlay]; [loadingOverlayIndicator startAnimation:nil]; } - (void)showLoadingForItem:(P4Item *)item { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(showLoadingForItem:) object:item]; [progressIndicator removeFromSuperview]; if (!item.isLoading) return; loadingItem = item; if (!progressIndicator) { progressIndicator = [[NSProgressIndicator alloc] init]; [progressIndicator setStyle:NSProgressIndicatorSpinningStyle]; [progressIndicator setIndeterminate:YES]; [progressIndicator setControlSize:NSRegularControlSize]; } NSArray *columns = [browserView valueForKey:@"_columns"]; NSView *columnView = [columns objectAtIndex:browserView.lastColumn]; CGRect frame = columnView.frame; frame.origin.x += (frame.size.width - 32.0f) * 0.5f; frame.origin.y += (frame.size.height - 32.0f) * 0.5f; frame.size.width = frame.size.height = 32.0f; progressIndicator.frame = frame; [progressIndicator setAutoresizing:NSViewAutoresizingLeftEdge]; [progressIndicator startAnimation:nil]; [columnView.superview addSubview:progressIndicator]; } - (void)showErrorForItem:(P4Item *)item { [warningIndicator removeFromSuperview]; if (!item.hasError) return; static NSImage *warningImage; if (!warningImage) warningImage = [NSImage imageNamed:@"Warning.png"]; NSArray *columns = [browserView valueForKey:@"_columns"]; NSView *columnView = columns.count ? [columns objectAtIndex:browserView.lastColumn] : browserView; CGRect frame = columns.count ? columnView.frame : [browserView frameOfColumn:0]; frame.origin.x += (frame.size.width - 32.0f) * 0.5f; frame.origin.y += (frame.size.height - 32.0f) * 0.5f; frame.size.width = frame.size.height = 32.0f; if (!warningIndicator) { warningIndicator = [[PSView alloc] init]; warningIndicator.image = warningImage; [warningIndicator setAutoresizing:NSViewAutoresizingLeftEdge]; } warningIndicator.frame = frame; [columnView.superview addSubview:warningIndicator]; } #pragma mark - P4WorkspaceDefaults Notification - (void)updateFavoritesNotification:(NSNotification *)notification { if ([detailsItem isDirectory]) [self itemDidLoad:detailsItem]; } #pragma mark - NSBrowser delegate - (id)rootItemForBrowser:(NSBrowser *)browser { if (rootPath) return [rootItem itemAtPath:rootPath] ?: rootItem; return rootItem; } - (NSInteger)browser:(NSBrowser *)browser numberOfChildrenOfItem:(P4Item *)item { return item.children.count; } - (id)browser:(NSBrowser *)browser child:(NSInteger)index ofItem:(P4Item *)item { NSArray *children = item.children; if (index < children.count) return [children objectAtIndex:index]; return nil; } - (BOOL)browser:(NSBrowser *)browser isLeafItem:(P4Item *)item { return !item.isDirectory; } - (id)browser:(NSBrowser *)browser objectValueForItem:(P4Item *)item { return item.name; } - (void)browser:(NSBrowser *)sender willDisplayCell:(ColumnCell *)cell atRow:(NSInteger)row column:(NSInteger)column {; P4Item *item = [browserView itemAtRow:row inColumn:column]; cell.image = item.icon; cell.alternateImage = item.iconHighlighted; [cell setLeaf:![item isDirectory]]; cell.unread = item.isUnread; cell.tag = [self hasFilteredTagsForItem:item]; static NSColor *shelvedColor; if (!shelvedColor) shelvedColor = [NSColor colorWithHexString:@"#f2c600"]; cell.overlayColor = item.isShelved ? shelvedColor : item.overlay; } - (void)browser:(NSBrowser *)browser didChangeLastColumn:(NSInteger)oldLastColumn toColumn:(NSInteger)column { if (column < 0) return; P4Item *item = [browser parentForItemsInColumn:column]; [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(showLoadingForItem:) object:item]; [self performSelector:@selector(showLoadingForItem:) withObject:item afterDelay:0.25f]; [self showErrorForItem:item]; } - (NSViewController *)browser:(NSBrowser *)browser headerViewControllerForItem:(P4Item *)item { if (!item) return nil; if (!columnHeaders) columnHeaders = [NSMutableArray array]; [columnHeaders makeObjectsPerformSelector:@selector(deselect)]; ColumnViewHeader *header = [[ColumnViewHeader alloc] init]; [header setDelegate:self]; [header setSelected:detailsItem == item]; NSInteger column = [browserView lastColumn]+1; if (column < columnHeaders.count) [columnHeaders replaceObjectAtIndex:column withObject:header]; else [columnHeaders addObject:header]; if (!item.parent) // Store, but don't use root items return nil; return header; } - (BOOL)browser:(NSBrowser *)browser shouldEditItem:(P4Item *)item { return item.isEditable; } - (void)browser:(NSBrowser *)browser setObjectValue:(NSString *)value forItem:(P4Item *)item { [self renameItem:item name:value]; } #pragma mark - Drag and Drop support - (BOOL)browser:(NSBrowser *)browser writeRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column toPasteboard:(NSPasteboard *)pasteboard { P4Item *item = [browser parentForItemsInColumn:column]; NSArray *items = [item.children objectsAtIndexes:rowIndexes]; return [self writeItems:items toPasteboard:pasteboard]; } - (NSArray *)browser:(NSBrowser *)browser namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination forDraggedRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column { P4Item *item = [browser parentForItemsInColumn:column]; NSArray *items = [item.children objectsAtIndexes:rowIndexes]; return [self namesOfPromisedItems:items droppedAtDestination:dropDestination]; } - (NSImage *)browser:(NSBrowser *)browser draggingImageForRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column withEvent:(NSEvent *)event offset:(NSPointPointer)dragImageOffset { NSImage *result = [browser draggingImageForRowsWithIndexes:rowIndexes inColumn:column withEvent:event offset:dragImageOffset]; [result lockFocus]; if (rowIndexes.count > 1) { NSPoint mouse = (CGPoint) { result.size.width * 0.5f - dragImageOffset->x, result.size.height * 0.5f - dragImageOffset->y, }; NSShadow *shadow = [[NSShadow alloc] init]; [shadow setShadowOffset:NSMakeSize(0.5, 0.5)]; [shadow setShadowBlurRadius:5.0]; [shadow setShadowColor:[NSColor blackColor]]; NSDictionary *attrs = @{ NSShadowAttributeName : shadow, NSForegroundColorAttributeName : [NSColor whiteColor], }; NSInteger cornerSize = 10.0f; NSGradient *gradient = [[NSGradient alloc] initWithStartingColor:[NSColor colorWithHexString:@"#FCC"] endingColor:[NSColor redColor]]; NSBezierPath *bezier = [NSBezierPath bezierPathWithRoundedRect:(CGRect) { mouse, {20.0f, 20.0f} } xRadius:cornerSize yRadius:cornerSize]; [gradient drawInBezierPath:bezier angle:90.0f]; BOOL copy = ([NSApp currentEvent].modifierFlags & NSAlternateKeyMask) != 0; NSString *str = [NSString stringWithFormat:@"%@%ld", copy ? @"+": @"", rowIndexes.count]; NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:str attributes:attrs]; [attrStr drawAtPoint:(CGPoint) { mouse.x + (20.0f - attrStr.size.width) * 0.5f, mouse.y + (20.0f - attrStr.size.height) * 0.5f + 1, }]; [result unlockFocus]; } return result; } - (NSDragOperation)browser:(NSBrowser *)browser validateDrop:(id )info proposedRow:(NSInteger *)row column:(NSInteger *)column dropOperation:(NSBrowserDropOperation *)dropOperation { NSDragOperation operation = NSDragOperationEvery; // Check if option key is pressed if ([[NSApp currentEvent] modifierFlags] & NSAlternateKeyMask) operation = NSDragOperationCopy; // Accept only file types if ([[[info draggingPasteboard] types] indexOfObject:NSFilenamesPboardType] == -1) return NSDragOperationNone; if (*column == -1) return NSDragOperationNone; P4Item *parent = [browserView parentForItemsInColumn:*column]; NSInteger rows = parent.children.count; if (*dropOperation == NSBrowserDropAbove) { if (*row < rows) { *row = -1; return NSDragOperationNone; } *row = -1; return operation; } P4Item *target = [browserView itemAtRow:*row inColumn:*column]; if (!target.isDirectory || !target.isEditable) { // Can't drop onto a file. Retarget to the column *row = -1; *dropOperation = NSBrowserDropOn; if (!parent.isEditable) return NSDragOperationNone; } return operation; } - (BOOL)browser:(NSBrowser *)browser acceptDrop:(id )info atRow:(NSInteger)row column:(NSInteger)column dropOperation:(NSBrowserDropOperation)dropOperation { if (column == -1) return NO; // Find the target folder P4Item *target = nil; if (row == -1) target = [browser parentForItemsInColumn:column]; else target = [browser itemAtRow:row inColumn:column]; return [self acceptDrop:info target:target]; } #pragma mark - ColumnHeader delegate - (void)columnViewHeader:(ColumnViewHeader *)header clickedWithItem:(P4Item *)item { [[browserView window] makeFirstResponder:browserView]; // Conclude editing // Set new details item (before selecting row, so header can be selected) detailsItem = item; NSInteger column = [browserView columnOfItem:item]; if (column < 0) column = [browserView lastColumn]; if (browserView.lastColumn == column) { // Change is inside current parent. Select parent row in previous column NSUInteger row = [browserView selectedRowInColumn:column-1]; [browserView selectRow:row inColumn:column-1]; } else { // Trim selection to header's parent [[browserView animator] setLastColumn:column]; } [self selectionChanged:@[ item ]]; // Stop indicator if (item == loadingItem) { loadingItem = nil; [progressIndicator stopAnimation:nil]; [progressIndicator removeFromSuperview]; } // Show details for header's folder ColumnViewDetails *detailsController; detailsController = [[ColumnViewDetails alloc] initWithItem:item actionDelegate:[self.view.window windowController]]; [browserView setDetailViewController:detailsController]; } #pragma mark - P4Item delegate - (void)itemDidLoad:(P4Item *)item { [super itemDidLoad:item]; if (isLoading) { isLoading = NO; [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(showLoadingOverlay) object:nil]; [loadingOverlay removeFromSuperview]; if (isReloading) { isReloading = NO; // Set scrollview's position NSScrollView *scrollView = nil; for (NSView *subview in [browserView subviews]) if ([subview isKindOfClass:NSClassFromString(@"_NSBrowserScrollView")]) scrollView = (id)subview; [scrollView scrollClipView:scrollView.contentView toPoint:CGPointMake(reloadScroll, 0.0)]; if ([detailsItem isDirectory]) { detailsItem = item; // Select header NSInteger column = [browserView lastColumn]; ColumnViewHeader *header = [columnHeaders objectAtIndex:column]; [header setSelected:YES]; [self columnViewHeader:header clickedWithItem:detailsItem]; } else { [self clickAction]; } } } else { // Remove loading indicator if (item == loadingItem) { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(showLoadingForItem:) object:item]; loadingItem = nil; [progressIndicator stopAnimation:nil]; [progressIndicator removeFromSuperview]; } // Redisplay (for name, icon, overlay changes) [browserView setNeedsDisplay:YES]; // Redisplay detail view if (detailsItem == item && [browserView detailViewController]) { ColumnViewDetails *detailsController; detailsController = [[ColumnViewDetails alloc] initWithItem:item actionDelegate:[self.view.window windowController]]; [browserView setDetailViewController:detailsController]; } // Directory change if ([item isDirectory]) { NSInteger column = [browserView columnOfItem:item]; if (column < 0) return; // Reload children [browserView reloadColumn:column]; // Refresh column header [[columnHeaders objectAtIndex:column] setRepresentedObject:item]; } } } - (void)itemDidInvalidate:(P4Item *)item { if (item == detailsItem) [browserView setDetailViewController:nil]; } - (void)item:(id)item didFailWithError:(NSError *)error { [self failWithError:error]; [self itemDidLoad:item]; [self showErrorForItem:item]; PSLog(@"ColumnView error: %@", error.localizedDescription); } #pragma mark - Quicklook Panel delegate - (BOOL)acceptsPreviewPanelControl:(QLPreviewPanel *)panel { return YES; } - (void)beginPreviewPanelControl:(QLPreviewPanel *)panel { quickLookPanel = panel; panel.delegate = self; panel.dataSource = self; } - (void)endPreviewPanelControl:(QLPreviewPanel *)panel { quickLookPanel = nil; } - (BOOL)previewPanel:(QLPreviewPanel *)panel handleEvent:(NSEvent *)event { // Redirect all key down events to the browser if ([event type] == NSKeyDown) { [browserView keyDown:event]; return YES; } return NO; } - (NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel *)panel { quickLookItems = [self selectedItems]; return quickLookItems.count; } - (id )previewPanel:(QLPreviewPanel *)panel previewItemAtIndex:(NSInteger)index { return [quickLookItems objectAtIndex:index]; } - (NSRect)previewPanel:(QLPreviewPanel *)panel sourceFrameOnScreenForPreviewItem:(id )item { NSIndexPath *indexPath = [browserView selectionIndexPath]; if (!indexPath) return CGRectZero; // Calculate selected cell's image frame NSUInteger row = [indexPath indexAtPosition:indexPath.length-1]; CGRect cellRect = [browserView frameOfRow:row inColumn:indexPath.length-1]; NSCell *cell = [browserView selectedCellInColumn:browserView.selectedColumn]; CGRect imageRect = [cell imageRectForBounds:cellRect]; CGRect columnRect = [browserView frameOfColumn:indexPath.length-1]; imageRect.origin.x += columnRect.origin.x; imageRect.origin.y += columnRect.origin.y; // Check that the icon rect is visible on screen CGRect visibleRect = [browserView visibleRect]; if (!NSIntersectsRect(visibleRect, imageRect)) return CGRectZero; // Convert icon rect to screen coordinates imageRect = [browserView convertRect:imageRect toView:nil]; imageRect = [browserView.window convertRectToScreen:imageRect]; return imageRect; } - (id)previewPanel:(QLPreviewPanel *)panel transitionImageForPreviewItem:(id )previewItem contentRect:(NSRect *)contentRect { P4Item *item = (P4Item *)previewItem; return item.iconHighlighted; } #pragma mark - Menu delegate - (void)menuNeedsUpdate:(NSMenu *)menu { [menu setAllowsContextMenuPlugIns:NO]; NSInteger col = [browserView clickedColumn]; NSInteger row = [browserView clickedRow]; NSIndexSet *set = [browserView selectedRowIndexesInColumn:col]; P4Item *item = nil; if (set.count > 1 && [set containsIndex:row]) { item = [browserView parentForItemsInColumn:col]; // Multiple [menu setItems:[item.children objectsAtIndexes:set] delegate:[self.view.window windowController]]; return; } else if (col >= 0) { if (row < 0) item = [browserView parentForItemsInColumn:col]; else item = [browserView itemAtRow:row inColumn:col]; } [menu setItem:item delegate:[self.view.window windowController]]; } @end