// // IconViewItem.m // Perforce // // Created by Adam Czubernat on 07.06.2013. // Copyright (c) 2013 Perforce Software, Inc. All rights reserved. // #import "IconViewItem.h" #import "IconViewController.h" #pragma mark - IconBackgroundView // View that is sending click actions only when not clicking on the // subviews (deselect) and is not passing events to superviews. View is // sending doubleClick actions only on subviews. @interface IconBackgroundView : PSView @end @implementation IconBackgroundView - (void)mouseDown:(NSEvent *)theEvent { // Check if clicked on subviews if ([self hitTest:[self.superview convertPoint:theEvent.locationInWindow fromView:nil]] != self) { [super mouseDown:theEvent]; // Act normally if ([theEvent clickCount] > 1) [super sendActionsForEvents:PSViewEventDoubleClick]; return; } // Clicked outside subviews (on view itself) if ([theEvent clickCount] == 1) [super sendActionsForEvents:PSViewEventClick]; // Clicks only outside // Don't pass event further } - (void)mouseDragged:(NSEvent *)theEvent { [super mouseDown:theEvent]; } - (void)sendActionsForEvents:(PSViewEvent)events { // Disable executing actions through [super mouseDown] } @end #pragma mark - IconViewItemController const char * IconItemGlobalQueueLabel = "com.perforce.PreviewQueue"; dispatch_queue_t IconItemGlobalQueue = NULL; @interface IconViewItem () { P4Item *item; NSTrackingArea *trackingArea; NSArray *btnActions; NSImage *image; __weak IBOutlet PSView *iconView; __weak IBOutlet PSView *selectionView; __weak IBOutlet PSView *statusView; __weak IBOutlet NSTextField *versionLabel; __weak IBOutlet NSTextField *titleLabel; __weak IBOutlet PSView *versionSeparator; __weak IBOutlet PSView *horizontalLine; __weak IBOutlet NSImageView *unreadBadge; __weak IBOutlet NSImageView *tagBadge; __weak IBOutlet PSView *actionBtsContent; __weak IBOutlet NSTextField *firstTitle; __weak IBOutlet NSTextField *firstLabel; __weak IBOutlet NSTextField *secondTitle; __weak IBOutlet NSTextField *secondLabel; } - (void)loadDirectoryItem; - (void)loadFileItem; - (void)loadImage; - (void)setRevision:(NSString *)revision; - (void)textFieldDidEndEditingNotification:(NSNotification *)notification; - (void)backgroundClick; - (void)contentDoubleClick; @end @implementation IconViewItem @synthesize delegate, iconView; static NSColor *textColor; static NSColor *textColorSecondary; static NSColor *backgroundColor; static NSColor *backgroundColorSelected; static NSColor *lineColor; static NSColor *statusColorAvailable; static NSColor *statusColorCheckedOut; static NSColor *statusColorCheckedOutSomeone; static NSColor *statusColorShelved; #pragma mark - Overrides - (NSString *)nibName { return NSStringFromClass([self class]); } - (void)loadView { [super loadView]; // Load images and colors static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ textColor = [NSUserDefaults colorForKey:kColorTextColumn]; textColorSecondary = [NSUserDefaults colorForKey:kColorTextIconSecondary]; backgroundColor = [NSUserDefaults colorForKey:kColorBackgroundIcon]; backgroundColorSelected = [NSUserDefaults colorForKey:kColorBackgroundSelected]; lineColor = [NSColor colorWithHexString:@"#CCC"]; statusColorAvailable = [NSUserDefaults colorForKey:kColorStatusAvailable]; statusColorCheckedOut = [NSUserDefaults colorForKey:kColorStatusCheckedOut]; statusColorCheckedOutSomeone = [NSUserDefaults colorForKey:kColorStatusCheckedOutSomeone]; statusColorShelved = [NSColor colorWithHexString:@"#ffd700"]; }); [self setSelected:NO]; selectionView.backgroundColor = backgroundColor; statusView.backgroundColor = statusColorAvailable; horizontalLine.backgroundColor = versionSeparator.backgroundColor = lineColor; actionBtsContent.backgroundColor = [NSUserDefaults colorForKey:kColorBackgroundIconViewActionButton]; //colorWithAlphaComponent: 0.5]; [actionBtsContent setHidden:YES]; [self initializeMouseTracking]; // Add actions PSView *view = (PSView *)self.view; [view addTarget:self action:@selector(backgroundClick) forEvents:PSViewEventClick]; [view addTarget:self action:@selector(contentDoubleClick) forEvents:PSViewEventDoubleClick]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldDidEndEditingNotification:) name:NSControlTextDidEndEditingNotification object:titleLabel]; } - (id)copyWithZone:(NSZone *)zone { IconViewItem *copy = [super copyWithZone:zone]; copy.delegate = delegate; [copy view]; // Force loading views return copy; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self name:NSControlTextDidEndEditingNotification object:nil]; } - (void)setRepresentedObject:(P4Item *)anItem { [super setRepresentedObject:anItem]; item = anItem; // Load determined by type if ([anItem isDirectory]) [self loadDirectoryItem]; else [self loadFileItem]; [unreadBadge setHidden:!item.isUnread]; if ([(BrowserViewController *)delegate hasFilteredTagsForItem:item]) { CGRect frame = unreadBadge.frame; if (item.isUnread) frame.origin.x -= frame.size.width + 2.0f; [tagBadge setFrame:frame]; [tagBadge setHidden:NO]; } else { [tagBadge setHidden:YES]; } // Load common values for two types titleLabel.stringValue = anItem.name; [self loadImage]; } - (void)setSelected:(BOOL)selected { if (selected && [delegate respondsToSelector:@selector(iconViewItemSelected:)]) [delegate iconViewItemSelected:self]; [super setSelected:selected]; if (selected) { firstTitle.textColor = secondTitle.textColor = versionLabel.textColor = firstLabel.textColor = secondLabel.textColor = titleLabel.textColor = [NSColor whiteColor]; selectionView.backgroundColor = backgroundColorSelected; } else { if (titleLabel.isEditable) [self.view.window makeFirstResponder:self.collectionView]; firstTitle.textColor = secondTitle.textColor = versionLabel.textColor = textColorSecondary; firstLabel.textColor = secondLabel.textColor = titleLabel.textColor = textColor; selectionView.backgroundColor = backgroundColor; } } #pragma mark - Public - (NSString *)title { return titleLabel.stringValue; } - (void)beginEditing { [titleLabel setEditable:YES]; [self.view.window makeFirstResponder:titleLabel]; } - (void)abortEditing { [titleLabel abortEditing]; [titleLabel setEditable:NO]; } #pragma mark - Click Actions - (void)loadDirectoryItem { [self setRevision:nil]; firstTitle.stringValue = firstLabel.stringValue = secondTitle.stringValue = secondLabel.stringValue = @""; statusView.backgroundColor = statusColorAvailable; } - (void)loadFileItem { NSDictionary *metadata = item.metadata; [self setRevision:[metadata objectForKey:@"haveRev"]]; statusView.backgroundColor = statusColorAvailable; if (item.isTracked) { NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; formatter.dateStyle = NSDateFormatterLongStyle; NSNumber *headTime = [metadata objectForKey:@"headModTime"]; NSDate *headDate = headTime ? [NSDate dateWithTimeIntervalSince1970: headTime.doubleValue] : nil; firstTitle.stringValue = @"MODIFIED IN"; firstLabel.stringValue = [formatter stringFromDate:headDate] ?: @""; NSString *title, *label; if (item.status) { title = @"CHECKED OUT BY"; label = @"You"; } else if (item.statusOwner) { title = @"CHECKED OUT BY"; label = item.statusOwner; } else { title = @"CHECKED IN"; label = @"Available"; } secondTitle.stringValue = title ?: @""; secondLabel.stringValue = label ?: @""; if (item.statusOwner) { [secondLabel setAttributedStringValue: [[NSAttributedString alloc] initWithString:item.statusOwner attributes:@{ NSFontAttributeName : secondLabel.font, NSUnderlineStyleAttributeName : @(NSSingleUnderlineStyle), NSLinkAttributeName : [NSString stringWithFormat:@"mailto:%@", item.statusOwner], }]]; [secondLabel setAllowsEditingTextAttributes:YES]; [secondLabel setSelectable:YES]; } } else if (item.status) { firstTitle.stringValue = @"ACTION"; firstLabel.stringValue = item.status ?: @""; secondTitle.stringValue = @"UNTRACKED"; secondLabel.stringValue = @""; } else { firstTitle.stringValue = @"UNTRACKED"; firstLabel.stringValue = secondTitle.stringValue = secondLabel.stringValue = @""; } if (item.isShelved) { statusView.backgroundColor = statusColorShelved; } else if (item.statusOwner) { statusView.backgroundColor = statusColorCheckedOutSomeone; } else if (item.status) { statusView.backgroundColor = statusColorCheckedOut; } } - (void)loadImage { if (!iconView) return; // Load image asynchronously static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ IconItemGlobalQueue = dispatch_queue_create(IconItemGlobalQueueLabel, NULL); }); dispatch_async(IconItemGlobalQueue, ^{ if (!image) image = [item previewWithSize:iconView.bounds.size]; dispatch_async(dispatch_get_main_queue(), ^{ iconView.image = image; }); }); } - (void)setRevision:(NSString *)revision { if (!revision) { titleLabel.frame = CGRectUnion(titleLabel.frame, versionLabel.frame); titleLabel.alignment = NSCenterTextAlignment; [versionLabel setHidden:YES]; [versionSeparator setHidden:YES]; return; } titleLabel.alignment = NSLeftTextAlignment; [versionLabel setHidden:NO]; [versionSeparator setHidden:NO]; versionLabel.stringValue = [NSString stringWithFormat:@"V.%@", revision]; [versionLabel sizeToFit]; CGFloat offset = CGRectGetMaxX(versionLabel.frame) + 6.0f; CGRect frame = versionSeparator.frame; frame.origin.x = offset; versionSeparator.frame = frame; offset += 7.0f; frame = titleLabel.frame; frame.size.width = CGRectGetMaxX(frame) - offset; frame.origin.x = offset; titleLabel.frame = frame; } - (void)textFieldDidEndEditingNotification:(NSNotification *)notification { if ([delegate respondsToSelector:@selector(iconViewItemDidEndEditing:)]) [delegate iconViewItemDidEndEditing:self]; [titleLabel setEditable:NO]; [self.view.window makeFirstResponder:self.collectionView]; } - (void)backgroundClick { self.collectionView.selectionIndexes = nil; } - (void)contentDoubleClick { if ([delegate respondsToSelector:@selector(iconViewItemOpened:)]) [delegate iconViewItemOpened:self]; } #pragma mark - TrackingArea setup - (void) initializeMouseTracking { if(trackingArea != nil) { [selectionView removeTrackingArea:trackingArea]; } int opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways); trackingArea = [ [NSTrackingArea alloc] initWithRect:[selectionView bounds] options:opts owner:self userInfo:nil]; [selectionView addTrackingArea:trackingArea]; } -(void)mouseEntered:(NSEvent *)theEvent { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(showActionButtons) object:nil]; [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(resetActionButtons) object:nil]; [self performSelector:@selector(showActionButtons) withObject:nil afterDelay:0.5f]; } -(void)mouseExited:(NSEvent *)theEvent { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(showActionButtons) object:nil]; if (!actionBtsContent.isHidden) { [PSView animateWithDuration:0.1f animations:^{ [[actionBtsContent animator] setAlphaValue:0.0f]; } completion:^{ [actionBtsContent setHidden:YES]; }]; [self performSelector:@selector(resetActionButtons) withObject:nil afterDelay:0.25f]; } } #pragma mark - Private - (void)resetActionButtons { [actionBtsContent.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; } - (void)showActionButtons { [self setActionBtns]; [actionBtsContent setHidden:NO]; [actionBtsContent setAlphaValue:0.0f]; [PSView animateWithDuration:0.25f animations:^{ [[actionBtsContent animator] setAlphaValue:1.0f]; }]; } #define btnWidth 48 #define separatorWidth 5 - (void)setActionBtns { btnActions = item.actions; NSButton *tmpBtn; NSRect initialFrame; NSRect separatorFrame; NSRect separatorImageContentFrame; NSImageView *separator; PSView *separatorImageContent; int xOffset = 0; [actionBtsContent.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; for (P4ItemAction *p4ItemAction in btnActions) { NSImage *actionImage; SEL itemActionSelector = p4ItemAction.selector; if(itemActionSelector == @selector(showInFinder)) { actionImage = [NSImage imageNamed:@"IconFinder.png"]; } else if(itemActionSelector == @selector(deleteItem)) { actionImage = [NSImage imageNamed:@"IconTrash.png"]; } else if(itemActionSelector == @selector(checkIn) || itemActionSelector == @selector(checkInAll)) { actionImage = [NSImage imageNamed:@"IconCheckin.png"]; } else if(itemActionSelector == @selector(checkout)) { actionImage = [NSImage imageNamed:@"IconCheckout.png"]; } else if(itemActionSelector == @selector(open)) { actionImage = [NSImage imageNamed:@"IconReadonly.png"]; } else if(itemActionSelector == @selector(revert)) { actionImage = [NSImage imageNamed:@"IconRevert.png"]; } else if((!item.isTracked && itemActionSelector == @selector(unmapFromWorkspace)) || itemActionSelector == @selector(mapToWorkspace)) { actionImage = [NSImage imageNamed:@"IconFavorite.png"]; } else { continue; } p4ItemAction.delegate = [self.view.window windowController]; initialFrame = CGRectMake(xOffset, 4.0f, btnWidth, actionBtsContent.frame.size.height-8.0f); xOffset += btnWidth; if(p4ItemAction != [btnActions lastObject]) { separatorImageContentFrame = CGRectMake(xOffset, 0, separatorWidth, actionBtsContent.frame.size.height); separatorFrame = CGRectMake(0, 0, separatorWidth, actionBtsContent.frame.size.height); xOffset += separatorWidth; separator = [[NSImageView alloc] init]; [separator setImage:[NSImage imageNamed:@"BarSeparator.png"]]; [separator setFrame:separatorFrame]; separatorImageContent = [[PSView alloc] initWithFrame:separatorImageContentFrame]; separatorImageContent.backgroundColor = [NSUserDefaults colorForKey:kColorBackgroundIconViewActionButton]; [separatorImageContent addSubview:separator]; [actionBtsContent addSubview:separatorImageContent]; } tmpBtn = [[NSButton alloc] init]; [tmpBtn setFrame:initialFrame]; [tmpBtn setBordered:NO]; [tmpBtn.cell setBackgroundColor:[NSUserDefaults colorForKey:kColorBackgroundIconViewActionButton]]; [tmpBtn.cell setImageScaling:NSImageScaleProportionallyDown]; [tmpBtn setTarget:self]; [tmpBtn setAction:@selector(performItemAction:)]; [tmpBtn setTag:[btnActions indexOfObjectIdenticalTo:p4ItemAction]]; [tmpBtn setImage:actionImage]; [tmpBtn setToolTip:p4ItemAction.name]; [actionBtsContent addSubview:tmpBtn]; // [actionBtsContent addSubview:separatorImageContent]; } } - (void)performItemAction:(NSButton*)sender { P4ItemAction *action = [btnActions objectAtIndex:sender.tag]; [action performAction]; } @end