// // ColumnViewDetails.m // Perforce // // Created by Adam Czubernat on 03.06.2013. // Copyright (c) 2013 Perforce Software, Inc. All rights reserved. // #import "ColumnViewDetails.h" #import "TagsView.h" #import "VersionsViewController.h" const char * DetailsGlobalQueueLabel = "com.perforce.PreviewQueue"; dispatch_queue_t DetailsGlobalQueue = NULL; @interface ColumnViewDetails () { P4Item *item; __unsafe_unretained id actionDelegate; NSImage *image; NSArray *itemActions; VersionsViewController *versionsController; // Outlets __weak IBOutlet PSView *contentView; __weak IBOutlet NSScrollView *contentScrollView; __weak IBOutlet NSTextField *versionLabel; __weak IBOutlet NSTextField *titleLabel; __weak IBOutlet PSView *versionSeparator; __weak IBOutlet PSView *titleSeparator; __weak IBOutlet PSView *imageView; __weak IBOutlet NSView *actionsHeader; __weak IBOutlet NSTextField *actionsHeaderTitle; __weak IBOutlet NSView *tagsHeader; __weak IBOutlet NSTextField *tagsHeaderTitle; __weak IBOutlet TagsView *tagsView; IBOutlet NSView *prototypeMetadataView; __weak IBOutlet NSTextField *prototypeMetadataTitle; __weak IBOutlet NSTextField *prototypeMetadataValue; IBOutlet NSView *prototypeActionView; __weak IBOutlet NSImageView *prototypeActionImage; __weak IBOutlet NSTextField *prototypeActionTitle; __weak IBOutlet NSButton *prototypeActionButton; __weak IBOutlet PSView *prototypeActionSeparator; } - (void)loadDirectoryItem; - (void)loadFileItem; - (void)loadImage; - (void)setRevision:(NSString *)revision; - (NSView *)addPrototypeView:(NSView *)prototype; - (void)fitSubviews; - (void)addMetadataWithTitle:(NSString *)title value:(NSString *)value; - (void)addAction:(P4ItemAction *)action; @end @implementation ColumnViewDetails static NSImage *horizontalSeparatorImage; static NSColor *backgroundColor; static NSColor *selectionColor; static NSColor *textColor; static NSColor *textColorSelected; static NSColor *textColorSecondary; static NSColor *textColorTag; static NSColor *textColorCheckedOut; - (id)initWithItem:(P4Item *)anItem actionDelegate:(id)delegate { if (self = [self initWithNibName:NSStringFromClass([self class]) bundle:nil]) { item = anItem; actionDelegate = delegate; } return self; } - (void)showVersions { if (versionsController) [versionsController.view removeFromSuperview]; versionsController = [[VersionsViewController alloc] initWithItem:item actionDelegate:actionDelegate]; CGRect frame = versionsController.view.frame; frame.origin.x = contentScrollView.frame.size.width; frame.size.height = self.view.frame.size.height; versionsController.view.frame = frame; [self.view addSubview:versionsController.view]; frame = self.view.frame; frame.size.width = CGRectGetMaxX(versionsController.view.frame); self.view.frame = frame; } - (void)loadView { [super loadView]; // Load images and colors static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ horizontalSeparatorImage = [NSImage imageNamed:@"SeparatorHorizontalDark.png"]; horizontalSeparatorImage = [horizontalSeparatorImage resizableImageWithLeftCap:10.0f rightCap:1.0f]; backgroundColor = [NSUserDefaults colorForKey:kColorBackgroundColumn]; selectionColor = [NSUserDefaults colorForKey:kColorBackgroundColumnSelected]; textColor = [NSUserDefaults colorForKey:kColorTextColumn]; textColorSelected = [NSUserDefaults colorForKey:kColorTextColumnSelected]; textColorSecondary = [NSUserDefaults colorForKey:kColorTextIconSecondary]; textColorTag = [NSUserDefaults colorForKey:kColorBackgroundTagDark]; textColorCheckedOut = [NSUserDefaults colorForKey:kColorStatusCheckedOut]; }); // Set images and colors [contentScrollView setBackgroundColor:backgroundColor]; [titleSeparator setImage:horizontalSeparatorImage]; [titleSeparator setContentMode:PSViewContentModeFillHorizontal]; [prototypeActionSeparator setImage:horizontalSeparatorImage]; [prototypeActionSeparator setContentMode:PSViewContentModeFillHorizontal]; [versionSeparator setBackgroundColor:textColorCheckedOut]; titleLabel.textColor = prototypeMetadataValue.textColor = prototypeActionTitle.textColor = textColor; prototypeMetadataTitle.textColor = actionsHeaderTitle.textColor = tagsHeaderTitle.textColor = textColorSecondary; versionLabel.textColor = textColorCheckedOut; // Load determined by type if ([item isDirectory]) [self loadDirectoryItem]; else [self loadFileItem]; // Load common values for two types titleLabel.stringValue = item.name; itemActions = item.actions; for (P4ItemAction *action in itemActions) { [self addAction:action]; } if ((!item.isTracked || item.isDirectory) && ![item.status isEqualToString:@"add"] && ![item.status isEqualToString:@"move/add"]) { [tagsHeader removeFromSuperview]; [tagsView removeFromSuperview]; tagsHeader = tagsView = nil; } else { [tagsView setAllowsAdding:item.isEditable || item.status]; [tagsView setItem:item]; } // Resize content view [self fitSubviews]; // Remove prototypes [self removePrototypeView:prototypeMetadataView]; [self removePrototypeView:prototypeActionView]; [self loadImage]; } #pragma mark - Private - (void)loadDirectoryItem { [self setRevision:nil]; } - (void)loadFileItem { NSDictionary *metadata = item.metadata; [self setRevision:[metadata objectForKey:@"haveRev"]]; // Add concretes NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; formatter.dateStyle = NSDateFormatterLongStyle; if (!item.isTracked) [self addMetadataWithTitle:@"UNTRACKED" value:@""]; if (item.isTracked) { NSNumber *headTime = [metadata objectForKey:@"headModTime"]; NSDate *headDate = headTime ? [NSDate dateWithTimeIntervalSince1970: headTime.doubleValue] : nil; [self addMetadataWithTitle:@"MODIFIED IN" value:[formatter stringFromDate:headDate]]; if (item.status) { [self addMetadataWithTitle:@"CHECKED OUT BY" value:@"You"]; } else if (item.statusOwner) { prototypeMetadataTitle.stringValue = @"CHECKED OUT BY"; [prototypeMetadataValue setAttributedStringValue: [[NSAttributedString alloc] initWithString:item.statusOwner attributes:@{ NSFontAttributeName : prototypeMetadataValue.font, NSUnderlineStyleAttributeName : @(NSSingleUnderlineStyle), NSLinkAttributeName : [NSString stringWithFormat:@"mailto:%@", item.statusOwner], }]]; [prototypeMetadataValue setAllowsEditingTextAttributes:YES]; [prototypeMetadataValue setSelectable:YES]; [self addPrototypeView:prototypeMetadataView]; } else [self addMetadataWithTitle:@"CHECKED IN" value:@"Available"]; } if (item.status) [self addMetadataWithTitle:@"ACTION" value:item.status]; NSString *movedFile = [item.metadata objectForKey:@"movedFile"]; if (movedFile.length) { NSString *parentPath = [item.remotePath stringByDeletingPath]; if ([[movedFile stringByDeletingPath] isEqualToString:parentPath]) [self addMetadataWithTitle:@"RENAMED FROM" value:[movedFile lastPathComponent]]; else if ([item.status isEqualToString:@"move/delete"]) [self addMetadataWithTitle:@"MOVED TO" value:movedFile]; else [self addMetadataWithTitle:@"MOVED FROM" value:movedFile]; } } - (void)loadImage { if (!imageView) return; // Load image asynchronously static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ DetailsGlobalQueue = dispatch_queue_create(DetailsGlobalQueueLabel, NULL); }); dispatch_async(DetailsGlobalQueue, ^{ if (!image) image = [item previewWithSize:imageView.bounds.size]; dispatch_async(dispatch_get_main_queue(), ^{ imageView.image = image; }); }); } - (void)setRevision:(NSString *)revision { if (!revision) { titleLabel.frame = CGRectUnion(titleLabel.frame, versionLabel.frame); [versionLabel removeFromSuperview]; [versionSeparator removeFromSuperview]; return; } 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 += 8.0f; frame = titleLabel.frame; frame.size.width = CGRectGetMaxX(frame) - offset; frame.origin.x = offset; titleLabel.frame = frame; } - (NSView *)addPrototypeView:(NSView *)prototype { NSData *archivedView = [NSKeyedArchiver archivedDataWithRootObject:prototype]; NSView *copy = [NSKeyedUnarchiver unarchiveObjectWithData:archivedView]; if (prototype.superview) { [prototype removeFromSuperview]; } else { // Move subviews below CGFloat height = copy.frame.size.height; CGFloat offset = copy.frame.origin.y + height; for (NSView *subview in contentView.subviews) { CGRect frame = subview.frame; if (frame.origin.y >= offset) continue; frame.origin.y -= height; subview.frame = frame; } } CGRect frame = prototype.frame; frame.origin.y -= frame.size.height; prototype.frame = frame; [contentView addSubview:copy]; return copy; } - (void)removePrototypeView:(NSView *)prototype { if (!prototype.superview) return; [prototype removeFromSuperview]; CGFloat height = prototype.frame.size.height; CGFloat offset = prototype.frame.origin.y + height; for (NSView *subview in contentView.subviews) { CGRect frame = subview.frame; if (frame.origin.y >= offset) continue; frame.origin.y += height; subview.frame = frame; } } - (void)fitSubviews { CGFloat min = 0.0f; for (NSView *subview in contentView.subviews) { min = fminf(subview.frame.origin.y, min); } if (min > 0.0f) return; // Resize content container to fit CGRect frame = contentView.frame; frame.size.height -= min; contentView.frame = frame; } - (void)addMetadataWithTitle:(NSString *)title value:(NSString *)value { if (!title || !value) return; prototypeMetadataTitle.stringValue = title; prototypeMetadataValue.stringValue = value; [self addPrototypeView:prototypeMetadataView]; } - (void)addAction:(P4ItemAction *)action { if (action.disabled) return; action.delegate = actionDelegate; NSImage *actionImage; SEL actionSelector = action.selector; if (actionSelector == @selector(open)) { actionImage = [NSImage imageNamed:@"IconReadonlyDark48"]; } else if (actionSelector == @selector(openWithCheckout)) { actionImage = [NSImage imageNamed:@"IconOpenDark"]; } else if (actionSelector == @selector(openFromDepot)) { actionImage = [NSImage imageNamed:@"IconOpenFromDepotDark"]; } else if (actionSelector == @selector(addItem)) { actionImage = [NSImage imageNamed:@"IconMarkForAddDark"]; } else if (actionSelector == @selector(revertIfUnchanged)) { actionImage = [NSImage imageNamed:@"IconUndoCheckoutDark"]; } else if (actionSelector == @selector(mapToWorkspace)) { actionImage = [NSImage imageNamed:@"IconMapToWorkspaceDark"]; } else if (actionSelector == @selector(unmapFromWorkspace)) { actionImage = [NSImage imageNamed:@"IconUnmapFromWorkspaceDark"]; } else if (actionSelector == @selector(shelve)) { actionImage = [NSImage imageNamed:@"IconShelveDark"]; } else if (actionSelector == @selector(unshelve)) { actionImage = [NSImage imageNamed:@"IconUnshelveDark"]; } else if (actionSelector == @selector(discardShelve)) { actionImage = [NSImage imageNamed:@"IconShelveDiscardDark"]; } else if (actionSelector == @selector(showVersions)) { actionImage = [NSImage imageNamed:@"IconVersionsDark"]; } else if (actionSelector == @selector(createDirectory)) { actionImage = [NSImage imageNamed:@"IconNewDirectoryDark"]; } else if (actionSelector == @selector(showInFinder)) { actionImage = [NSImage imageNamed:@"IconFinderDark48.png"]; } else if (actionSelector == @selector(deleteItem)) { actionImage = [NSImage imageNamed:@"IconTrashDark48.png"]; } else if (actionSelector == @selector(checkIn) || actionSelector == @selector(checkInAll)) { actionImage = [NSImage imageNamed:@"IconCheckinDark48.png"]; } else if (actionSelector == @selector(checkout)) { actionImage = [NSImage imageNamed:@"IconCheckoutDark48.png"]; } else if (actionSelector == @selector(open)) { actionImage = [NSImage imageNamed:@"IconReadonlyDark48.png"]; } else if (actionSelector == @selector(revert)) { actionImage = [NSImage imageNamed:@"IconRevertDark.png"]; } else if (actionSelector == @selector(copyShareLink)) { actionImage = [NSImage imageNamed:@"IconCopyLinkDark"]; } else { actionImage = [NSImage imageNamed:@"IconFavoriteDark.png"]; } prototypeActionTitle.stringValue = action.name; prototypeActionImage.image = actionImage; prototypeActionButton.tag = 666; // Number of the Beast prototypeActionButton.alphaValue = 0.5f; NSView *copy = [self addPrototypeView:prototypeActionView]; NSButton *actionButton = [copy viewWithTag:666]; [actionButton setTarget:action]; [actionButton setAction:@selector(performAction)]; } #pragma mark - Menu delegate - (void)menuWillOpen:(NSMenu *)menu { [menu setAllowsContextMenuPlugIns:NO]; [menu setItem:item delegate:[self.view.window windowController]]; } @end