diff options
Diffstat (limited to 'macosx/HBQueueController.mm')
-rw-r--r-- | macosx/HBQueueController.mm | 1716 |
1 files changed, 903 insertions, 813 deletions
diff --git a/macosx/HBQueueController.mm b/macosx/HBQueueController.mm index 542b2ba33..9ca4137bf 100644 --- a/macosx/HBQueueController.mm +++ b/macosx/HBQueueController.mm @@ -1,3 +1,4 @@ + /* HBQueueController This file is part of the HandBrake source code. @@ -5,92 +6,53 @@ It may be used under the terms of the GNU General Public License. */ #import "HBQueueController.h" + #import "HBCore.h" #import "Controller.h" +#import "HBOutputPanelController.h" #import "HBQueueOutlineView.h" -#import "HBImageAndTextCell.h" #import "HBUtilities.h" #import "HBJob.h" -#import "HBAudioDefaults.h" -#import "HBAudioTrack.h" +#import "HBJob+UIAdditions.h" -#import "HBPicture+UIAdditions.h" -#import "HBFilters+UIAdditions.h" +#import "HBDistributedArray.h" -#define HB_ROW_HEIGHT_TITLE_ONLY 17.0 +#import "HBDockTile.h" // Pasteboard type for or drag operations -#define DragDropSimplePboardType @"HBQueueCustomOutlineViewPboardType" +#define DragDropSimplePboardType @"HBQueueCustomOutlineViewPboardType" -#pragma mark - +// DockTile update freqency in total percent increment +#define dockTileUpdateFrequency 0.1f -//------------------------------------------------------------------------------------ -// NSMutableAttributedString (HBAdditions) -//------------------------------------------------------------------------------------ - -@interface NSMutableAttributedString (HBAdditions) -- (void) appendString: (NSString*)aString withAttributes: (NSDictionary *)aDictionary; -@end - -@implementation NSMutableAttributedString (HBAdditions) -- (void) appendString: (NSString*)aString withAttributes: (NSDictionary *)aDictionary -{ - NSAttributedString *s = [[[NSAttributedString alloc] initWithString:aString - attributes:aDictionary] autorelease]; - [self appendAttributedString:s]; -} -@end - -#pragma mark - +#define HB_ROW_HEIGHT_TITLE_ONLY 17.0 @interface HBQueueController () <HBQueueOutlineViewDelegate> -{ - HBController *fHBController; // reference to HBController - NSMutableArray *fJobGroups; // mirror image of the queue array from controller.mm - - int pidNum; // Records the PID number from HBController for this instance - int fEncodingQueueItem; // corresponds to the index of fJobGroups encoding item - int fPendingCount; // Number of various kinds of job groups in fJobGroups. - int fWorkingCount; - NSMutableIndexSet *fSavedExpandedItems; // used by save/restoreOutlineViewState to preserve which items are expanded - NSMutableIndexSet *fSavedSelectedItems; // used by save/restoreOutlineViewState to preserve which items are selected - NSMutableDictionary *descriptions; +@property (nonatomic, readonly) HBDockTile *dockTile; +@property (nonatomic, readwrite) double dockIconProgress; - NSTimer *fAnimationTimer; // animates the icon of the current job in the queue outline view - int fAnimationIndex; // used to generate name of image used to animate the current job in the queue outline view +@property (assign) IBOutlet NSTextField *progressTextField; +@property (assign) IBOutlet NSTextField *countTextField; +@property (assign) IBOutlet HBQueueOutlineView *outlineView; - IBOutlet NSTextField *fProgressTextField; +@property (nonatomic, readonly) NSMutableDictionary *descriptions; - IBOutlet HBQueueOutlineView *fOutlineView; - IBOutlet NSTextField *fQueueCountField; - NSArray *fDraggedNodes; +@property (nonatomic, readonly) HBDistributedArray *jobs; +@property (nonatomic, retain) HBJob *currentJob; - // Text Styles - NSMutableParagraphStyle *ps; - NSDictionary *detailAttr; - NSDictionary *detailBoldAttr; - NSDictionary *titleAttr; - NSDictionary *shortHeightAttr; -} - -@property (nonatomic, readonly) HBCore *queueCore; +@property (nonatomic, readwrite) NSUInteger pendingItemsCount; +@property (nonatomic, readwrite) NSUInteger workingItemsCount; -/* control encodes in the window */ -- (IBAction)removeSelectedQueueItem: (id)sender; -- (IBAction)revealSelectedQueueItem: (id)sender; -- (IBAction)editSelectedQueueItem: (id)sender; +@property (nonatomic, retain) NSArray *dragNodesArray; @end @implementation HBQueueController -//------------------------------------------------------------------------------------ -// init -//------------------------------------------------------------------------------------ -- (id)init +- (instancetype)init { if (self = [super initWithWindowNibName:@"Queue"]) { @@ -101,267 +63,743 @@ // If/when we switch to using bindings, this can probably go away. [self window]; - // Our defaults - [[NSUserDefaults standardUserDefaults] registerDefaults:@{@"QueueWindowIsOpen": @"NO"}]; + _descriptions = [[NSMutableDictionary alloc] init]; + + // Workaround to avoid a bug in Snow Leopard + // we can switch back to [[NSApplication sharedApplication] applicationIconImage] + // when we won't support it anymore. + NSImage *appIcon = [NSImage imageNamed:@"HandBrake"]; + [appIcon setSize:NSMakeSize(1024, 1024)]; + + // Load the dockTile and instiante initial text fields + _dockTile = [[HBDockTile alloc] initWithDockTile:[[NSApplication sharedApplication] dockTile] + image:appIcon]; - fJobGroups = [[NSMutableArray arrayWithCapacity:0] retain]; - descriptions = [[NSMutableDictionary alloc] init]; + int loggingLevel = [[[NSUserDefaults standardUserDefaults] objectForKey:@"LoggingLevel"] intValue]; - [self initStyles]; + // Init a separate instance of libhb for the queue + _core = [[HBCore alloc] initWithLoggingLevel:loggingLevel]; + _core.name = @"QueueCore"; + + [self loadQueueFile]; } return self; } -- (void)setQueueArray:(NSMutableArray *)QueueFileArray +- (void)dealloc +{ + // clear the delegate so that windowWillClose is not attempted + if ([[self window] delegate] == self) + [[self window] setDelegate:nil]; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + [_core release]; + [_jobs release]; + [_currentJob release]; + + [_dockTile release]; + [_descriptions release]; + [_dragNodesArray release]; + + [super dealloc]; +} + +- (void)windowDidLoad +{ + // lets setup our queue list outline view for drag and drop here + [self.outlineView registerForDraggedTypes:@[DragDropSimplePboardType]]; + [self.outlineView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:YES]; + [self.outlineView setVerticalMotionCanBeginDrag:YES]; + + // Don't allow autoresizing of main column, else the "delete" column will get + // pushed out of view. + [self.outlineView setAutoresizesOutlineColumn: NO]; +} + +- (void)windowWillClose:(NSNotification *)aNotification +{ + [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"QueueWindowIsOpen"]; +} + +/** + * Displays and brings the queue window to the front + */ +- (IBAction)showWindow:(id)sender +{ + [super showWindow:sender]; + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"QueueWindowIsOpen"]; +} + +#pragma mark Toolbar + +- (BOOL)validateToolbarItem:(NSToolbarItem *)theItem +{ + SEL action = theItem.action; + HBState s = self.core.state; + + if (action == @selector(toggleStartCancel:)) + { + if ((s == HBStatePaused) || (s == HBStateWorking) || (s == HBStateMuxing)) + { + theItem.image = [NSImage imageNamed:@"stopencode"]; + theItem.label = NSLocalizedString(@"Stop", @""); + theItem.toolTip = NSLocalizedString(@"Stop Encoding", @""); + return YES; + } + else + { + theItem.image = [NSImage imageNamed:@"encode"]; + theItem.label = NSLocalizedString(@"Start", @""); + theItem.toolTip = NSLocalizedString(@"Start Encoding", @""); + return (self.pendingItemsCount > 0); + } + } + + if (action == @selector(togglePauseResume:)) + { + if (s == HBStatePaused) + { + theItem.image = [NSImage imageNamed:@"encode"]; + theItem.label = NSLocalizedString(@"Resume", @""); + theItem.toolTip = NSLocalizedString(@"Resume Encoding", @""); + return YES; + } + else + { + theItem.image = [NSImage imageNamed:@"pauseencode"]; + theItem.label = NSLocalizedString(@"Pause", @""); + theItem.toolTip = NSLocalizedString(@"Pause Encoding", @""); + return (s == HBStateWorking || s == HBStateMuxing); + } + } + + return NO; +} + +#pragma mark - +#pragma mark Queue File + +- (void)reloadQueue { - [fJobGroups setArray:QueueFileArray]; - [descriptions removeAllObjects]; + [self getQueueStats]; + [self.outlineView reloadData]; +} - [fOutlineView reloadData]; +- (void)loadQueueFile +{ + NSURL *queueURL = [NSURL fileURLWithPath:[[HBUtilities appSupportPath] stringByAppendingPathComponent:@"Queue/Queue.hbqueue"]]; + _jobs = [[HBDistributedArray alloc] initWithURL:queueURL]; + + [self reloadQueue]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reloadQueue) name:HBDistributedArrayChanged object:_jobs]; +} + +- (void)addJob:(HBJob *)item +{ + [self.jobs beginTransaction]; + [self.jobs addObject:item]; + [self.jobs commit]; + + [self reloadQueue]; +} +- (void)addJobsFromArray:(NSArray *)items; +{ + [self.jobs beginTransaction]; + [self.jobs addObjectsFromArray:items]; + [self.jobs commit]; + + [self reloadQueue]; +} + +- (BOOL)jobExistAtURL:(NSURL *)url +{ + for (HBJob *item in self.jobs) + { + if ([item.destURL isEqualTo:url]) + { + return YES; + } + } + return NO; +} + +- (void)removeQueueItemAtIndex:(NSUInteger)index +{ + [self.jobs beginTransaction]; + if (self.jobs.count > index) + { + [self.jobs removeObjectAtIndex:index]; + } + [self.jobs commit]; + + [self reloadQueue]; +} + +/** + * Updates the queue status label. + */ +- (void)getQueueStats +{ // lets get the stats on the status of the queue array - - fPendingCount = 0; - fWorkingCount = 0; - - int i = 0; - for (HBJob *job in fJobGroups) - { - if (job.state == HBJobStateWorking) // being encoded - { - fWorkingCount++; - // we have an encoding job so, lets start the animation timer - if (job.pidId == pidNum) - { - fEncodingQueueItem = i; - } - } + NSUInteger pendingCount = 0; + NSUInteger workingCount = 0; + + for (HBJob *job in self.jobs) + { + if (job.state == HBJobStateWorking) // being encoded + { + workingCount++; + } if (job.state == HBJobStateReady) // pending { - fPendingCount++; - } - i++; - } + pendingCount++; + } + } - // Set the queue status field in the queue window - NSMutableString *string; - if (fPendingCount == 0) + NSString *string; + if (pendingCount == 0) { - string = [NSMutableString stringWithFormat: NSLocalizedString( @"No encode pending", @"" )]; + string = NSLocalizedString(@"No encode pending", @""); } - else if (fPendingCount == 1) + else if (pendingCount == 1) { - string = [NSMutableString stringWithFormat: NSLocalizedString( @"%d encode pending", @"" ), fPendingCount]; + string = [NSString stringWithFormat: NSLocalizedString(@"%d encode pending", @""), pendingCount]; } else { - string = [NSMutableString stringWithFormat: NSLocalizedString( @"%d encodes pending", @"" ), fPendingCount]; + string = [NSString stringWithFormat: NSLocalizedString(@"%d encodes pending", @""), pendingCount]; } - [fQueueCountField setStringValue:string]; - + + self.countTextField.stringValue = string; + [self.controller setQueueState:string]; + + self.pendingItemsCount = pendingCount; + self.workingItemsCount = workingCount; } -/* This method sets the status string in the queue window - * and is called from Controller.mm (fHBController) - * instead of running another timer here polling libhb - * for encoding status - */ -- (void)setQueueStatusString:(NSString *)statusString +- (NSUInteger)count { - [fProgressTextField setStringValue:statusString]; + return self.jobs.count; } -//------------------------------------------------------------------------------------ -// dealloc -//------------------------------------------------------------------------------------ -- (void)dealloc +/** + * Used to get the next pending queue item and return it if found + */ +- (HBJob *)getNextPendingQueueItem { - // clear the delegate so that windowWillClose is not attempted - if( [[self window] delegate] == self ) - [[self window] setDelegate:nil]; + for (HBJob *job in self.jobs) + { + if (job.state == HBJobStateReady) + { + return job; + } + } + return nil; +} - [fJobGroups release]; +/** + * This method will set any item marked as encoding back to pending + * currently used right after a queue reload + */ +- (void)setEncodingJobsAsPending +{ + [self.jobs beginTransaction]; + for (HBJob *job in self.jobs) + { + // We want to keep any queue item that is pending or was previously being encoded + if (job.state == HBJobStateWorking) + { + job.state = HBJobStateReady; + } + } + [self.jobs commit]; - [fSavedExpandedItems release]; - [fSavedSelectedItems release]; + [self reloadQueue]; +} - [ps release]; - [detailAttr release]; - [detailBoldAttr release]; - [titleAttr release]; - [shortHeightAttr release]; +/** + * This method will clear the queue of any encodes that are not still pending + * this includes both successfully completed encodes as well as cancelled encodes + */ +- (void)clearEncodedJobs +{ + [self.jobs beginTransaction]; + NSMutableArray *encodedJobs = [NSMutableArray array]; + for (HBJob *job in self.jobs) + { + if (job.state == HBJobStateCompleted || job.state == HBJobStateCanceled) + { + [encodedJobs addObject:job]; + } + } - [[NSNotificationCenter defaultCenter] removeObserver:self]; + [self.jobs removeObjectsInArray:encodedJobs]; + [self.jobs commit]; - [super dealloc]; + [self reloadQueue]; } -//------------------------------------------------------------------------------------ -// Receive HB handle -//------------------------------------------------------------------------------------ -- (void)setCore: (HBCore *)core +/** + * This method will clear the queue of all encodes. effectively creating an empty queue + */ +- (void)removeAllJobs { - _queueCore = core; -} + [self.jobs beginTransaction]; + [self.jobs removeAllObjects]; + [self.jobs commit]; -//------------------------------------------------------------------------------------ -// Receive HBController -//------------------------------------------------------------------------------------ -- (void)setHBController: (HBController *)controller -{ - fHBController = controller; + [self reloadQueue]; } -- (void)setPidNum: (int)myPidnum +/** + * this is actually called from the queue controller to modify the queue array and return it back to the queue controller + */ +- (void)moveObjectsInQueueArray:(NSMutableArray *)array fromIndexes:(NSIndexSet *)indexSet toIndex:(NSUInteger)insertIndex { - pidNum = myPidnum; - [HBUtilities writeToActivityLog: "HBQueueController : My Pidnum is %d", pidNum]; + [self.jobs beginTransaction]; + + NSUInteger index = [indexSet lastIndex]; + NSUInteger aboveInsertIndexCount = 0; + + NSUInteger removeIndex; + + if (index >= insertIndex) + { + removeIndex = index + aboveInsertIndexCount; + aboveInsertIndexCount++; + } + else + { + removeIndex = index; + insertIndex--; + } + + id object = [self.jobs[removeIndex] retain]; + [self.jobs removeObjectAtIndex:removeIndex]; + [self.jobs insertObject:object atIndex:insertIndex]; + [object release]; + + // We save all of the Queue data here + // and it also gets sent back to the queue controller + [self.jobs commit]; + [self reloadQueue]; } #pragma mark - +#pragma mark Queue Job Processing -//------------------------------------------------------------------------------------ -// Displays and brings the queue window to the front -//------------------------------------------------------------------------------------ -- (IBAction) showQueueWindow: (id)sender +/** + * Starts the queue + */ +- (void)encodeNextQueueItem { - [self showWindow:sender]; - [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"QueueWindowIsOpen"]; - [self startAnimatingCurrentWorkingEncodeInQueue]; + // Check to see if there are any more pending items in the queue + HBJob *nextJob = [self getNextPendingQueueItem]; + + // If we still have more pending items in our queue, lets go to the next one + if (nextJob) + { + self.currentJob = nextJob; + // now we mark the queue item as working so another instance can not come along and try to scan it while we are scanning + self.currentJob.state = HBJobStateWorking; + + // now we can go ahead and scan the new pending queue item + [self performScan:self.currentJob.fileURL titleIdx:self.currentJob.titleIdx]; + } + else + { + self.currentJob = nil; + + [HBUtilities writeToActivityLog:"Queue Done, there are no more pending encodes"]; + + // Since there are no more items to encode, go to queueCompletedAlerts + // for user specified alerts after queue completed + [self queueCompletedAlerts]; + } } -//------------------------------------------------------------------------------------ -// windowDidLoad -//------------------------------------------------------------------------------------ -- (void)windowDidLoad +- (void)encodeCompleted { - /* lets setup our queue list outline view for drag and drop here */ - [fOutlineView registerForDraggedTypes: @[DragDropSimplePboardType] ]; - [fOutlineView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:YES]; - [fOutlineView setVerticalMotionCanBeginDrag: YES]; + // Since we are done with this encode, tell output to stop writing to the + // individual encode log. + [self.outputPanel endEncodeLog]; - // Don't allow autoresizing of main column, else the "delete" column will get - // pushed out of view. - [fOutlineView setAutoresizesOutlineColumn: NO]; + // Check to see if the encode state has not been cancelled + // to determine if we should check for encode done notifications. + if (self.currentJob.state != HBJobStateCanceled) + { + // Both the Growl Alert and Sending to tagger can be done as encodes roll off the queue + if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"AlertWhenDone"] isEqualToString: @"Growl Notification"] || + [[[NSUserDefaults standardUserDefaults] stringForKey:@"AlertWhenDone"] isEqualToString: @"Alert Window And Growl"]) + { + // If Play System Alert has been selected in Preferences + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"AlertWhenDoneSound"] == YES) + { + NSBeep(); + } + [self showGrowlDoneNotification:self.currentJob.destURL]; + } + + // Mark the encode just finished as done + self.currentJob.state = HBJobStateCompleted; + + // Send to tagger + [self sendToExternalApp:self.currentJob.destURL]; + } + + self.currentJob = nil; + + // since we have successfully completed an encode, we go to the next + [self encodeNextQueueItem]; + + [self.window.toolbar validateVisibleItems]; + [self reloadQueue]; } -//------------------------------------------------------------------------------------ -// windowWillClose -//------------------------------------------------------------------------------------ -- (void)windowWillClose:(NSNotification *)aNotification +/** + * Here we actually tell hb_scan to perform the source scan, using the path to source and title number + */ +- (void)performScan:(NSURL *)scanURL titleIdx:(NSInteger)index { - [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"QueueWindowIsOpen"]; - [self stopAnimatingCurrentJobGroupInQueue]; + // Tell HB to output a new activity log file for this encode + [self.outputPanel startEncodeLog:self.currentJob.destURL]; + + // Only scan 10 previews before an encode - additional previews are + // only useful for autocrop and static previews, which are already taken care of at this point + [self.core scanURL:scanURL + titleIndex:index + previews:10 + minDuration:0 + progressHandler:^(HBState state, hb_state_t hb_state) { + NSMutableString *status = [NSMutableString stringWithFormat: + NSLocalizedString( @"Queue Scanning title %d of %d…", @"" ), + hb_state.param.scanning.title_cur, hb_state.param.scanning.title_count]; + if (hb_state.param.scanning.preview_cur) + { + [status appendFormat:@", preview %d…", hb_state.param.scanning.preview_cur]; + } + + self.progressTextField.stringValue = status; + [self.controller setQueueInfo:status progress:0 hidden:NO]; + } + completationHandler:^(BOOL success) { + if (success) + { + [self doEncodeQueueItem]; + } + else + { + [self.jobs beginTransaction]; + + self.currentJob.state = HBJobStateCanceled; + [self encodeCompleted]; + + [self.jobs commit]; + [self reloadQueue]; + } + + [self.window.toolbar validateVisibleItems]; + }]; } -#pragma mark Toolbar +/** + * This assumes that we have re-scanned and loaded up a new queue item to send to libhb + */ +- (void)doEncodeQueueItem +{ + // Reset the title in the job. + self.currentJob.title = self.core.titles[0]; + + // We should be all setup so let 'er rip + [self.core encodeJob:self.currentJob + progressHandler:^(HBState state, hb_state_t hb_state) { + NSMutableString *string = nil; + CGFloat progress = 0; + switch (state) + { + case HBStateSearching: + { + #define p hb_state.param.working + string = [NSMutableString stringWithFormat: + NSLocalizedString(@"Searching for start point… : %.2f %%", @""), + 100.0 * p.progress]; + + if (p.seconds > -1) + { + [string appendFormat:NSLocalizedString(@" (ETA %02dh%02dm%02ds)", @"" ), p.hours, p.minutes, p.seconds]; + } + #undef p + + break; + } + case HBStateWorking: + { + #define p hb_state.param.working + NSString *pass_desc = @""; + if (p.job_cur == 1 && p.job_count > 1) + { + if ([self.currentJob.subtitles.tracks.firstObject[keySubTrackIndex] intValue] == -1) + { + pass_desc = NSLocalizedString(@"(subtitle scan)", @""); + } + } + + if (pass_desc.length) + { + string = [NSMutableString stringWithFormat: + NSLocalizedString(@"Encoding: %@ \nPass %d %@ of %d, %.2f %%", @""), + self.currentJob.destURL.lastPathComponent, + p.job_cur, pass_desc, p.job_count, 100.0 * p.progress]; + } + else + { + string = [NSMutableString stringWithFormat: + NSLocalizedString(@"Encoding: %@ \nPass %d of %d, %.2f %%", @""), + self.currentJob.destURL.lastPathComponent, + p.job_cur, p.job_count, 100.0 * p.progress]; + } + + if (p.seconds > -1) + { + if (p.rate_cur > 0.0) + { + [string appendFormat: + NSLocalizedString(@" (%.2f fps, avg %.2f fps, ETA %02dh%02dm%02ds)", @""), + p.rate_cur, p.rate_avg, p.hours, p.minutes, p.seconds]; + } + else + { + [string appendFormat: + NSLocalizedString(@" (ETA %02dh%02dm%02ds)", @""), + p.hours, p.minutes, p.seconds]; + } + } + + progress = (p.progress + p.job_cur - 1) / p.job_count; + + // Update dock icon + if (self.dockIconProgress < 100.0 * progress) + { + // ETA format is [XX]X:XX:XX when ETA is greater than one hour + // [X]X:XX when ETA is greater than 0 (minutes or seconds) + // When these conditions doesn't applied (eg. when ETA is undefined) + // we show just a tilde (~) + + NSString *etaStr = @""; + if (p.hours > 0) + etaStr = [NSString stringWithFormat:@"%d:%02d:%02d", p.hours, p.minutes, p.seconds]; + else if (p.minutes > 0 || p.seconds > 0) + etaStr = [NSString stringWithFormat:@"%d:%02d", p.minutes, p.seconds]; + else + etaStr = @"~"; + + [self.dockTile updateDockIcon:progress withETA:etaStr]; + + self.dockIconProgress += dockTileUpdateFrequency; + } + #undef p + + break; + } + case HBStateMuxing: + { + string = [NSMutableString stringWithString:NSLocalizedString(@"Muxing…", @"")]; + + // Update dock icon + [self.dockTile updateDockIcon:1.0 withETA:@""]; + + break; + } + case HBStatePaused: + { + string = [NSMutableString stringWithString:NSLocalizedString(@"Paused", @"")]; + break; + } + default: + break; + } + + // Update text field + self.progressTextField.stringValue = string; + [self.controller setQueueInfo:string progress:progress * 100.0 hidden:NO]; + } + completationHandler:^(BOOL success) { + NSString *info = NSLocalizedString(@"Encode Finished.", @""); + + self.progressTextField.stringValue = info; + [self.controller setQueueInfo:info progress:100.0 hidden:YES]; + + // Restore dock icon + [self.dockTile updateDockIcon:-1.0 withETA:@""]; + self.dockIconProgress = 0; + + [self.jobs beginTransaction]; + + [self encodeCompleted]; + + [self.jobs commit]; + [self reloadQueue]; + }]; + + // We are done using the title, remove it from the job + self.currentJob.title = nil; +} + +/** + * Cancels and deletes the current job and starts processing the next in queue. + */ +- (void)doCancelCurrentJob +{ + [self.jobs beginTransaction]; + + self.currentJob.state = HBJobStateCanceled; + [self.core cancelEncode]; + + [self.jobs commit]; + [self reloadQueue]; +} -//------------------------------------------------------------------------------------ -// validateToolbarItem: -//------------------------------------------------------------------------------------ -- (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem +/** + * Cancels and deletes the current job and stops libhb from processing the remaining encodes. + */ +- (void)doCancelCurrentJobAndStop { - // Optional method: This message is sent to us since we are the target of some - // toolbar item actions. + [self.jobs beginTransaction]; - if (!self.queueCore) return NO; + self.currentJob.state = HBJobStateCanceled; - BOOL enable = NO; - HBState s = self.queueCore.state; + [self.core cancelEncode]; - if ([[toolbarItem itemIdentifier] isEqualToString:@"HBQueueStartCancelToolbarIdentifier"]) - { - if ((s == HBStatePaused) || (s == HBStateWorking) || (s == HBStateMuxing)) - { - enable = YES; - [toolbarItem setImage:[NSImage imageNamed: @"stopencode"]]; - [toolbarItem setLabel: @"Stop"]; - [toolbarItem setToolTip: @"Stop Encoding"]; - } + [self.jobs commit]; + [self reloadQueue]; +} - else if (fPendingCount > 0) - { - enable = YES; - [toolbarItem setImage:[NSImage imageNamed: @"encode"]]; - [toolbarItem setLabel: @"Start"]; - [toolbarItem setToolTip: @"Start Encoding"]; - } +#pragma mark - Encode Done Actions - else +#define SERVICE_NAME @"Encode Done" + +/** + * Register a test notification and make + * it enabled by default + */ +- (NSDictionary *)registrationDictionaryForGrowl +{ + return @{GROWL_NOTIFICATIONS_ALL: @[SERVICE_NAME], + GROWL_NOTIFICATIONS_DEFAULT: @[SERVICE_NAME]}; +} + +- (void)showGrowlDoneNotification:(NSURL *)fileURL +{ + // This end of encode action is called as each encode rolls off of the queue + // Setup the Growl stuff + NSString *growlMssg = [NSString stringWithFormat:@"your HandBrake encode %@ is done!", fileURL.lastPathComponent]; + [GrowlApplicationBridge notifyWithTitle:@"Put down that cocktail…" + description:growlMssg + notificationName:SERVICE_NAME + iconData:nil + priority:0 + isSticky:1 + clickContext:nil]; +} + +- (void)sendToExternalApp:(NSURL *)fileURL +{ + // This end of encode action is called as each encode rolls off of the queue + if([[NSUserDefaults standardUserDefaults] boolForKey: @"sendToMetaX"] == YES) + { + NSString *sendToApp = [[NSUserDefaults standardUserDefaults] objectForKey:@"SendCompletedEncodeToApp"]; + if (![sendToApp isEqualToString:@"None"]) { - enable = NO; - [toolbarItem setImage:[NSImage imageNamed: @"encode"]]; - [toolbarItem setLabel: @"Start"]; - [toolbarItem setToolTip: @"Start Encoding"]; + [HBUtilities writeToActivityLog: "trying to send encode to: %s", [sendToApp UTF8String]]; + NSAppleScript *myScript = [[NSAppleScript alloc] initWithSource: [NSString stringWithFormat: @"%@%@%@%@%@", @"tell application \"",sendToApp,@"\" to open (POSIX file \"", fileURL.path, @"\")"]]; + [myScript executeAndReturnError: nil]; + [myScript release]; } } +} - if ([[toolbarItem itemIdentifier] isEqualToString:@"HBQueuePauseResumeToolbarIdentifier"]) +- (void)queueCompletedAlerts +{ + // If Play System Alert has been selected in Preferences + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"AlertWhenDoneSound"] == YES) { - if (s == HBStatePaused) - { - enable = YES; - [toolbarItem setImage:[NSImage imageNamed: @"encode"]]; - [toolbarItem setLabel: @"Resume"]; - [toolbarItem setToolTip: @"Resume Encoding"]; - } + NSBeep(); + } - else if ((s == HBStateWorking) || (s == HBStateMuxing)) - { - enable = YES; - [toolbarItem setImage:[NSImage imageNamed: @"pauseencode"]]; - [toolbarItem setLabel: @"Pause"]; - [toolbarItem setToolTip: @"Pause Encoding"]; - } - else - { - enable = NO; - [toolbarItem setImage:[NSImage imageNamed: @"pauseencode"]]; - [toolbarItem setLabel: @"Pause"]; - [toolbarItem setToolTip: @"Pause Encoding"]; - } + // If Alert Window or Window and Growl has been selected + if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"AlertWhenDone"] isEqualToString: @"Alert Window"] || + [[[NSUserDefaults standardUserDefaults] stringForKey:@"AlertWhenDone"] isEqualToString: @"Alert Window And Growl"]) + { + // On Screen Notification + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:NSLocalizedString(@"Put down that cocktail…", @"")]; + [alert setInformativeText:NSLocalizedString(@"Your HandBrake queue is done!", @"")]; + [NSApp requestUserAttention:NSCriticalRequest]; + [alert runModal]; + [alert release]; } - return enable; + // If sleep has been selected + if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"AlertWhenDone"] isEqualToString: @"Put Computer To Sleep"]) + { + // Sleep + NSDictionary *errorDict; + NSAppleScript *scriptObject = [[NSAppleScript alloc] initWithSource: + @"tell application \"Finder\" to sleep"]; + [scriptObject executeAndReturnError: &errorDict]; + [scriptObject release]; + } + // If Shutdown has been selected + if( [[[NSUserDefaults standardUserDefaults] stringForKey:@"AlertWhenDone"] isEqualToString: @"Shut Down Computer"] ) + { + // Shut Down + NSDictionary *errorDict; + NSAppleScript *scriptObject = [[NSAppleScript alloc] initWithSource:@"tell application \"Finder\" to shut down"]; + [scriptObject executeAndReturnError: &errorDict]; + [scriptObject release]; + } } -#pragma mark - - - -#pragma mark Queue Item Controls +#pragma mark - Queue Item Controls - (void)HB_deleteSelectionFromTableView:(NSTableView *)tableView { [self removeSelectedQueueItem:tableView]; } -//------------------------------------------------------------------------------------ -// Delete encodes from the queue window and accompanying array -// Also handling first cancelling the encode if in fact its currently encoding. -//------------------------------------------------------------------------------------ -- (IBAction)removeSelectedQueueItem: (id)sender +/** + * Delete encodes from the queue window and accompanying array + * Also handling first cancelling the encode if in fact its currently encoding. + */ +- (IBAction)removeSelectedQueueItem:(id)sender { - NSIndexSet *targetedRow = [fOutlineView targetedRowIndexes]; + NSIndexSet *targetedRow = [self.outlineView targetedRowIndexes]; NSUInteger row = [targetedRow firstIndex]; if (row == NSNotFound) + { return; - /* if this is a currently encoding job, we need to be sure to alert the user, - * to let them decide to cancel it first, then if they do, we can come back and - * remove it */ - - if ([fJobGroups[row] state] == HBJobStateWorking) + } + + // if this is a currently encoding job, we need to be sure to alert the user, + // to let them decide to cancel it first, then if they do, we can come back and + // remove it + if (self.jobs[row] == self.currentJob) { - /* We pause the encode here so that it doesn't finish right after and then - * screw up the sync while the window is open - */ - [fHBController Pause:NULL]; - NSString *alertTitle = [NSString stringWithFormat:NSLocalizedString(@"Stop This Encode and Remove It ?", nil)]; + // We pause the encode here so that it doesn't finish right after and then + // screw up the sync while the window is open + [self togglePauseResume:nil]; + + NSString *alertTitle = [NSString stringWithFormat:NSLocalizedString(@"Stop This Encode and Remove It?", nil)]; + // Which window to attach the sheet to? - NSWindow *docWindow = nil; + NSWindow *targetWindow = nil; if ([sender respondsToSelector: @selector(window)]) { - docWindow = [sender window]; + targetWindow = [sender window]; } NSAlert *alert = [[NSAlert alloc] init]; @@ -371,56 +809,55 @@ [alert addButtonWithTitle:NSLocalizedString(@"Stop Encoding and Delete", nil)]; [alert setAlertStyle:NSCriticalAlertStyle]; - [alert beginSheetModalForWindow:docWindow + [alert beginSheetModalForWindow:targetWindow modalDelegate:self didEndSelector:@selector(didDimissCancelCurrentJob:returnCode:contextInfo:) - contextInfo:nil]; + contextInfo:self.jobs[row]]; [alert release]; } - else - { + else if ([self.jobs[row] state] != HBJobStateWorking) + { // since we are not a currently encoding item, we can just be removed - [fHBController removeQueueFileItem:row]; + [self removeQueueItemAtIndex:row]; } } -- (void) didDimissCancelCurrentJob: (NSWindow *)sheet returnCode: (int)returnCode contextInfo: (void *)contextInfo +- (void)didDimissCancelCurrentJob:(NSAlert *)alert + returnCode:(NSInteger)returnCode + contextInfo:(void *)contextInfo { - /* We resume encoding and perform the appropriate actions - * Note: Pause: is a toggle type method based on hb's current - * state, if it paused, it will resume encoding and vice versa. - * In this case, we are paused from the calling window, so calling - * [fHBController Pause:NULL]; Again will resume encoding - */ - [fHBController Pause:NULL]; + // We resume encoding and perform the appropriate actions + // Note: Pause: is a toggle type method based on hb's current + // state, if it paused, it will resume encoding and vice versa. + // In this case, we are paused from the calling window, so calling + // [self togglePauseResume:nil]; Again will resume encoding + [self togglePauseResume:nil]; + if (returnCode == NSAlertSecondButtonReturn) { - /* We need to save the currently encoding item number first */ - int encodingItemToRemove = fEncodingQueueItem; - /* Since we are encoding, we need to let fHBController Cancel this job - * upon which it will move to the next one if there is one - */ - [fHBController doCancelCurrentJob]; - /* Now, we can go ahead and remove the job we just cancelled since - * we have its item number from above - */ - [fHBController removeQueueFileItem:encodingItemToRemove]; + // We need to save the currently encoding item number first + NSInteger index = [self.jobs indexOfObject:self.currentJob]; + // Since we are encoding, we need to let fHBController Cancel this job + // upon which it will move to the next one if there is one + [self doCancelCurrentJob]; + // Now, we can go ahead and remove the job we just cancelled since + // we have its item number from above + [self removeQueueItemAtIndex:index]; } - } -//------------------------------------------------------------------------------------ -// Show the finished encode in the finder -//------------------------------------------------------------------------------------ +/** + * Show the finished encode in the finder + */ - (IBAction)revealSelectedQueueItem: (id)sender { - NSIndexSet *targetedRow = [fOutlineView targetedRowIndexes]; + NSIndexSet *targetedRow = [self.outlineView targetedRowIndexes]; NSInteger row = [targetedRow firstIndex]; if (row != NSNotFound) { while (row != NSNotFound) { - HBJob *queueItemToOpen = [fOutlineView itemAtRow:row]; + HBJob *queueItemToOpen = [self.outlineView itemAtRow:row]; [[NSWorkspace sharedWorkspace] selectFile:queueItemToOpen.destURL.path inFileViewerRootedAtPath:nil]; row = [targetedRow indexGreaterThanIndex: row]; @@ -428,65 +865,177 @@ } } -//------------------------------------------------------------------------------------ -// Starts or cancels the processing of jobs depending on the current state -//------------------------------------------------------------------------------------ -- (IBAction)toggleStartCancel: (id)sender +- (void)remindUserOfSleepOrShutdown { - if (!self.queueCore) return; + if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"AlertWhenDone"] isEqualToString:@"Put Computer To Sleep"]) + { + // Warn that computer will sleep after encoding + NSBeep(); + [NSApp requestUserAttention:NSCriticalRequest]; - HBState s = self.queueCore.state; - if ((s == HBStatePaused) || (s == HBStateWorking) || (s == HBStateMuxing)) + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:NSLocalizedString(@"The computer will sleep after encoding is done.", @"")]; + [alert setInformativeText:NSLocalizedString(@"You have selected to sleep the computer after encoding. To turn off sleeping, go to the HandBrake preferences.", @"")]; + [alert addButtonWithTitle:NSLocalizedString(@"OK", @"")]; + [alert addButtonWithTitle:NSLocalizedString(@"Preferences…",@"")]; + + NSInteger response = [alert runModal]; + if (response == NSAlertSecondButtonReturn) + { + [self.controller showPreferencesWindow:nil]; + } + [alert release]; + } + else if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"AlertWhenDone"] isEqualToString:@"Shut Down Computer"]) { - [fHBController Cancel: self]; + // Warn that computer will shut down after encoding + NSBeep(); + [NSApp requestUserAttention:NSCriticalRequest]; + + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:NSLocalizedString(@"The computer will shut down after encoding is done.", @"")]; + [alert setInformativeText:NSLocalizedString(@"You have selected to shut down the computer after encoding. To turn off shut down, go to the HandBrake preferences.", @"")]; + [alert addButtonWithTitle:NSLocalizedString(@"OK", @"")]; + [alert addButtonWithTitle:NSLocalizedString(@"Preferences…", @"")]; + + NSInteger response = [alert runModal]; + if (response == NSAlertSecondButtonReturn) + { + [self.controller showPreferencesWindow:nil]; + } + [alert release]; } - else if (fPendingCount > 0) +} + +/** + * Rip: puts up an alert before ultimately calling doRip + */ +- (IBAction)rip:(id)sender +{ + // Rip or Cancel ? + if (self.core.state == HBStateWorking || self.core.state == HBStatePaused) { - [fHBController Rip: NULL]; + [self cancel:sender]; } + // If there are pending jobs in the queue, then this is a rip the queue + else if (self.pendingItemsCount > 0) + { + // We check to see if we need to warn the user that the computer will go to sleep + // or shut down when encoding is finished + [self remindUserOfSleepOrShutdown]; + + [self.jobs beginTransaction]; + + [self encodeNextQueueItem]; + + [self.jobs commit]; + [self reloadQueue]; + } +} + +/** + * Displays an alert asking user if the want to cancel encoding of current job. + * Cancel: returns immediately after posting the alert. Later, when the user + * acknowledges the alert, doCancelCurrentJob is called. + */ +- (IBAction)cancel:(id)sender +{ + [self.core pause]; + + // Which window to attach the sheet to? + NSWindow *window; + if ([sender respondsToSelector:@selector(window)]) + { + window = [sender window]; + } + else + { + window = self.window; + } + + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:NSLocalizedString(@"You are currently encoding. What would you like to do ?", nil)]; + [alert setInformativeText:NSLocalizedString(@"Your encode will be cancelled if you don't continue encoding.", nil)]; + [alert addButtonWithTitle:NSLocalizedString(@"Continue Encoding", nil)]; + [alert addButtonWithTitle:NSLocalizedString(@"Cancel Current and Stop", nil)]; + [alert addButtonWithTitle:NSLocalizedString(@"Cancel Current and Continue", nil)]; + [alert setAlertStyle:NSCriticalAlertStyle]; + + [alert beginSheetModalForWindow:window + modalDelegate:self + didEndSelector:@selector(didDimissCancel:returnCode:contextInfo:) + contextInfo:nil]; + [alert release]; } -//------------------------------------------------------------------------------------ -// Toggles the pause/resume state of libhb -//------------------------------------------------------------------------------------ -- (IBAction)togglePauseResume: (id)sender +- (void)didDimissCancel:(NSAlert *)alert + returnCode:(NSInteger)returnCode + contextInfo:(void *)contextInfo { - if (!self.queueCore) return; + [self.core resume]; - HBState s = self.queueCore.state; + if (returnCode == NSAlertSecondButtonReturn) + { + [self doCancelCurrentJobAndStop]; // <- this also stops libhb + } + else if (returnCode == NSAlertThirdButtonReturn) + { + [self doCancelCurrentJob]; + } +} + +/** + * Starts or cancels the processing of jobs depending on the current state + */ +- (IBAction)toggleStartCancel:(id)sender +{ + HBState s = self.core.state; + if ((s == HBStatePaused) || (s == HBStateWorking) || (s == HBStateMuxing)) + { + [self cancel:self]; + } + else if (self.pendingItemsCount > 0) + { + [self rip:self]; + } +} + +/** + * Toggles the pause/resume state of libhb + */ +- (IBAction)togglePauseResume:(id)sender +{ + HBState s = self.core.state; if (s == HBStatePaused) { - [self.queueCore resume]; - [self startAnimatingCurrentWorkingEncodeInQueue]; + [self.core resume]; } - else if ((s == HBStateWorking) || (s == HBStateMuxing)) + else if (s == HBStateWorking || s == HBStateMuxing) { - [self.queueCore pause]; - [self stopAnimatingCurrentJobGroupInQueue]; + [self.core pause]; } } -//------------------------------------------------------------------------------------ -// Send the selected queue item back to the main window for rescan and possible edit. -//------------------------------------------------------------------------------------ -- (IBAction)editSelectedQueueItem: (id)sender +/** + * Send the selected queue item back to the main window for rescan and possible edit. + */ +- (IBAction)editSelectedQueueItem:(id)sender { - NSInteger row = [fOutlineView clickedRow]; + NSInteger row = [self.outlineView clickedRow]; if (row == NSNotFound) { return; } - /* if this is a currently encoding job, we need to be sure to alert the user, - * to let them decide to cancel it first, then if they do, we can come back and - * remove it */ - - HBJob *job = fJobGroups[row]; + // if this is a currently encoding job, we need to be sure to alert the user, + // to let them decide to cancel it first, then if they do, we can come back and + // remove it + HBJob *job = self.jobs[row]; if (job.state == HBJobStateWorking) { // We pause the encode here so that it doesn't finish right after and then // screw up the sync while the window is open - [fHBController Pause:NULL]; + [self togglePauseResume:nil]; NSString *alertTitle = [NSString stringWithFormat:NSLocalizedString(@"Stop This Encode and Remove It ?", nil)]; // Which window to attach the sheet to? NSWindow *docWindow = nil; @@ -505,115 +1054,29 @@ [alert beginSheetModalForWindow:docWindow modalDelegate:self didEndSelector:@selector(didDimissCancelCurrentJob:returnCode:contextInfo:) - contextInfo:nil]; + contextInfo:job]; [alert release]; } else { - /* since we are not a currently encoding item, we can just be cancelled */ - [fHBController rescanQueueItemToMainWindow:row]; - } -} - - -#pragma mark - -#pragma mark Animate Encoding Item - -//------------------------------------------------------------------------------------ -// Starts animating the job icon of the currently processing job in the queue outline -// view. -//------------------------------------------------------------------------------------ -- (void) startAnimatingCurrentWorkingEncodeInQueue -{ - if (!fAnimationTimer) - fAnimationTimer = [[NSTimer scheduledTimerWithTimeInterval:1.0/12.0 // 1/12 because there are 6 images in the animation cycle - target:self - selector:@selector(animateWorkingEncodeInQueue:) - userInfo:nil - repeats:YES] retain]; -} - -//------------------------------------------------------------------------------------ -// If a job is currently processing, its job icon in the queue outline view is -// animated to its next state. -//------------------------------------------------------------------------------------ -- (void) animateWorkingEncodeInQueue:(NSTimer*)theTimer -{ - if (fWorkingCount > 0) - { - fAnimationIndex++; - fAnimationIndex %= 6; // there are 6 animation images; see outlineView:objectValueForTableColumn:byItem: below. - [self animateWorkingEncodeIconInQueue]; - } -} + // since we are not a currently encoding item, we can just be cancelled + HBJob *item = [[[self.jobs[row] representedObject] copy] autorelease]; + [self.controller rescanJobToMainWindow:item]; -/* We need to make sure we denote only working encodes even for multiple instances */ -- (void) animateWorkingEncodeIconInQueue -{ - NSInteger row = fEncodingQueueItem; /// need to set to fEncodingQueueItem - NSInteger col = [fOutlineView columnWithIdentifier: @"icon"]; - if (row != -1 && col != -1) - { - NSRect frame = [fOutlineView frameOfCellAtColumn:col row:row]; - [fOutlineView setNeedsDisplayInRect: frame]; + // Now that source is loaded and settings applied, delete the queue item from the queue + [self removeQueueItemAtIndex:row]; } } -//------------------------------------------------------------------------------------ -// Stops animating the job icon of the currently processing job in the queue outline -// view. -//------------------------------------------------------------------------------------ -- (void) stopAnimatingCurrentJobGroupInQueue -{ - if (fAnimationTimer && [fAnimationTimer isValid]) - { - [fAnimationTimer invalidate]; - [fAnimationTimer release]; - fAnimationTimer = nil; - } -} - - -#pragma mark - - -- (void)moveObjectsInArray:(NSMutableArray *)array fromIndexes:(NSIndexSet *)indexSet toIndex:(NSUInteger)insertIndex -{ - NSUInteger index = [indexSet lastIndex]; - NSUInteger aboveInsertIndexCount = 0; - - while (index != NSNotFound) - { - NSUInteger removeIndex; - - if (index >= insertIndex) - { - removeIndex = index + aboveInsertIndexCount; - aboveInsertIndexCount++; - } - else - { - removeIndex = index; - insertIndex--; - } - - id object = [array[removeIndex] retain]; - [array removeObjectAtIndex:removeIndex]; - [array insertObject:object atIndex:insertIndex]; - [object release]; - - index = [indexSet indexLessThanIndex:index]; - } -} - - #pragma mark - #pragma mark NSOutlineView delegate - - (id)outlineView:(NSOutlineView *)fOutlineView child:(NSInteger)index ofItem:(id)item { if (item == nil) - return fJobGroups[index]; + { + return self.jobs[index]; + } // We are only one level deep, so we can't be asked about children NSAssert (NO, @"HBQueueController outlineView:child:ofItem: can't handle nested items."); @@ -639,23 +1102,27 @@ // Our outline view has no levels, so number of children will be zero for all // top-level items. if (item == nil) - return [fJobGroups count]; + { + return [self.jobs count]; + } else + { return 0; + } } - (void)outlineViewItemDidCollapse:(NSNotification *)notification { id item = [notification userInfo][@"NSObject"]; - NSInteger row = [fOutlineView rowForItem:item]; - [fOutlineView noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(row,1)]]; + NSInteger row = [self.outlineView rowForItem:item]; + [self.outlineView noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(row,1)]]; } - (void)outlineViewItemDidExpand:(NSNotification *)notification { id item = [notification userInfo][@"NSObject"]; - NSInteger row = [fOutlineView rowForItem:item]; - [fOutlineView noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(row,1)]]; + NSInteger row = [self.outlineView rowForItem:item]; + [self.outlineView noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(row,1)]]; } - (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item @@ -685,403 +1152,24 @@ } } -- (void)initStyles -{ - // Attributes - ps = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] retain]; - [ps setHeadIndent: 40.0]; - [ps setParagraphSpacing: 1.0]; - [ps setTabStops:@[]]; // clear all tabs - [ps addTabStop: [[[NSTextTab alloc] initWithType: NSLeftTabStopType location: 20.0] autorelease]]; - - detailAttr = [@{NSFontAttributeName: [NSFont systemFontOfSize:10.0], - NSParagraphStyleAttributeName: ps} retain]; - - detailBoldAttr = [@{NSFontAttributeName: [NSFont boldSystemFontOfSize:10.0], - NSParagraphStyleAttributeName: ps} retain]; - - titleAttr = [@{NSFontAttributeName: [NSFont systemFontOfSize:[NSFont systemFontSize]], - NSParagraphStyleAttributeName: ps} retain]; - - shortHeightAttr = [@{NSFontAttributeName: [NSFont systemFontOfSize:2.0]} retain]; -} - - (id)outlineView:(NSOutlineView *)fOutlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { - if ([[tableColumn identifier] isEqualToString:@"desc"]) + if ([tableColumn.identifier isEqualToString:@"desc"]) { HBJob *job = item; - if ([descriptions objectForKey:@(job.hash)]) - { - return [descriptions objectForKey:@(job.hash)]; - } - - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - /* Below should be put into a separate method but I am way too f'ing lazy right now */ - NSMutableAttributedString *finalString = [[NSMutableAttributedString alloc] initWithString: @""]; - - /* First line, we should strip the destination path and just show the file name and add the title num and chapters (if any) */ - NSString *summaryInfo; - - NSString *titleString = [NSString stringWithFormat:@"Title %d", job.titleIdx]; - - NSString *startStopString = @""; - if (job.range.type == HBRangeTypeChapters) - { - // Start Stop is chapters - startStopString = (job.range.chapterStart == job.range.chapterStop) ? - [NSString stringWithFormat:@"Chapter %d", job.range.chapterStart] : - [NSString stringWithFormat:@"Chapters %d through %d", job.range.chapterStart, job.range.chapterStop]; - } - else if (job.range.type == HBRangeTypeSeconds) + if (self.descriptions[@(job.hash)]) { - // Start Stop is seconds - startStopString = [NSString stringWithFormat:@"Seconds %d through %d", job.range.secondsStart, job.range.secondsStop]; - } - else if (job.range.type == HBRangeTypeFrames) - { - // Start Stop is Frames - startStopString = [NSString stringWithFormat:@"Frames %d through %d", job.range.frameStart, job.range.frameStop]; - } - NSString *passesString = @""; - // check to see if our first subtitle track is Foreign Language Search, in which case there is an in depth scan - if (job.subtitles.tracks.count && [job.subtitles.tracks[0][@"keySubTrackIndex"] intValue] == -1) - { - passesString = [passesString stringByAppendingString:@"1 Foreign Language Search Pass - "]; - } - if (job.video.qualityType == 1 || job.video.twoPass == NO) - { - passesString = [passesString stringByAppendingString:@"1 Video Pass"]; - } - else - { - if (job.video.turboTwoPass == YES) - { - passesString = [passesString stringByAppendingString:@"2 Video Passes First Turbo"]; - } - else - { - passesString = [passesString stringByAppendingString:@"2 Video Passes"]; - } + return self.descriptions[@(job.hash)]; } - [finalString appendString:[NSString stringWithFormat:@"%@", job.fileURL.path.lastPathComponent] withAttributes:titleAttr]; + NSAttributedString *finalString = job.attributedDescription; + self.descriptions[@(job.hash)] = finalString;; - /* lets add the output file name to the title string here */ - NSString *outputFilenameString = job.destURL.lastPathComponent; - - summaryInfo = [NSString stringWithFormat: @" (%@, %@, %@) -> %@", titleString, startStopString, passesString, outputFilenameString]; - - [finalString appendString:[NSString stringWithFormat:@"%@\n", summaryInfo] withAttributes:detailAttr]; - - // Insert a short-in-height line to put some white space after the title - [finalString appendString:@"\n" withAttributes:shortHeightAttr]; - // End of Title Stuff - - // Second Line (Preset Name) - [finalString appendString: @"Preset: " withAttributes:detailBoldAttr]; - [finalString appendString:[NSString stringWithFormat:@"%@\n", job.presetName] withAttributes:detailAttr]; - - // Third Line (Format Summary) - NSString *audioCodecSummary = @""; // This seems to be set by the last track we have available... - // Lets also get our audio track detail since we are going through the logic for use later - - NSMutableArray *audioDetails = [NSMutableArray array]; - BOOL autoPassthruPresent = NO; - - for (HBAudioTrack *audioTrack in job.audio.tracks) - { - if (audioTrack.enabled) - { - audioCodecSummary = [NSString stringWithFormat: @"%@", audioTrack.codec[keyAudioCodecName]]; - NSNumber *drc = audioTrack.drc; - NSNumber *gain = audioTrack.gain; - NSString *detailString = [NSString stringWithFormat: @"%@ Encoder: %@ Mixdown: %@ SampleRate: %@(khz) Bitrate: %@(kbps), DRC: %@, Gain: %@", - audioTrack.track[keyAudioTrackName], - audioTrack.codec[keyAudioCodecName], - audioTrack.mixdown[keyAudioMixdownName], - audioTrack.sampleRate[keyAudioSampleRateName], - audioTrack.bitRate[keyAudioBitrateName], - (0.0 < [drc floatValue]) ? (NSObject *)drc : (NSObject *)@"Off", - (0.0 != [gain floatValue]) ? (NSObject *)gain : (NSObject *)@"Off" - ]; - [audioDetails addObject: detailString]; - // check if we have an Auto Passthru output track - if ([audioTrack.codec[keyAudioCodecName] isEqualToString: @"Auto Passthru"]) - { - autoPassthruPresent = YES; - } - } - } - - NSString *jobFormatInfo; - if (job.chaptersEnabled) - jobFormatInfo = [NSString stringWithFormat:@"%@ Container, %@ Video %@ Audio, Chapter Markers\n", - @(hb_container_get_name(job.container)), @(hb_video_encoder_get_name(job.video.encoder)), audioCodecSummary]; - else - jobFormatInfo = [NSString stringWithFormat:@"%@ Container, %@ Video %@ Audio\n", - @(hb_container_get_name(job.container)), @(hb_video_encoder_get_name(job.video.encoder)), audioCodecSummary]; - - [finalString appendString: @"Format: " withAttributes:detailBoldAttr]; - [finalString appendString: jobFormatInfo withAttributes:detailAttr]; - - // Optional String for muxer options - NSMutableString *containerOptions = [NSMutableString stringWithString:@""]; - if ((job.container & HB_MUX_MASK_MP4) && job.mp4HttpOptimize) - { - [containerOptions appendString:@" - Web optimized"]; - } - if ((job.container & HB_MUX_MASK_MP4) && job.mp4iPodCompatible) - { - [containerOptions appendString:@" - iPod 5G support"]; - } - if ([containerOptions hasPrefix:@" - "]) - { - [containerOptions deleteCharactersInRange:NSMakeRange(0, 3)]; - } - if (containerOptions.length) - { - [finalString appendString:@"Container Options: " withAttributes:detailBoldAttr]; - [finalString appendString:containerOptions withAttributes:detailAttr]; - [finalString appendString:@"\n" withAttributes:detailAttr]; - } - - // Fourth Line (Destination Path) - [finalString appendString: @"Destination: " withAttributes:detailBoldAttr]; - [finalString appendString: job.destURL.path withAttributes:detailAttr]; - [finalString appendString:@"\n" withAttributes:detailAttr]; - - - // Fifth Line Picture Details - NSString *pictureInfo = [NSString stringWithFormat:@"%@", job.picture.summary]; - if (job.picture.keepDisplayAspect) - { - pictureInfo = [pictureInfo stringByAppendingString:@" Keep Aspect Ratio"]; - } - [finalString appendString:@"Picture: " withAttributes:detailBoldAttr]; - [finalString appendString:pictureInfo withAttributes:detailAttr]; - [finalString appendString:@"\n" withAttributes:detailAttr]; - - /* Optional String for Picture Filters */ - if (job.filters.summary.length) - { - NSString *pictureFilters = [NSString stringWithFormat:@"%@", job.filters.summary]; - [finalString appendString:@"Filters: " withAttributes:detailBoldAttr]; - [finalString appendString:pictureFilters withAttributes:detailAttr]; - [finalString appendString:@"\n" withAttributes:detailAttr]; - } - - // Sixth Line Video Details - NSString * videoInfo = [NSString stringWithFormat:@"Encoder: %@", @(hb_video_encoder_get_name(job.video.encoder))]; - - // for framerate look to see if we are using vfr detelecine - if (job.video.frameRate == 0) - { - if (job.video.frameRateMode == 0) - { - // we are using same as source with vfr detelecine - videoInfo = [NSString stringWithFormat:@"%@ Framerate: Same as source (Variable Frame Rate)", videoInfo]; - } - else - { - // we are using a variable framerate without dropping frames - videoInfo = [NSString stringWithFormat:@"%@ Framerate: Same as source (Constant Frame Rate)", videoInfo]; - } - } - else - { - // we have a specified, constant framerate - if (job.video.frameRateMode == 0) - { - videoInfo = [NSString stringWithFormat:@"%@ Framerate: %@ (Peak Frame Rate)", videoInfo, @(hb_video_framerate_get_name(job.video.frameRate))]; - } - else - { - videoInfo = [NSString stringWithFormat:@"%@ Framerate: %@ (Constant Frame Rate)", videoInfo, @(hb_video_framerate_get_name(job.video.frameRate))]; - } - } - - - if (job.video.qualityType == 0) // ABR - { - videoInfo = [NSString stringWithFormat:@"%@ Bitrate: %d(kbps)", videoInfo, job.video.avgBitrate]; - } - else // CRF - { - videoInfo = [NSString stringWithFormat:@"%@ Constant Quality: %.2f", videoInfo ,job.video.quality]; - } - - [finalString appendString: @"Video: " withAttributes:detailBoldAttr]; - [finalString appendString: videoInfo withAttributes:detailAttr]; - [finalString appendString:@"\n" withAttributes:detailAttr]; - - - if (job.video.encoder == HB_VCODEC_X264 || job.video.encoder == HB_VCODEC_X265) - { - // we are using x264/x265 - NSString *encoderPresetInfo = @""; - if (job.video.advancedOptions) - { - // we are using the old advanced panel - if (job.video.videoOptionExtra.length) - { - encoderPresetInfo = [encoderPresetInfo stringByAppendingString:job.video.videoOptionExtra]; - } - else - { - encoderPresetInfo = [encoderPresetInfo stringByAppendingString:@"default settings"]; - } - } - else - { - // we are using the x264 system - encoderPresetInfo = [encoderPresetInfo stringByAppendingString: [NSString stringWithFormat:@"Preset: %@", job.video.preset]]; - if (job.video.tune.length) - { - encoderPresetInfo = [encoderPresetInfo stringByAppendingString: [NSString stringWithFormat:@" - Tune: %@", job.video.tune]]; - } - if (job.video.videoOptionExtra.length) - { - encoderPresetInfo = [encoderPresetInfo stringByAppendingString: [NSString stringWithFormat:@" - Options: %@", job.video.videoOptionExtra]]; - } - if (job.video.profile.length) - { - encoderPresetInfo = [encoderPresetInfo stringByAppendingString: [NSString stringWithFormat:@" - Profile: %@", job.video.profile]]; - } - if (job.video.level.length) - { - encoderPresetInfo = [encoderPresetInfo stringByAppendingString: [NSString stringWithFormat:@" - Level: %@", job.video.level]]; - } - } - [finalString appendString: @"Encoder Options: " withAttributes:detailBoldAttr]; - [finalString appendString: encoderPresetInfo withAttributes:detailAttr]; - [finalString appendString:@"\n" withAttributes:detailAttr]; - } - else - { - // we are using libavcodec - NSString *lavcInfo = @""; - if (job.video.videoOptionExtra.length) - { - lavcInfo = [lavcInfo stringByAppendingString:job.video.videoOptionExtra]; - } - else - { - lavcInfo = [lavcInfo stringByAppendingString: @"default settings"]; - } - [finalString appendString: @"Encoder Options: " withAttributes:detailBoldAttr]; - [finalString appendString: lavcInfo withAttributes:detailAttr]; - [finalString appendString:@"\n" withAttributes:detailAttr]; - } - - - // Seventh Line Audio Details - int audioDetailCount = 0; - for (NSString *anAudioDetail in audioDetails) { - audioDetailCount++; - if (anAudioDetail.length) { - [finalString appendString: [NSString stringWithFormat: @"Audio Track %d ", audioDetailCount] withAttributes: detailBoldAttr]; - [finalString appendString: anAudioDetail withAttributes: detailAttr]; - [finalString appendString: @"\n" withAttributes: detailAttr]; - } - } - - // Eigth Line Auto Passthru Details - // only print Auto Passthru settings if we have an Auro Passthru output track - if (autoPassthruPresent == YES) - { - NSString *autoPassthruFallback = @"", *autoPassthruCodecs = @""; - HBAudioDefaults *audioDefaults = job.audio.defaults; - autoPassthruFallback = [autoPassthruFallback stringByAppendingString:@(hb_audio_encoder_get_name(audioDefaults.encoderFallback))]; - if (audioDefaults.allowAACPassthru) - { - autoPassthruCodecs = [autoPassthruCodecs stringByAppendingString:@"AAC"]; - } - if (audioDefaults.allowAC3Passthru) - { - if (autoPassthruCodecs.length) - { - autoPassthruCodecs = [autoPassthruCodecs stringByAppendingString:@", "]; - } - autoPassthruCodecs = [autoPassthruCodecs stringByAppendingString:@"AC3"]; - } - if (audioDefaults.allowDTSHDPassthru) - { - if (autoPassthruCodecs.length) - { - autoPassthruCodecs = [autoPassthruCodecs stringByAppendingString:@", "]; - } - autoPassthruCodecs = [autoPassthruCodecs stringByAppendingString:@"DTS-HD"]; - } - if (audioDefaults.allowDTSPassthru) - { - if (autoPassthruCodecs.length) - { - autoPassthruCodecs = [autoPassthruCodecs stringByAppendingString:@", "]; - } - autoPassthruCodecs = [autoPassthruCodecs stringByAppendingString:@"DTS"]; - } - if (audioDefaults.allowMP3Passthru) - { - if (autoPassthruCodecs.length) - { - autoPassthruCodecs = [autoPassthruCodecs stringByAppendingString:@", "]; - } - autoPassthruCodecs = [autoPassthruCodecs stringByAppendingString:@"MP3"]; - } - [finalString appendString: @"Auto Passthru Codecs: " withAttributes: detailBoldAttr]; - if (autoPassthruCodecs.length) - { - [finalString appendString: autoPassthruCodecs withAttributes: detailAttr]; - } - else - { - [finalString appendString: @"None" withAttributes: detailAttr]; - } - [finalString appendString: @"\n" withAttributes: detailAttr]; - [finalString appendString: @"Auto Passthru Fallback: " withAttributes: detailBoldAttr]; - [finalString appendString: autoPassthruFallback withAttributes: detailAttr]; - [finalString appendString: @"\n" withAttributes: detailAttr]; - } - - // Ninth Line Subtitle Details - int i = 0; - for (NSDictionary *track in job.subtitles.tracks) - { - // Ignore the none track. - if (i == job.subtitles.tracks.count - 1) - { - continue; - } - - /* remember that index 0 of Subtitles can contain "Foreign Audio Search*/ - [finalString appendString: @"Subtitle: " withAttributes:detailBoldAttr]; - [finalString appendString: track[@"keySubTrackName"] withAttributes:detailAttr]; - if ([track[@"keySubTrackForced"] intValue] == 1) - { - [finalString appendString: @" - Forced Only" withAttributes:detailAttr]; - } - if ([track[@"keySubTrackBurned"] intValue] == 1) - { - [finalString appendString: @" - Burned In" withAttributes:detailAttr]; - } - if ([track[@"keySubTrackDefault"] intValue] == 1) - { - [finalString appendString: @" - Default" withAttributes:detailAttr]; - } - [finalString appendString:@"\n" withAttributes:detailAttr]; - i++; - } - - [pool release]; - - [descriptions setObject:finalString forKey:@(job.hash)]; - - return [finalString autorelease]; + return finalString; } - else if ([[tableColumn identifier] isEqualToString:@"icon"]) + else if ([tableColumn.identifier isEqualToString:@"icon"]) { HBJob *job = item; if (job.state == HBJobStateCompleted) @@ -1090,7 +1178,7 @@ } else if (job.state == HBJobStateWorking) { - return [NSImage imageNamed: [NSString stringWithFormat: @"EncodeWorking%d", fAnimationIndex]]; + return [NSImage imageNamed:@"EncodeWorking0"]; } else if (job.state == HBJobStateCanceled) { @@ -1106,10 +1194,13 @@ return @""; } } -/* This method inserts the proper action icons into the far right of the queue window */ + +/** + * This method inserts the proper action icons into the far right of the queue window + */ - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item { - if ([[tableColumn identifier] isEqualToString:@"desc"]) + if ([tableColumn.identifier isEqualToString:@"desc"]) { // nb: The "desc" column is currently an HBImageAndTextCell. However, we are longer // using the image portion of the cell so we could switch back to a regular NSTextFieldCell. @@ -1117,7 +1208,7 @@ // Set the image here since the value returned from outlineView:objectValueForTableColumn: didn't specify the image part [cell setImage:nil]; } - else if ([[tableColumn identifier] isEqualToString:@"action"]) + else if ([tableColumn.identifier isEqualToString:@"action"]) { [cell setEnabled: YES]; BOOL highlighted = [outlineView isRowSelected:[outlineView rowForItem: item]] && [[outlineView window] isKeyWindow] && ([[outlineView window] firstResponder] == outlineView); @@ -1152,43 +1243,40 @@ { // By default, the disclosure image gets centered vertically in the cell. We want // always at the top. - if ([outlineView isItemExpanded: item]) + if ([outlineView isItemExpanded:item]) + { [cell setImagePosition: NSImageAbove]; + } else + { [cell setImagePosition: NSImageOnly]; + } } #pragma mark - #pragma mark NSOutlineView delegate (dragging related) -//------------------------------------------------------------------------------------ -// NSTableView delegate -//------------------------------------------------------------------------------------ - - - (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard { // Dragging is only allowed of the pending items. - if ([items[0] state] != HBJobStateReady) // 2 is pending + if ([items[0] state] != HBJobStateReady) { return NO; } - + // Don't retain since this is just holding temporaral drag information, and it is //only used during a drag! We could put this in the pboard actually. - fDraggedNodes = items; - + self.dragNodesArray = items; + // Provide data for our custom type, and simple NSStrings. [pboard declareTypes:@[DragDropSimplePboardType] owner:self]; - + // the actual data doesn't matter since DragDropSimplePboardType drags aren't recognized by anyone but us!. [pboard setData:[NSData data] forType:DragDropSimplePboardType]; - + return YES; } - -/* This method is used to validate the drops. */ - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index { // Don't allow dropping ONTO an item since they can't really contain any children. @@ -1201,20 +1289,19 @@ // Don't allow dropping INTO an item since they can't really contain any children. if (item != nil) { - index = [fOutlineView rowForItem: item] + 1; + index = [self.outlineView rowForItem: item] + 1; item = nil; } - - // NOTE: Should we allow dropping a pending job *above* the - // finished or already encoded jobs ? + // We do not let the user drop a pending job before or *above* // already finished or currently encoding jobs. - if (index <= fEncodingQueueItem) + NSInteger encodingIndex = [self.jobs indexOfObject:self.currentJob]; + if (index <= encodingIndex) { return NSDragOperationNone; - index = MAX (index, fEncodingQueueItem); + index = MAX (index, encodingIndex); } - + [outlineView setDropItem:item dropChildIndex:index]; return NSDragOperationGeneric; } @@ -1223,14 +1310,17 @@ { NSMutableIndexSet *moveItems = [NSMutableIndexSet indexSet]; - for (id obj in fDraggedNodes) - [moveItems addIndex:[fJobGroups indexOfObject:obj]]; + for (id obj in self.dragNodesArray) + { + [moveItems addIndex:[self.jobs indexOfObject:obj]]; + } // Successful drop, we use moveObjectsInQueueArray:... in fHBController // to properly rearrange the queue array, save it to plist and then send it back here. // since Controller.mm is handling all queue array manipulation. - // We *could do this here, but I think we are better served keeping that code together. - [fHBController moveObjectsInQueueArray:fJobGroups fromIndexes:moveItems toIndex: index]; + // We could do this here, but I think we are better served keeping that code together. + [self moveObjectsInQueueArray:self.jobs fromIndexes:moveItems toIndex: index]; + return YES; } |