// // 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; // Selected details P4Item *detailsItem; // Selected header P4Item *headerItem; ColumnViewHeader *headerController; NSMutableArray *columnHeaders; // Will Load NSString *reloadPath; NSArray *reloadSelection; CGFloat reloadScroll; ColumnViewHeader *reloadHeader; // QuickLook QLPreviewPanel *quickLookPanel; NSArray *quickLookItems; } - (void)clickAction; - (void)doubleClickAction; - (void)selectionChangedToItem:(P4Item *)item; - (void)deselectHeader; - (void)showLoadingOverlay; - (void)showLoadingForItem:(P4Item *)item; - (void)showErrorForItem:(P4Item *)item; - (void)updateFavoritesNotification:(NSNotification *)notification; @end @implementation ColumnViewController - (void)loadView { [super loadView]; [browserView setBackgroundColor:[NSUserDefaults colorForKey: kColorBackgroundColumnContainer]]; [browserView setCellClass:[ColumnCell class]]; [browserView setRowHeight:24.0f]; [browserView setSendsActionOnArrowKeys:YES]; [browserView setAllowsMultipleSelection:YES]; [browserView setAllowsBranchSelection:YES]; [browserView registerForDraggedTypes:@[ NSFilenamesPboardType ]]; [browserView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:YES]; [browserView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:NO]; [browserView setTarget:self]; [browserView setAction:@selector(clickAction)]; [browserView setDoubleAction:@selector(doubleClickAction)]; [browserView setFocusRingType:NSFocusRingTypeNone]; [(id)browserView setBorderType:NSNoBorder]; [[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)setRootItem:(P4Item *)item { [super setRootItem:item]; [self deselectHeader]; [browserView loadColumnZero]; } - (void)setWorkingItem:(P4Item *)item { [super setWorkingItem:item]; [self selectionChangedToItem:item]; [self deselectHeader]; if (!workingItem) return; NSString *rootPath = rootItem.path; NSString *relative = [workingItem.path stringByRemovingPrefix:rootPath]; [browserView loadColumnZero]; [browserView setPath:relative]; } - (void)setSelectedIndexes:(NSIndexSet *)indexes { [browserView selectRowIndexes:indexes inColumn:browserView.lastColumn]; [self clickAction]; } - (void)refresh { [browserView setNeedsDisplay]; } - (void)willLoadPath:(NSString *)path { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(showLoadingOverlay) object:nil]; [self performSelector:@selector(showLoadingOverlay) withObject:nil afterDelay:0.25f]; reloadPath = path; if (isReloading) { // 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; reloadSelection = [[self selectedItems] valueForKey:@"path"]; reloadHeader = headerController; } } - (void)showVersions:(P4Item *)item { if (item != detailsItem) { [browserView selectRow:[browserView clickedRow] inColumn:[browserView clickedColumn]]; [self clickAction]; } ColumnViewDetails *details = (id)[browserView detailViewController]; [details showVersions]; } - (void)editItemName:(P4Item *)item { NSInteger row = [[item.parent children] indexOfObjectIdenticalTo:item]; NSIndexPath *indexPath = [browserView indexPathForColumn:[browserView lastColumn]]; indexPath = [indexPath indexPathByAddingIndex:row]; [browserView setSelectionIndexPath:indexPath]; [browserView editItemAtIndexPath:indexPath withEvent:nil select:NO]; [self selectionChangedToItem:item]; } #pragma mark - Private - (void)clickAction { NSArray *selection = [self selectedItems]; if (!quickLookPanel && [QLPreviewPanel sharedPreviewPanelExists]) [[QLPreviewPanel sharedPreviewPanel] updateController]; if (quickLookPanel) { quickLookItems = selection; [quickLookPanel reloadData]; } if (selection.count > 1) return; // Don't show details for multiple items P4Item *item = selection.lastObject ?: rootItem; [self selectionChangedToItem:item]; // 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 selectionChangedToItem:item]; // Perform default item action P4ItemAction *action = [item defaultAction]; action.delegate = [self.view.window windowController]; [action performAction]; } - (void)selectionChangedToItem:(P4Item *)item { [self deselectHeader]; // // Auto mark as unread // if (!item.isDirectory && item.isUpdated) // [item markAsRead]; // Get parent if item is not expandable P4Item *nodeItem = [item isDirectory] ? item : [item parent]; if (workingItem != nodeItem) [super setWorkingItem:nodeItem]; [self selectionChanged:item]; } - (void)deselectHeader { [headerController setSelected:NO]; headerController = nil; headerItem = nil; } - (void)showLoadingOverlay { [loadingOverlay setFrame:self.view.bounds]; [self.view addSubview:loadingOverlay]; [loadingOverlayIndicator startAnimation:nil]; } - (void)showLoadingForItem:(P4Item *)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)/2.0f; frame.origin.y += (frame.size.height - 32.0f)/2.0f; 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)/2.0f; frame.origin.y += (frame.size.height - 32.0f)/2.0f; 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 { 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.selectedImage = 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]; [self showLoadingForItem:item]; [self showErrorForItem:item]; } - (NSViewController *)browser:(NSBrowser *)browser headerViewControllerForItem:(P4Item *)item { if (item == rootItem) return nil; ColumnViewHeader *header = [[ColumnViewHeader alloc] init]; header.delegate = self; if (headerItem == item) { // New header should be selected headerController = header; header.selected = YES; } // Get column number from parent count NSInteger column = 0; for (P4Item *parent = item.parent; parent; column++, parent = parent.parent); if (!columnHeaders) columnHeaders = [NSMutableArray array]; if (column-1 < columnHeaders.count) [columnHeaders replaceObjectAtIndex:column-1 withObject:header]; else [columnHeaders addObject:header]; 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 Keyboard support - (void)browserDidReceiveCommandDeleteKey:(NSBrowser *)browser { if (![browserView selectionIndexPath]) return; NSArray *selectedItems = [self selectedItems]; [rootItem performAction:@selector(deleteItems:) items:selectedItems delegate:[self.view.window windowController]]; } - (void)browserDidReceiveSpacebarKey:(NSBrowser *)browser { QLPreviewPanel *previewPanel = [QLPreviewPanel sharedPreviewPanel]; // Dismiss if visible if ([QLPreviewPanel sharedPreviewPanelExists] && [previewPanel isVisible]) { [previewPanel orderOut:nil]; return; } if (![browserView selectionIndexPath]) return; [previewPanel updateController]; [previewPanel makeKeyAndOrderFront:nil]; } #pragma mark - Drag and Drop support - (BOOL)browser:(NSBrowser *)browser writeRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column toPasteboard:(NSPasteboard *)pasteboard { NSMutableArray *filenames = [NSMutableArray array]; [rowIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { P4Item *item = [browser itemAtRow:idx inColumn:column]; if ([item isEditable]) [filenames addObject:item.localPath]; }]; [pasteboard declareTypes:@[NSFilenamesPboardType] owner:self]; [pasteboard setPropertyList:filenames forType:NSFilenamesPboardType]; return YES; } - (BOOL)browser:(NSBrowser *)browser canDragRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column withEvent:(NSEvent *)event { NSMutableArray *items = [NSMutableArray array]; [rowIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { P4Item *item = [browser itemAtRow:idx inColumn:column]; if ([item isEditable]) [items addObject:item]; }]; return items.count; } - (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]; PSLog(@""); [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; 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) / 2.0f, mouse.y + (20.0f - attrStr.size.height) / 2.0f + 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 { // Check if option key is pressed BOOL copy = info.draggingSourceOperationMask == NSDragOperationCopy; NSArray *paths = [info.draggingPasteboard propertyListForType:NSFilenamesPboardType]; if (column == -1 || !paths.count) 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 insertFiles:paths intoItem:target copy:copy]; } #pragma mark - ColumnHeader delegate - (void)columnViewHeader:(ColumnViewHeader *)header clickedWithItem:(P4Item *)item { [[browserView window] makeFirstResponder:browserView]; // Conclude editing // Get column number from parent count NSInteger column = 0; for (P4Item *parent = item.parent; parent; column++, parent = parent.parent); if (browserView.lastColumn == column) { // Change is inside current parent. Select parent row in previous column NSUInteger row = [browserView selectedRowInColumn:column-1]; // Store item for selection because new header will be created headerItem = item; [browserView selectRow:row inColumn:column-1]; [self selectionChanged:item]; } else { [[browserView animator] setLastColumn:column]; // Trim selection to header's parent [self selectionChangedToItem:item]; // Store selected header as selected headerItem = item; headerController = header; } // 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]; detailsItem = item; } #pragma mark - P4Item delegate - (void)itemDidLoad:(P4Item *)item { [super itemDidLoad:item]; // Remove loading indicator if (item == loadingItem) { loadingItem = nil; [progressIndicator stopAnimation:nil]; [progressIndicator removeFromSuperview]; } if (isLoading) { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(showLoadingOverlay) object:nil]; [loadingOverlay removeFromSuperview]; P4Item *item = [rootItem cachedItemForPath:reloadPath]; if (!item) { [self failWithError: [NSError errorWithFormat:@"Couldn't find file at %@", reloadPath]]; isLoading = isReloading = NO; return; } isLoading = NO; [self setSelectedItems:@[ item ]]; if (isReloading) { isReloading = NO; // Find items for previously selected paths NSInteger idx = 0; NSMutableIndexSet *indexes = [NSMutableIndexSet indexSet]; for (P4Item *child in item.children) { if ([reloadSelection containsObject:child.path]) [indexes addIndex:idx]; idx++; } [browserView selectRowIndexes:indexes inColumn:browserView.lastColumn]; // 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 (reloadHeader) [self columnViewHeader:reloadHeader clickedWithItem:item]; else [self clickAction]; } return; } // 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]) { // Get column number from parent count NSInteger column = 0; for (P4Item *parent = item.parent; parent; column++, parent = parent.parent); if (column > browserView.lastColumn) return; if (item != [browserView parentForItemsInColumn:column]) return; // Reload children [browserView reloadColumn:column]; // // Refresh navigation // id lastItem = [navigationController lastItem]; // [navigationController removeLastItem]; // [navigationController setLastItem:lastItem]; // Refresh column header if (column > 0) [[columnHeaders objectAtIndex:column-1] 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 convertRectToBase:imageRect]; imageRect.origin = [[browserView window] convertBaseToScreen:imageRect.origin]; return imageRect; } - (id)previewPanel:(QLPreviewPanel *)panel transitionImageForPreviewItem:(id )previewItem contentRect:(NSRect *)contentRect { P4Item *item = (P4Item *)previewItem; return item.iconHighlighted; } #pragma mark - Menu delegate - (void)menuWillOpen:(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