/* HBPresetsViewController.m $
This file is part of the HandBrake source code.
Homepage: .
It may be used under the terms of the GNU General Public License. */
#import "HBPresetsViewController.h"
#import "HBAddCategoryController.h"
@import HandBrakeKit;
// drag and drop pasteboard type
#define kHandBrakePresetPBoardType @"handBrakePresetPBoardType"
// KVO Context
static void *HBPresetsViewControllerContext = &HBPresetsViewControllerContext;
@interface HBPresetCellView : NSTableCellView
@end
@implementation HBPresetCellView
- (void)setBackgroundStyle:(NSBackgroundStyle)backgroundStyle
{
[super setBackgroundStyle:backgroundStyle];
// Customize the built-in preset text color
if ([self.objectValue isBuiltIn])
{
if (backgroundStyle == NSBackgroundStyleDark)
{
self.textField.textColor = [NSColor selectedControlTextColor];
}
else
{
self.textField.textColor = [NSColor blueColor];
}
}
else
{
self.textField.textColor = [NSColor controlTextColor];
}
}
@end
@interface HBPresetsViewController ()
@property (nonatomic, strong) HBPresetsManager *presets;
@property (nonatomic, readwrite) HBPreset *selectedPresetInternal;
@property (nonatomic, unsafe_unretained) IBOutlet NSTreeController *treeController;
@property (nonatomic, strong) IBOutlet NSTextField *headerLabel;
@property (nonatomic, strong) IBOutlet NSLayoutConstraint *headerBottomConstraint;
/**
* Helper var for drag & drop
*/
@property (nonatomic, strong) NSArray *dragNodesArray;
/**
* The status (expanded or not) of the categories.
*/
@property (nonatomic, strong) NSMutableArray *expandedNodes;
@property (nonatomic, unsafe_unretained) IBOutlet NSOutlineView *outlineView;
@end
@implementation HBPresetsViewController
- (instancetype)initWithPresetManager:(HBPresetsManager *)presetManager
{
self = [super initWithNibName:@"Presets" bundle:nil];
if (self)
{
_presets = presetManager;
_selectedPresetInternal = presetManager.defaultPreset;
_expandedNodes = [[NSArray arrayWithArray:[[NSUserDefaults standardUserDefaults]
objectForKey:@"HBPreviewViewExpandedStatus"]] mutableCopy];
}
return self;
}
- (void)loadView
{
[super loadView];
if (NSAppKitVersionNumber >= NSAppKitVersionNumber10_10)
{
self.view.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua];
}
// drag and drop support
[self.outlineView registerForDraggedTypes:@[kHandBrakePresetPBoardType]];
// Re-expand the items
[self expandNodes:[self.treeController.arrangedObjects childNodes]];
[self.treeController setSelectionIndexPath:[self.presets indexPathOfPreset:self.selectedPreset]];
// Update header state
self.showHeader = _showHeader;
[self.treeController addObserver:self forKeyPath:@"selectedObjects" options:NSKeyValueObservingOptionNew context:HBPresetsViewControllerContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == HBPresetsViewControllerContext)
{
HBPreset *selectedNode = [[self.treeController selectedObjects] firstObject];
if (selectedNode && selectedNode.isLeaf && selectedNode != self.selectedPresetInternal)
{
self.selectedPresetInternal = selectedNode;
[self.delegate selectionDidChange];
}
}
else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (BOOL)validateUserInterfaceItem:(id < NSValidatedUserInterfaceItem >)anItem
{
SEL action = anItem.action;
if (action == @selector(exportPreset:))
{
if (![[self.treeController selectedObjects] firstObject])
{
return NO;
}
}
if (action == @selector(setDefault:))
{
if (![[[self.treeController selectedObjects] firstObject] isLeaf])
{
return NO;
}
}
return YES;
}
#pragma mark -
#pragma mark Import Export Preset(s)
- (IBAction)exportPreset:(id)sender
{
// Find the current selection, it can be a category too.
HBPreset *selectedPreset = [[[self.treeController selectedObjects] firstObject] copy];
// Open a panel to let the user choose where and how to save the export file
NSSavePanel *panel = [NSSavePanel savePanel];
panel.title = NSLocalizedString(@"Export presets", @"Export presets save panel title");
// We get the current file name and path from the destination field here
NSURL *defaultExportDirectory = [[NSURL fileURLWithPath:NSHomeDirectory()] URLByAppendingPathComponent:@"Desktop"];
panel.directoryURL = defaultExportDirectory;
panel.nameFieldStringValue = [NSString stringWithFormat:@"%@.json", selectedPreset.name];
[panel beginWithCompletionHandler:^(NSInteger result)
{
if (result == NSModalResponseOK)
{
NSURL *presetExportDirectory = [panel.URL URLByDeletingLastPathComponent];
[[NSUserDefaults standardUserDefaults] setURL:presetExportDirectory forKey:@"LastPresetExportDirectoryURL"];
[selectedPreset writeToURL:panel.URL atomically:YES format:HBPresetFormatJson removeRoot:NO];
}
}];
}
- (IBAction)importPreset:(id)sender
{
NSOpenPanel *panel = [NSOpenPanel openPanel];
panel.title = NSLocalizedString(@"Import presets", @"Import preset open panel title");
panel.allowsMultipleSelection = YES;
panel.canChooseFiles = YES;
panel.canChooseDirectories = NO;
panel.allowedFileTypes = @[@"plist", @"xml", @"json"];
if ([[NSUserDefaults standardUserDefaults] URLForKey:@"LastPresetImportDirectoryURL"])
{
panel.directoryURL = [[NSUserDefaults standardUserDefaults] URLForKey:@"LastPresetImportDirectoryURL"];
}
else
{
panel.directoryURL = [[NSURL fileURLWithPath:NSHomeDirectory()] URLByAppendingPathComponent:@"Desktop"];
}
[panel beginWithCompletionHandler:^(NSInteger result)
{
[[NSUserDefaults standardUserDefaults] setURL:panel.directoryURL forKey:@"LastPresetImportDirectoryURL"];
if (result == NSModalResponseOK)
{
for (NSURL *url in panel.URLs)
{
NSError *error;
HBPreset *import = [[HBPreset alloc] initWithContentsOfURL:url error:&error];
if (import == nil)
{
[self presentError:error];
}
for (HBPreset *child in import.children)
{
[self.presets addPreset:child];
}
}
}
}];
}
#pragma mark - UI Methods
- (void)setShowHeader:(BOOL)showHeader
{
_showHeader = showHeader;
self.headerLabel.hidden = !showHeader;
self.headerBottomConstraint.active = showHeader;
}
- (IBAction)clicked:(id)sender
{
if (self.delegate && [[self.treeController.selectedObjects firstObject] isLeaf])
{
[self.delegate selectionDidChange];
}
}
- (IBAction)renamed:(id)sender
{
if (self.delegate && [[self.treeController.selectedObjects firstObject] isLeaf])
{
[self.delegate selectionDidChange];
[[NSNotificationCenter defaultCenter] postNotificationName:HBPresetsChangedNotification object:nil];
}
}
- (IBAction)addNewPreset:(id)sender
{
if (self.delegate)
{
[self.delegate showAddPresetPanel:sender];
}
}
- (IBAction)deletePreset:(id)sender
{
if ([self.treeController canRemove])
{
// Alert user before deleting preset
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = NSLocalizedString(@"Are you sure you want to permanently delete the selected preset?", @"Delete preset alert -> message");
alert.informativeText = NSLocalizedString(@"You can't undo this action.", @"Delete preset alert -> informative text");
[alert addButtonWithTitle:NSLocalizedString(@"Delete Preset", @"Delete preset alert -> first button")];
[alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"Delete preset alert -> second button")];
alert.alertStyle = NSAlertStyleCritical;
NSInteger status = [alert runModal];
if (status == NSAlertFirstButtonReturn)
{
[self.presets deletePresetAtIndexPath:[self.treeController selectionIndexPath]];
[self setSelection:self.presets.defaultPreset];
}
}
}
- (IBAction)insertCategory:(id)sender
{
HBAddCategoryController *addCategoryController = [[HBAddCategoryController alloc] initWithPresetManager:self.presets];
NSModalResponse returnCode = [NSApp runModalForWindow:addCategoryController.window];
if (returnCode == NSModalResponseOK)
{
NSIndexPath *indexPath = [self.presets indexPathOfPreset:addCategoryController.category];
[self.treeController setSelectionIndexPath:indexPath];
}
}
- (IBAction)setDefault:(id)sender
{
HBPreset *selectedNode = [[self.treeController selectedObjects] firstObject];
if (selectedNode.isLeaf)
{
self.presets.defaultPreset = selectedNode;
[[NSNotificationCenter defaultCenter] postNotificationName:HBPresetsChangedNotification object:nil];
}
}
- (void)setSelectedPreset:(HBPreset *)selectedPreset
{
_selectedPresetInternal = selectedPreset;
[self setSelection:selectedPreset];
}
- (HBPreset *)selectedPreset
{
return _selectedPresetInternal;
}
- (void)setSelection:(HBPreset *)preset
{
NSIndexPath *idx = [self.presets indexPathOfPreset:preset];
if (idx)
{
[self.treeController setSelectionIndexPath:idx];
}
}
- (IBAction)updateBuiltInPresets:(id)sender
{
[self.presets generateBuiltInPresets];
// Re-expand the items
[self expandNodes:[self.treeController.arrangedObjects childNodes]];
}
#pragma mark - Expanded node persistence methods
- (void)expandNodes:(NSArray *)childNodes
{
for (id node in childNodes)
{
[self expandNodes:[node childNodes]];
if ([self.expandedNodes containsObject:@([[node representedObject] hash])])
[self.outlineView expandItem:node expandChildren:YES];
}
}
- (void)outlineViewItemDidExpand:(NSNotification *)notification
{
HBPreset *node = [[[notification userInfo] valueForKey:@"NSObject"] representedObject];
if (![self.expandedNodes containsObject:@(node.hash)])
{
[self.expandedNodes addObject:@(node.hash)];
[[NSUserDefaults standardUserDefaults] setObject:self.expandedNodes forKey:@"HBPreviewViewExpandedStatus"];
}
}
- (void)outlineViewItemDidCollapse:(NSNotification *)notification
{
HBPreset *node = [[[notification userInfo] valueForKey:@"NSObject"] representedObject];
[self.expandedNodes removeObject:@(node.hash)];
[[NSUserDefaults standardUserDefaults] setObject:self.expandedNodes forKey:@"HBPreviewViewExpandedStatus"];
}
#pragma mark - Drag & Drops
/**
* draggingSourceOperationMaskForLocal
*/
- (NSDragOperation)draggingSession:(NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context
{
return NSDragOperationMove;
}
/**
* outlineView:writeItems:toPasteboard
*/
- (BOOL)outlineView:(NSOutlineView *)ov writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard
{
// Return no if we are trying to drag a built-in preset
for (id item in items) {
if ([[item representedObject] isBuiltIn])
return NO;
}
[pboard declareTypes:@[kHandBrakePresetPBoardType] owner:self];
// keep track of this nodes for drag feedback in "validateDrop"
self.dragNodesArray = items;
return YES;
}
/**
* outlineView:validateDrop:proposedItem:proposedChildrenIndex:
*
* This method is used by NSOutlineView to determine a valid drop target.
*/
- (NSDragOperation)outlineView:(NSOutlineView *)ov
validateDrop:(id )info
proposedItem:(id)item
proposedChildIndex:(NSInteger)index
{
NSDragOperation result = NSDragOperationNone;
if (!item)
{
if (index == 0)
{
// don't allow to drop on top
result = NSDragOperationNone;
}
else
{
// no item to drop on
result = NSDragOperationGeneric;
}
}
else
{
if (index == -1 || [[item representedObject] isBuiltIn] || [self.dragNodesArray containsObject:item])
{
// don't allow dropping on a child
result = NSDragOperationNone;
}
else
{
// drop location is a container
result = NSDragOperationMove;
}
}
return result;
}
/**
* handleInternalDrops:pboard:withIndexPath:
*
* The user is doing an intra-app drag within the outline view.
*/
- (void)handleInternalDrops:(NSPasteboard *)pboard withIndexPath:(NSIndexPath *)indexPath
{
// user is doing an intra app drag within the outline view:
NSArray *newNodes = self.dragNodesArray;
// move the items to their new place (we do this backwards, otherwise they will end up in reverse order)
NSInteger idx;
for (idx = ([newNodes count] - 1); idx >= 0; idx--)
{
[self.treeController moveNode:newNodes[idx] toIndexPath:indexPath];
// Call manually this because the NSTreeController doesn't call
// the KVC accessors method for the root node.
if (indexPath.length == 1)
{
[self.presets performSelector:@selector(nodeDidChange:) withObject:nil];
}
}
// keep the moved nodes selected
NSMutableArray *indexPathList = [NSMutableArray array];
for (NSUInteger i = 0; i < [newNodes count]; i++)
{
[indexPathList addObject:[newNodes[i] indexPath]];
}
[self.treeController setSelectionIndexPaths: indexPathList];
}
/**
* outlineView:acceptDrop:item:childIndex
*
* This method is called when the mouse is released over an outline view that previously decided to allow a drop
* via the validateDrop method. The data source should incorporate the data from the dragging pasteboard at this time.
* 'index' is the location to insert the data as a child of 'item', and are the values previously set in the validateDrop: method.
*
*/
- (BOOL)outlineView:(NSOutlineView *)ov acceptDrop:(id )info item:(id)targetItem childIndex:(NSInteger)index
{
// note that "targetItem" is a NSTreeNode proxy
//
BOOL result = NO;
// find the index path to insert our dropped object(s)
NSIndexPath *indexPath;
if (targetItem)
{
// drop down inside the tree node:
// feth the index path to insert our dropped node
indexPath = [[targetItem indexPath] indexPathByAddingIndex:index];
}
else
{
// drop at the top root level
if (index == -1) // drop area might be ambibuous (not at a particular location)
indexPath = [NSIndexPath indexPathWithIndex:self.presets.root.children.count]; // drop at the end of the top level
else
indexPath = [NSIndexPath indexPathWithIndex:index]; // drop at a particular place at the top level
}
NSPasteboard *pboard = [info draggingPasteboard]; // get the pasteboard
// check the dragging type -
if ([pboard availableTypeFromArray:@[kHandBrakePresetPBoardType]])
{
// user is doing an intra-app drag within the outline view
[self handleInternalDrops:pboard withIndexPath:indexPath];
result = YES;
}
return result;
}
@end