/* 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 "HBPresetsManager.h"
#import "HBPreset.h"
// drag and drop pasteboard type
#define kHandBrakePresetPBoardType @"handBrakePresetPBoardType"
@interface HBPresetsViewController ()
@property (nonatomic, strong) HBPresetsManager *presets;
@property (nonatomic, unsafe_unretained) IBOutlet NSTreeController *treeController;
/**
* Helper var for drag & drop
*/
@property (nonatomic, strong) NSArray *dragNodesArray;
/**
* The status (expanded or not) of the folders.
*/
@property (nonatomic, strong) NSMutableArray *expandedNodes;
@property (unsafe_unretained) IBOutlet NSOutlineView *outlineView;
@end
@implementation HBPresetsViewController
@synthesize enabled = _enabled;
- (instancetype)initWithPresetManager:(HBPresetsManager *)presetManager
{
self = [super initWithNibName:@"Presets" bundle:nil];
if (self)
{
_presets = presetManager;
_expandedNodes = [[NSArray arrayWithArray:[[NSUserDefaults standardUserDefaults]
objectForKey:@"HBPreviewViewExpandedStatus"]] mutableCopy];
}
return self;
}
- (void)loadView
{
[super loadView];
// 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.presets.defaultPreset]];
}
- (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 folder 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", nil);
// 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 == NSOKButton)
{
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", nil);
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"];
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
- (IBAction)clicked:(id)sender
{
if (self.delegate && [[self.treeController.selectedObjects firstObject] isLeaf])
{
[self.delegate selectionDidChange];
}
}
- (IBAction)addNewPreset:(id)sender
{
if (self.delegate)
{
[self.delegate showAddPresetPanel:sender];
}
}
- (IBAction)deletePreset:(id)sender
{
if ([self.treeController canRemove])
{
// Save the current selection path and apply it again after the deletion
NSIndexPath *currentSelection = [self.treeController selectionIndexPath];
/* Alert user before deleting preset */
NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"Are you sure you want to permanently delete the selected preset?", nil)
defaultButton:NSLocalizedString(@"Delete Preset", nil)
alternateButton:NSLocalizedString(@"Cancel", nil)
otherButton:nil
informativeTextWithFormat:NSLocalizedString(@"You can't undo this action.", nil)];
[alert setAlertStyle:NSCriticalAlertStyle];
NSInteger status = [alert runModal];
if (status == NSAlertDefaultReturn)
{
[self.presets deletePresetAtIndexPath:[self.treeController selectionIndexPath]];
}
[self.treeController setSelectionIndexPath:currentSelection];
}
}
- (IBAction)insertFolder:(id)sender
{
NSIndexPath *selectionIndexPath = [self.treeController selectionIndexPath];
if (!selectionIndexPath || [[[self.treeController selectedObjects] firstObject] isBuiltIn])
{
selectionIndexPath = [NSIndexPath indexPathWithIndex:self.presets.root.children.count];
}
HBPreset *node = [[HBPreset alloc] initWithFolderName:@"New Folder" builtIn:NO];
[self.treeController insertObject:node atArrangedObjectIndexPath:selectionIndexPath];
}
- (IBAction)setDefault:(id)sender
{
HBPreset *selectedNode = [[self.treeController selectedObjects] firstObject];
if ([[selectedNode valueForKey:@"isLeaf"] boolValue])
{
self.presets.defaultPreset = selectedNode;
}
}
- (void)deselect
{
[self.treeController setSelectionIndexPath:nil];
}
- (void)setSelection:(HBPreset *)preset
{
NSIndexPath *idx = [self.presets indexPathOfPreset:preset];
if (idx)
{
[self.treeController setSelectionIndexPath:idx];
}
}
- (HBPreset *)selectedPreset
{
HBPreset *selectedNode = [[self.treeController selectedObjects] firstObject];
if ([[selectedNode valueForKey:@"isLeaf"] boolValue])
{
return selectedNode;
}
else
{
return self.presets.defaultPreset;
}
}
- (IBAction)updateBuiltInPresets:(id)sender
{
[self.presets generateBuiltInPresets];
// Re-expand the items
[self expandNodes:[self.treeController.arrangedObjects childNodes]];
}
#pragma mark - Added Functionality (optional)
/* We use this to provide tooltips for the items in the presets outline view */
- (NSString *)outlineView:(NSOutlineView *)fPresetsOutlineView
toolTipForCell:(NSCell *)cell
rect:(NSRectPointer)rect
tableColumn:(NSTableColumn *)tc
item:(id)item
mouseLocation:(NSPoint)mouseLocation
{
return [[item representedObject] presetDescription];
}
/* Use to customize the font and display characteristics of the title cell */
- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
NSColor *fontColor;
if ([self.outlineView selectedRow] == [self.outlineView rowForItem:item])
{
fontColor = [NSColor blackColor];
}
else
{
if ([[item representedObject] isBuiltIn])
{
fontColor = [NSColor blueColor];
}
else // User created preset, use a black font
{
fontColor = [NSColor blackColor];
}
}
[cell setTextColor:fontColor];
}
#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