/* $Id: Controller.mm,v 1.79 2005/11/04 19:41:32 titer Exp $ This file is part of the HandBrake source code. Homepage: . It may be used under the terms of the GNU General Public License. */ #import "Controller.h" #import "HBQueueController.h" #import "HBOutputPanelController.h" #import "HBPreferencesController.h" #import "HBPresetsManager.h" #import "HBPreset.h" #import "HBDockTile.h" #import "HBUtilities.h" #import "HBVideoController.h" #import "HBAudioController.h" #import "HBSubtitlesController.h" #import "HBAdvancedController.h" #import "HBChapterTitlesController.h" #import "HBPictureController.h" #import "HBPreviewController.h" #import "HBPresetsViewController.h" #import "HBAddPresetController.h" #import "HBPicture+UIAdditions.h" #import "HBFilters+UIAdditions.h" #import "HBCore.h" #import "HBJob.h" // DockTile update freqency in total percent increment #define dockTileUpdateFrequency 0.1f @interface HBController () @property (nonatomic, copy) NSString *browsedSourceDisplayName; // The current job. @property (nonatomic, retain) HBJob *job; // The job to be applied from the queue. @property (nonatomic, retain) HBJob *jobFromQueue; // The current selected preset. @property (nonatomic, retain) HBPreset *selectedPreset; @property (nonatomic) BOOL customPreset; /** * The HBCore used for scanning. */ @property (nonatomic, retain) HBCore *core; /** * The HBCore used for encoding. */ @property (nonatomic, retain) HBCore *queueCore; @end /******************************* * HBController implementation * *******************************/ @implementation HBController - (instancetype)init { self = [super init]; if (self) { // Register the defaults preferences [HBPreferencesController registerUserDefaults]; /* Check for and create the App Support Preview directory if necessary */ NSString *previewDirectory = [[HBUtilities appSupportPath] stringByAppendingPathComponent:@"Previews"]; if (![[NSFileManager defaultManager] fileExistsAtPath:previewDirectory]) { [[NSFileManager defaultManager] createDirectoryAtPath:previewDirectory withIntermediateDirectories:YES attributes:nil error:NULL]; } // Inits the controllers outputPanel = [[HBOutputPanelController alloc] init]; fPictureController = [[HBPictureController alloc] init]; fPreviewController = [[HBPreviewController alloc] initWithDelegate:self]; fQueueController = [[HBQueueController alloc] init]; // we init the HBPresetsManager class NSURL *presetsURL = [NSURL fileURLWithPath:[[HBUtilities appSupportPath] stringByAppendingPathComponent:@"UserPresets.plist"]]; presetManager = [[HBPresetsManager alloc] initWithURL:presetsURL]; _selectedPreset = [presetManager.defaultPreset retain]; // 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]; [dockTile updateDockIcon:-1.0 withETA:@""]; // Lets report the HandBrake version number here to the activity log and text log file NSString *versionStringFull = [[NSString stringWithFormat:@"Handbrake Version: %@", [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]] stringByAppendingString:[NSString stringWithFormat: @" (%@)", [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]]]; [HBUtilities writeToActivityLog: "%s", [versionStringFull UTF8String]]; // Optional dvd nav UseDvdNav [HBCore setDVDNav:[[[NSUserDefaults standardUserDefaults] objectForKey:@"UseDvdNav"] boolValue]]; int loggingLevel = [[[NSUserDefaults standardUserDefaults] objectForKey:@"LoggingLevel"] intValue]; // Init libhb _core = [[HBCore alloc] initWithLoggingLevel:loggingLevel]; _core.name = @"ScanCore"; // Init a separate instance of libhb for user scanning and setting up jobs _queueCore = [[HBCore alloc] initWithLoggingLevel:loggingLevel]; _queueCore.name = @"QueueCore"; // Registers the observers to the cores notifications. [self registerScanCoreNotifications]; [self registerQueueCoreNotifications]; // Set the Growl Delegate [GrowlApplicationBridge setGrowlDelegate: self]; [fPictureController setDelegate:self]; [fPreviewController setCore:self.core]; [fQueueController setCore:self.queueCore]; [fQueueController setHBController:self]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(autoSetM4vExtension:) name:HBMixdownChangedNotification object:nil]; } return self; } - (void) applicationDidFinishLaunching: (NSNotification *) notification { // Checks for presets updates [self checkBuiltInsForUpdates]; // Show/Hide the Presets drawer upon launch based // on user preference DefaultPresetsDrawerShow if ([[NSUserDefaults standardUserDefaults] boolForKey:@"HBDefaultPresetsDrawerShow"]) { [fPresetDrawer open:self]; } /* Init QueueFile .plist */ [self loadQueueFile]; [self initQueueFSEvent]; /* Run hbInstances to get any info on other instances as well as set the * pid number for this instance in the case of multi-instance encoding. */ hbInstanceNum = [self hbInstances]; /* If we are a single instance it is safe to clean up the previews if there are any * left over. This is a bit of a kludge but will prevent a build up of old instance * live preview cruft. No danger of removing an active preview directory since they * are created later in HBPreviewController if they don't exist at the moment a live * preview encode is initiated. */ if (hbInstanceNum == 1) { NSString *PreviewDirectory = [[HBUtilities appSupportPath] stringByAppendingPathComponent:@"Previews"]; NSError *error; NSArray *files = [ [NSFileManager defaultManager] contentsOfDirectoryAtPath: PreviewDirectory error: &error ]; for( NSString *file in files ) { if( ![file isEqual: @"."] && ![file isEqual: @".."] ) { [ [NSFileManager defaultManager] removeItemAtPath: [ PreviewDirectory stringByAppendingPathComponent: file ] error: &error ]; if( error ) { //an error occurred [HBUtilities writeToActivityLog: "Could not remove existing preview at : %s",[file UTF8String] ]; } } } } [self enableUI: NO]; // Open debug output window now if it was visible when HB was closed if ([[NSUserDefaults standardUserDefaults] boolForKey:@"OutputPanelIsOpen"]) [self showDebugOutputPanel:nil]; // Open queue window now if it was visible when HB was closed if ([[NSUserDefaults standardUserDefaults] boolForKey:@"QueueWindowIsOpen"]) [self showQueueWindow:nil]; [self openMainWindow:nil]; /* Now we re-check the queue array to see if there are * any remaining encodes to be done in it and ask the * user if they want to reload the queue */ if ([QueueFileArray count] > 0) { /* run getQueueStats to see whats in the queue file */ [self getQueueStats]; /* this results in these values * fPendingCount = 0; * fWorkingCount = 0; */ /* On Screen Notification * We check to see if there is already another instance of hb running. * Note: hbInstances == 1 means we are the only instance of HandBrake.app */ if (hbInstanceNum > 1) { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:NSLocalizedString(@"There is already an instance of HandBrake running.", @"")]; [alert setInformativeText:NSLocalizedString(@"HandBrake will now load up the existing queue.", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Reload Queue", nil)]; [alert beginSheetModalForWindow:fWindow modalDelegate:self didEndSelector:@selector(didDimissReloadQueue:returnCode:contextInfo:) contextInfo:nil]; [alert release]; } else { if (fWorkingCount > 0 || fPendingCount > 0) { NSString *alertTitle; if (fWorkingCount > 0) { alertTitle = [NSString stringWithFormat: NSLocalizedString(@"HandBrake Has Detected %d Previously Encoding Item(s) and %d Pending Item(s) In Your Queue.", @""), fWorkingCount,fPendingCount]; } else { alertTitle = [NSString stringWithFormat: NSLocalizedString(@"HandBrake Has Detected %d Pending Item(s) In Your Queue.", @""), fPendingCount]; } NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:alertTitle]; [alert setInformativeText:NSLocalizedString(@"Do you want to reload them ?", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Reload Queue", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Empty Queue", nil)]; [alert setAlertStyle:NSCriticalAlertStyle]; [alert beginSheetModalForWindow:fWindow modalDelegate:self didEndSelector:@selector(didDimissReloadQueue:returnCode:contextInfo:) contextInfo:nil]; [alert release]; } else { /* Since we addressed any pending or previously encoding items above, we go ahead and make sure * the queue is empty of any finished items or cancelled items */ [self clearQueueAllItems]; if (self.core.state != HBStateScanning && !self.job) { // We show whichever open source window specified in LaunchSourceBehavior preference key if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"LaunchSourceBehavior"] isEqualToString: @"Open Source"]) { [self browseSources:nil]; } if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"LaunchSourceBehavior"] isEqualToString: @"Open Source (Title Specific)"]) { [self browseSources:(id)fOpenSourceTitleMMenu]; } } } } } else { if (self.core.state != HBStateScanning && !self.job) { // We show whichever open source window specified in LaunchSourceBehavior preference key if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"LaunchSourceBehavior"] isEqualToString: @"Open Source"]) { [self browseSources:nil]; } if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"LaunchSourceBehavior"] isEqualToString: @"Open Source (Title Specific)"]) { [self browseSources:(id)fOpenSourceTitleMMenu]; } } } currentQueueEncodeNameString = @""; } - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames { [self openFile:[NSURL fileURLWithPath:filenames.firstObject]]; [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess]; } - (void)openFile:(NSURL *)fileURL { if (self.core.state != HBStateScanning) { self.browsedSourceDisplayName = fileURL.lastPathComponent; [self performScan:fileURL scanTitleNum:0]; } } - (void)setJob:(HBJob *)job { // Set the jobs info to the view controllers fPictureController.picture = job.picture; fPictureController.filters = job.filters; fPreviewController.job = job; fVideoController.job = job; fAudioController.job = job; fSubtitlesViewController.job = job; fChapterTitlesController.job = job; if (job) { [[NSNotificationCenter defaultCenter] removeObserver:_job]; [[NSNotificationCenter defaultCenter] removeObserver:_job.picture]; [[NSNotificationCenter defaultCenter] removeObserver:_job.filters]; [[NSNotificationCenter defaultCenter] removeObserver:_job.video]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pictureSettingsDidChange) name:HBPictureChangedNotification object:job.picture]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pictureSettingsDidChange) name:HBFiltersChangedNotification object:job.filters]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(formatChanged:) name:HBContainerChangedNotification object:job]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(customSettingUsed) name:HBVideoChangedNotification object:job.video]; } // Retain the new job [_job autorelease]; _job = [job retain]; } #pragma mark - #pragma mark Multiple Instances /* hbInstances checks to see if other instances of HB are running and also sets the pid for this instance for multi-instance queue encoding */ /* Note for now since we are in early phases of multi-instance I have put in quite a bit of logging. Can be removed as we see fit. */ - (int) hbInstances { /* check to see if another instance of HandBrake.app is running */ NSArray *runningInstances = [NSRunningApplication runningApplicationsWithBundleIdentifier:[[NSBundle mainBundle] bundleIdentifier]]; NSRunningApplication *runningInstance; NSRunningApplication *thisInstance = [NSRunningApplication currentApplication]; NSString *thisInstanceAppPath = [[NSBundle mainBundle] bundlePath]; [HBUtilities writeToActivityLog: "hbInstances path to this instance: %s", [thisInstanceAppPath UTF8String]]; int hbInstances = 0; NSString *runningInstanceAppPath; pid_t runningInstancePidNum; for (runningInstance in runningInstances) { /*Report the path to each active instances app path */ runningInstancePidNum = [runningInstance processIdentifier]; runningInstanceAppPath = [[runningInstance bundleURL] path]; [HBUtilities writeToActivityLog: "hbInstance found instance pidnum: %d at path: %s", runningInstancePidNum, [runningInstanceAppPath UTF8String]]; /* see if this is us*/ if ([runningInstance isEqual: thisInstance]) { /* If so this is our pidnum */ [HBUtilities writeToActivityLog: "hbInstance MATCH FOUND, our pidnum is: %d", runningInstancePidNum]; /* Get the PID number for this hb instance, used in multi instance encoding */ pidNum = runningInstancePidNum; /* Report this pid to the activity log */ [HBUtilities writeToActivityLog: "Pid for this instance: %d", pidNum]; /* Tell fQueueController what our pidNum is */ [fQueueController setPidNum:pidNum]; } hbInstances++; } return hbInstances; } #pragma mark - #pragma mark Drag & drop handling // This method is used by OSX to know what kind of files can be drag & drop on the NSWindow // We only want filenames (and so folders too) - (NSDragOperation)draggingEntered:(id )sender { NSPasteboard *pboard = [sender draggingPasteboard]; if ([[pboard types] containsObject:NSFilenamesPboardType]) { NSArray *paths = [pboard propertyListForType:NSFilenamesPboardType]; return paths.count == 1 ? NSDragOperationGeneric : NSDragOperationNone; } return NSDragOperationNone; } // This method is doing the job after the drag & drop operation has been validated by [self draggingEntered] and OSX - (BOOL)performDragOperation:(id )sender { NSPasteboard *pboard = [sender draggingPasteboard]; if ([pboard.types containsObject:NSFilenamesPboardType]) { NSArray *paths = [pboard propertyListForType:NSFilenamesPboardType]; [self openFile:[NSURL fileURLWithPath:paths.firstObject]]; } return YES; } #pragma mark - - (void) didDimissReloadQueue: (NSWindow *)sheet returnCode: (int)returnCode contextInfo: (void *)contextInfo { [HBUtilities writeToActivityLog: "didDimissReloadQueue number of hb instances:%d", hbInstanceNum]; if (returnCode == NSAlertSecondButtonReturn) { [HBUtilities writeToActivityLog: "didDimissReloadQueue NSAlertSecondButtonReturn Chosen"]; [self clearQueueAllItems]; /* We show whichever open source window specified in LaunchSourceBehavior preference key */ if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"LaunchSourceBehavior"] isEqualToString: @"Open Source"]) { [self browseSources:nil]; } if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"LaunchSourceBehavior"] isEqualToString: @"Open Source (Title Specific)"]) { [self browseSources:(id)fOpenSourceTitleMMenu]; } } else { [HBUtilities writeToActivityLog: "didDimissReloadQueue NSAlertFirstButtonReturn Chosen"]; if (hbInstanceNum == 1) { [self setQueueEncodingItemsAsPending]; } [self reloadQueue]; [self showQueueWindow:NULL]; } } - (NSApplicationTerminateReply) applicationShouldTerminate: (NSApplication *) app { if (self.queueCore.state != HBStateIdle) { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:NSLocalizedString(@"Are you sure you want to quit HandBrake?", nil)]; [alert setInformativeText:NSLocalizedString(@"If you quit HandBrake your current encode will be reloaded into your queue at next launch. Do you want to quit anyway?", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Quit", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Don't Quit", nil)]; [alert setAlertStyle:NSCriticalAlertStyle]; NSInteger result = [alert runModal]; [alert release]; if (result == NSAlertFirstButtonReturn) { return NSTerminateNow; } else { return NSTerminateCancel; } } // Warn if items still in the queue else if (fPendingCount > 0) { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:NSLocalizedString(@"Are you sure you want to quit HandBrake?", nil)]; [alert setInformativeText:NSLocalizedString(@"There are pending encodes in your queue. Do you want to quit anyway?",nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Quit", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Don't Quit", nil)]; [alert setAlertStyle:NSCriticalAlertStyle]; NSInteger result = [alert runModal]; [alert release]; if (result == NSAlertFirstButtonReturn) { return NSTerminateNow; } else { return NSTerminateCancel; } } return NSTerminateNow; } - (void)applicationWillTerminate:(NSNotification *)aNotification { [presetManager savePresets]; [presetManager release]; [self closeQueueFSEvent]; [currentQueueEncodeNameString release]; [outputPanel release]; [fQueueController release]; [fPreviewController release]; [fPictureController release]; [dockTile release]; [[NSNotificationCenter defaultCenter] removeObserver:self]; self.core = nil; self.queueCore = nil; self.browsedSourceDisplayName = nil; [HBCore closeGlobal]; } - (void) awakeFromNib { /* For 64 bit builds, the threaded animation in the progress * indicators conflicts with the animation in the advanced tab * for reasons not completely clear. jbrjake found a note in the * 10.5 dev notes regarding this possiblility. It was also noted * that unless specified, setUsesThreadedAnimation defaults to true. * So, at least for now we set the indicator animation to NO for * both the scan and regular progress indicators for both 32 and 64 bit * as it test out fine on both and there is no reason our progress indicators * should require their own thread. */ [fScanIndicator setUsesThreadedAnimation:NO]; [fRipIndicator setUsesThreadedAnimation:NO]; // Presets initialization [self buildPresetsMenu]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(buildPresetsMenu) name:HBPresetsChangedNotification object:nil]; [fPresetDrawer setDelegate:self]; NSSize drawerSize = NSSizeFromString([[NSUserDefaults standardUserDefaults] stringForKey:@"HBDrawerSize"]); if (drawerSize.width) { [fPresetDrawer setContentSize: drawerSize]; } // Align the start / stop widgets with the chapter popups NSPoint startPoint = [fSrcChapterStartPopUp frame].origin; startPoint.y += 2; NSPoint endPoint = [fSrcChapterEndPopUp frame].origin; endPoint.y += 2; [fSrcTimeStartEncodingField setFrameOrigin:startPoint]; [fSrcTimeEndEncodingField setFrameOrigin:endPoint]; [fSrcFrameStartEncodingField setFrameOrigin:startPoint]; [fSrcFrameEndEncodingField setFrameOrigin:endPoint]; // Bottom [fStatusField setStringValue:@""]; // Register HBController's Window as a receiver for files/folders drag & drop operations [fWindow registerForDraggedTypes:@[NSFilenamesPboardType]]; // Set up the preset drawer fPresetsView = [[HBPresetsViewController alloc] initWithPresetManager:presetManager]; [fPresetDrawer setContentView:[fPresetsView view]]; fPresetsView.delegate = self; [[fPresetDrawer contentView] setAutoresizingMask:( NSViewWidthSizable | NSViewHeightSizable )]; // Set up the chapters title view fChapterTitlesController = [[HBChapterTitlesController alloc] init]; [fChaptersTitlesTab setView:[fChapterTitlesController view]]; // setup the subtitles view fSubtitlesViewController = [[HBSubtitlesController alloc] init]; [fSubtitlesTab setView:[fSubtitlesViewController view]]; // setup the audio controller fAudioController = [[HBAudioController alloc] init]; [fAudioTab setView:[fAudioController view]]; // setup the advanced view controller fAdvancedOptions = [[HBAdvancedController alloc] init]; [fAdvancedTab setView:[fAdvancedOptions view]]; // setup the video view controller fVideoController = [[HBVideoController alloc] initWithAdvancedController:fAdvancedOptions]; [fVideoTab setView:[fVideoController view]]; [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.HBShowAdvancedTab" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:NULL]; [fWindow recalculateKeyViewLoop]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == NULL) { if ([keyPath isEqualToString:@"values.HBShowAdvancedTab"]) { if ([[NSUserDefaults standardUserDefaults] boolForKey:@"HBShowAdvancedTab"]) { if (![[fMainTabView tabViewItems] containsObject:fAdvancedTab]) { [fMainTabView insertTabViewItem:fAdvancedTab atIndex:3]; [fAdvancedTab release]; } } else { [fAdvancedTab retain]; [fMainTabView removeTabViewItem:fAdvancedTab]; } } } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } - (void) enableUI: (BOOL) b { NSControl * controls[] = { fSrcTitleField, fSrcTitlePopUp, fSrcChapterStartPopUp, fSrcChapterToField, fSrcChapterEndPopUp, fSrcDuration1Field, fSrcDuration2Field, fDstFormatField, fDstFormatPopUp, fDstFile1Field, fDstFile2Field, fDstBrowseButton, fSrcAngleLabel, fSrcAnglePopUp, fDstMp4HttpOptFileCheck, fDstMp4iPodFileCheck, fEncodeStartStopPopUp, fSrcTimeStartEncodingField, fSrcTimeEndEncodingField, fSrcFrameStartEncodingField, fSrcFrameEndEncodingField, }; for (unsigned i = 0; i < (sizeof(controls) / sizeof(NSControl*)); i++) { if ([[controls[i] className] isEqualToString: @"NSTextField"]) { NSTextField *tf = (NSTextField*)controls[i]; if (![tf isBezeled]) { [tf setTextColor: (b ? [NSColor controlTextColor] : [NSColor disabledControlTextColor])]; continue; } } [controls[i] setEnabled: b]; } fPresetsView.enabled = b; } /** * Registers the observers to the scan core notifications */ - (void)registerScanCoreNotifications { NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; [[NSNotificationCenter defaultCenter] addObserverForName:HBCoreScanningNotification object:self.core queue:mainQueue usingBlock:^(NSNotification *note) { hb_state_t s = *(self.core.hb_state); #define p s.param.scanning if (p.preview_cur) { fSrcDVD2Field.stringValue = [NSString stringWithFormat: NSLocalizedString( @"Scanning title %d of %d, preview %d…", @"" ), p.title_cur, p.title_count, p.preview_cur]; } else { fSrcDVD2Field.stringValue = [NSString stringWithFormat: NSLocalizedString( @"Scanning title %d of %d…", @"" ), p.title_cur, p.title_count]; } fScanIndicator.hidden = NO; fScanHorizontalLine.hidden = YES; fScanIndicator.doubleValue = 100.0 * p.progress; #undef p }]; [[NSNotificationCenter defaultCenter] addObserverForName:HBCoreScanDoneNotification object:self.core queue:mainQueue usingBlock:^(NSNotification *note) { fScanHorizontalLine.hidden = NO; fScanIndicator.hidden = YES; fScanIndicator.indeterminate = NO; fScanIndicator.doubleValue = 0.0; [HBUtilities writeToActivityLog:"ScanDone state received from fHandle"]; [self showNewScan]; [[fWindow toolbar] validateVisibleItems]; }]; } - (void)registerQueueCoreNotifications { NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; [[NSNotificationCenter defaultCenter] addObserverForName:HBCoreScanningNotification object:self.queueCore queue:mainQueue usingBlock:^(NSNotification *note) { hb_state_t s = *(self.queueCore.hb_state); #define p s.param.scanning NSString *scan_status; if (p.preview_cur) { scan_status = [NSString stringWithFormat: NSLocalizedString( @"Queue Scanning title %d of %d, preview %d…", @"" ), p.title_cur, p.title_count, p.preview_cur]; } else { scan_status = [NSString stringWithFormat: NSLocalizedString( @"Queue Scanning title %d of %d…", @"" ), p.title_cur, p.title_count]; } fStatusField.stringValue = scan_status; // Set the status string in fQueueController as well [fQueueController setQueueStatusString: scan_status]; #undef p }]; [[NSNotificationCenter defaultCenter] addObserverForName:HBCoreScanDoneNotification object:self.queueCore queue:mainQueue usingBlock:^(NSNotification *note) { [HBUtilities writeToActivityLog:"ScanDone state received from fQueueEncodeLibhb"]; [self processNewQueueEncode]; [[fWindow toolbar] validateVisibleItems]; }]; [[NSNotificationCenter defaultCenter] addObserverForName:HBCoreSearchingNotification object:self.queueCore queue:mainQueue usingBlock:^(NSNotification *note) { hb_state_t s = *(self.queueCore.hb_state); #define p s.param.working NSMutableString *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]; } fStatusField.stringValue = string; // Set the status string in fQueueController as well [fQueueController setQueueStatusString: string]; // Update slider CGFloat progress_total = (p.progress + p.job_cur - 1) / p.job_count; fRipIndicator.indeterminate = NO; fRipIndicator.doubleValue = 100.0 * progress_total; // If progress bar hasn't been revealed at the bottom of the window, do // that now. This code used to be in doRip. I moved it to here to handle // the case where hb_start is called by HBQueueController and not from // HBController. if (!fRipIndicatorShown) { NSRect frame = [fWindow frame]; if (frame.size.width <= 591) frame.size.width = 591; frame.size.height += 36; frame.origin.y -= 36; [fWindow setFrame:frame display:YES animate:YES]; fRipIndicatorShown = YES; } #undef p }]; [[NSNotificationCenter defaultCenter] addObserverForName:HBCoreWorkingNotification object:self.queueCore queue:mainQueue usingBlock:^(NSNotification *note) { hb_state_t s = *(self.queueCore.hb_state); #define p s.param.working // Update text field NSString *pass_desc; if (p.job_cur == 1 && p.job_count > 1) { HBJob *queueJob = QueueFileArray[currentQueueEncodeIndex]; if (queueJob.subtitlesTracks.count && [[queueJob.subtitlesTracks firstObject][keySubTrackIndex] intValue] == -1) { pass_desc = NSLocalizedString(@"(subtitle scan)", @""); } else { pass_desc = @""; } } else { pass_desc = @""; } NSMutableString *string; if (pass_desc.length) { string = [NSMutableString stringWithFormat: NSLocalizedString(@"Encoding: %@ \nPass %d %@ of %d, %.2f %%", @""), currentQueueEncodeNameString, p.job_cur, pass_desc, p.job_count, 100.0 * p.progress]; } else { string = [NSMutableString stringWithFormat: NSLocalizedString(@"Encoding: %@ \nPass %d of %d, %.2f %%", @""), currentQueueEncodeNameString, 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]; } } fStatusField.stringValue = string; [fQueueController setQueueStatusString:string]; // Update slider CGFloat progress_total = (p.progress + p.job_cur - 1) / p.job_count; fRipIndicator.indeterminate = NO; fRipIndicator.doubleValue = 100.0 * progress_total; // If progress bar hasn't been revealed at the bottom of the window, do // that now. This code used to be in doRip. I moved it to here to handle // the case where hb_start is called by HBQueueController and not from // HBController. if (!fRipIndicatorShown) { NSRect frame = [fWindow frame]; if (frame.size.width <= 591) frame.size.width = 591; frame.size.height += 36; frame.origin.y -= 36; [fWindow setFrame:frame display:YES animate:YES]; fRipIndicatorShown = YES; } /* Update dock icon */ if (dockIconProgress < 100.0 * progress_total) { // 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 = @"~"; [dockTile updateDockIcon:progress_total withETA:etaStr]; dockIconProgress += dockTileUpdateFrequency; } #undef p }]; [[NSNotificationCenter defaultCenter] addObserverForName:HBCoreMuxingNotification object:self.queueCore queue:mainQueue usingBlock:^(NSNotification *note) { // Update text field fStatusField.stringValue = NSLocalizedString(@"Muxing…", @""); // Set the status string in fQueueController as well [fQueueController setQueueStatusString:NSLocalizedString(@"Muxing…", @"")]; // Update slider fRipIndicator.indeterminate = YES; [fRipIndicator startAnimation: nil]; // Update dock icon [dockTile updateDockIcon:1.0 withETA:@""]; }]; [[NSNotificationCenter defaultCenter] addObserverForName:HBCorePausedNotification object:self.queueCore queue:mainQueue usingBlock:^(NSNotification *note) { NSString *paused = NSLocalizedString(@"Paused", @""); fStatusField.stringValue = paused; [fQueueController setQueueStatusString:paused]; }]; [[NSNotificationCenter defaultCenter] addObserverForName:HBCoreWorkDoneNotification object:self.queueCore queue:mainQueue usingBlock:^(NSNotification *note) { fStatusField.stringValue = NSLocalizedString(@"Encode Finished.", @""); // Set the status string in fQueueController as well [fQueueController setQueueStatusString:NSLocalizedString(@"Encode Finished.", @"")]; fRipIndicator.indeterminate = NO; fRipIndicator.doubleValue = 0.0; [fRipIndicator setIndeterminate: NO]; [[fWindow toolbar] validateVisibleItems]; // Restore dock icon [dockTile updateDockIcon:-1.0 withETA:@""]; dockIconProgress = 0; if (fRipIndicatorShown) { NSRect frame = [fWindow frame]; if( frame.size.width <= 591 ) frame.size.width = 591; frame.size.height += -36; frame.origin.y -= -36; [fWindow setFrame:frame display:YES animate:YES]; fRipIndicatorShown = NO; } // Since we are done with this encode, tell output to stop writing to the // individual encode log. [outputPanel endEncodeLog]; // Check to see if the encode state has not been cancelled // to determine if we should check for encode done notifications. if (fEncodeState != 2) { // Get the output file name for the finished encode HBJob *finishedJob = QueueFileArray[currentQueueEncodeIndex]; // 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:finishedJob.destURL]; } // Send to tagger [self sendToExternalApp:finishedJob.destURL]; // since we have successfully completed an encode, we increment the queue counter [self incrementQueueItemDone:currentQueueEncodeIndex]; } }]; } #pragma mark - #pragma mark Toolbar - (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem { SEL action = toolbarItem.action; if (self.core) { if (self.core.state == HBStateScanning) { if (action == @selector(browseSources:)) { [toolbarItem setImage: [NSImage imageNamed: @"stopencode"]]; [toolbarItem setLabel: @"Cancel Scan"]; [toolbarItem setPaletteLabel: @"Cancel Scanning"]; [toolbarItem setToolTip: @"Cancel Scanning Source"]; return YES; } if (action == @selector(Rip:) || action == @selector(addToQueue:)) return NO; } else { if (action == @selector(browseSources:)) { [toolbarItem setImage: [NSImage imageNamed: @"source"]]; [toolbarItem setLabel: @"Source"]; [toolbarItem setPaletteLabel: @"Source"]; [toolbarItem setToolTip: @"Choose Video Source"]; return YES; } } HBState queueState = self.queueCore.state; if (queueState == HBStateWorking || queueState == HBStateSearching || queueState == HBStateMuxing) { if (action == @selector(Rip:)) { [toolbarItem setImage: [NSImage imageNamed: @"stopencode"]]; [toolbarItem setLabel: @"Stop"]; [toolbarItem setPaletteLabel: @"Stop"]; [toolbarItem setToolTip: @"Stop Encoding"]; return YES; } if (action == @selector(Pause:)) { [toolbarItem setImage: [NSImage imageNamed: @"pauseencode"]]; [toolbarItem setLabel: @"Pause"]; [toolbarItem setPaletteLabel: @"Pause Encoding"]; [toolbarItem setToolTip: @"Pause Encoding"]; return YES; } if (self.job) { if (action == @selector(addToQueue:)) return YES; if (action == @selector(showPicturePanel:)) return YES; if (action == @selector(showPreviewWindow:)) return YES; } } else if (queueState == HBStatePaused) { if (action == @selector(Pause:)) { [toolbarItem setImage: [NSImage imageNamed: @"encode"]]; [toolbarItem setLabel: @"Resume"]; [toolbarItem setPaletteLabel: @"Resume Encoding"]; [toolbarItem setToolTip: @"Resume Encoding"]; return YES; } if (action == @selector(Rip:)) return YES; if (action == @selector(addToQueue:)) return YES; if (action == @selector(showPicturePanel:)) return YES; if (action == @selector(showPreviewWindow:)) return YES; } else if (queueState == HBStateScanning) { return NO; } else if (queueState == HBStateWorkDone || queueState == HBStateScanDone || self.job) { if (action == @selector(Rip:)) { [toolbarItem setImage: [NSImage imageNamed: @"encode"]]; if (QueueFileArray.count > 0) [toolbarItem setLabel: @"Start Queue"]; else [toolbarItem setLabel: @"Start"]; [toolbarItem setPaletteLabel: @"Start Encoding"]; [toolbarItem setToolTip: @"Start Encoding"]; return YES; } if (action == @selector(addToQueue:)) return YES; if (action == @selector(showPicturePanel:)) return YES; if (action == @selector(showPreviewWindow:)) return YES; } } /* If there are any pending queue items, make sure the start/stop button is active */ if (action == @selector(Rip:) && fPendingCount > 0) return YES; if (action == @selector(showQueueWindow:)) return YES; if (action == @selector(toggleDrawer:)) return YES; if (action == @selector(browseSources:)) return YES; if (action == @selector(showDebugOutputPanel:)) return YES; return NO; } - (BOOL) validateMenuItem: (NSMenuItem *) menuItem { SEL action = [menuItem action]; if (self.queueCore) { HBState queueState = self.queueCore.state; if (action == @selector(addToQueue:) || action == @selector(addAllTitlesToQueue:) || action == @selector(showPicturePanel:) || action == @selector(showAddPresetPanel:)) return self.job && [fWindow attachedSheet] == nil; if (action == @selector(selectDefaultPreset:)) return [fWindow attachedSheet] == nil; if (action == @selector(Pause:)) { if (queueState == HBStateWorking) { if(![[menuItem title] isEqualToString:@"Pause Encoding"]) [menuItem setTitle:@"Pause Encoding"]; return YES; } else if (queueState == HBStatePaused) { if(![[menuItem title] isEqualToString:@"Resume Encoding"]) [menuItem setTitle:@"Resume Encoding"]; return YES; } else return NO; } if (action == @selector(Rip:)) { if (queueState == HBStateWorking || queueState == HBStateMuxing || queueState == HBStatePaused) { if(![[menuItem title] isEqualToString:@"Stop Encoding"]) [menuItem setTitle:@"Stop Encoding"]; return YES; } else if (self.job) { if(![[menuItem title] isEqualToString:@"Start Encoding"]) [menuItem setTitle:@"Start Encoding"]; return [fWindow attachedSheet] == nil; } else return NO; } if (action == @selector(browseSources:)) { if (self.core.state == HBStateScanning) return NO; else return [fWindow attachedSheet] == nil; } if (action == @selector(selectPresetFromMenu:)) { if ([menuItem.representedObject isEqualTo:self.selectedPreset]) { [menuItem setState:NSOnState]; } else { [menuItem setState:NSOffState]; } } } return YES; } #pragma mark - #pragma mark Encode Done Actions #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"]) { [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]; } } } - (void) queueCompletedAlerts { /* If Play System Alert has been selected in Preferences */ if( [[NSUserDefaults standardUserDefaults] boolForKey:@"AlertWhenDoneSound"] == YES ) { NSBeep(); } /* 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:@"Put down that cocktail…"]; [alert setInformativeText:@"Your HandBrake queue is done!"]; [NSApp requestUserAttention:NSCriticalRequest]; [alert runModal]; [alert release]; } /* 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 Get New Source /** * Opens the source browse window, called from Open Source widgets */ - (IBAction)browseSources:(id)sender { if (self.core.state == HBStateScanning) { [self.core cancelScan]; return; } NSOpenPanel *panel = [NSOpenPanel openPanel]; [panel setAllowsMultipleSelection:NO]; [panel setCanChooseFiles:YES]; [panel setCanChooseDirectories:YES]; NSURL *sourceDirectory; if ([[NSUserDefaults standardUserDefaults] URLForKey:@"HBLastSourceDirectoryURL"]) { sourceDirectory = [[NSUserDefaults standardUserDefaults] URLForKey:@"HBLastSourceDirectoryURL"]; } else { sourceDirectory = [[NSURL fileURLWithPath:NSHomeDirectory()] URLByAppendingPathComponent:@"Desktop"]; } [panel setDirectoryURL:sourceDirectory]; [panel beginSheetModalForWindow:fWindow completionHandler: ^(NSInteger result) { if (result == NSOKButton) { NSURL *scanURL = panel.URL; // we set the last searched source directory in the prefs here [[NSUserDefaults standardUserDefaults] setURL:scanURL.URLByDeletingLastPathComponent forKey:@"HBLastSourceDirectoryURL"]; // we order out sheet, which is the browse window as we need to open // the title selection sheet right away [panel orderOut:self]; if (sender == fOpenSourceTitleMMenu || [[NSApp currentEvent] modifierFlags] & NSAlternateKeyMask) { // We put the chosen source path in the source display text field for the // source title selection sheet in which the user specifies the specific title to be // scanned as well as the short source name in fSrcDsplyNameTitleScan just for display // purposes in the title panel // Full Path [fScanSrcTitlePathField setStringValue:scanURL.path]; NSString *displayTitlescanSourceName; if ([scanURL.lastPathComponent isEqualToString:@"VIDEO_TS"]) { // If VIDEO_TS Folder is chosen, choose its parent folder for the source display name // we have to use the title->path value so we get the proper name of the volume if a physical dvd is the source displayTitlescanSourceName = scanURL.URLByDeletingLastPathComponent.lastPathComponent; } else { // if not the VIDEO_TS Folder, we can assume the chosen folder is the source name displayTitlescanSourceName = scanURL.lastPathComponent; } // we set the source display name in the title selection dialogue [fSrcDsplyNameTitleScan setStringValue:displayTitlescanSourceName]; // we set the attempted scans display name for main window to displayTitlescanSourceName self.browsedSourceDisplayName = displayTitlescanSourceName; // We show the actual sheet where the user specifies the title to be scanned // as we are going to do a title specific scan [self showSourceTitleScanPanel:nil]; } else { // We are just doing a standard full source scan, so we specify "0" to libhb NSURL *url = panel.URL; // We check to see if the chosen file at path is a package if ([[NSWorkspace sharedWorkspace] isFilePackageAtPath:url.path]) { [HBUtilities writeToActivityLog:"trying to open a package at: %s", url.path.UTF8String]; // We check to see if this is an .eyetv package if ([url.pathExtension isEqualToString:@"eyetv"]) { [HBUtilities writeToActivityLog:"trying to open eyetv package"]; // We're looking at an EyeTV package - try to open its enclosed .mpg media file self.browsedSourceDisplayName = url.URLByDeletingPathExtension.lastPathComponent; NSString *mpgname; NSUInteger n = [[url.path stringByAppendingString: @"/"] completePathIntoString: &mpgname caseSensitive: YES matchesIntoArray: nil filterTypes: @[@"mpg"]]; if (n > 0) { // Found an mpeg inside the eyetv package, make it our scan path [HBUtilities writeToActivityLog:"found mpeg in eyetv package"]; url = [NSURL fileURLWithPath:mpgname]; } else { // We did not find an mpeg file in our package, so we do not call performScan [HBUtilities writeToActivityLog:"no valid mpeg in eyetv package"]; } } // We check to see if this is a .dvdmedia package else if ([url.pathExtension isEqualToString:@"dvdmedia"]) { // path IS a package - but dvdmedia packages can be treaded like normal directories self.browsedSourceDisplayName = url.URLByDeletingPathExtension.lastPathComponent; [HBUtilities writeToActivityLog:"trying to open dvdmedia package"]; } else { // The package is not an eyetv package, try to open it anyway self.browsedSourceDisplayName = url.lastPathComponent; [HBUtilities writeToActivityLog:"not a known to package"]; } } else { // path is not a package, so we call perform scan directly on our file if ([url.lastPathComponent isEqualToString:@"VIDEO_TS"]) { [HBUtilities writeToActivityLog:"trying to open video_ts folder (video_ts folder chosen)"]; // If VIDEO_TS Folder is chosen, choose its parent folder for the source display name url = url.URLByDeletingLastPathComponent; self.browsedSourceDisplayName = url.lastPathComponent; } else { [HBUtilities writeToActivityLog:"trying to open a folder or file"]; // if not the VIDEO_TS Folder, we can assume the chosen folder is the source name // make sure we remove any path extension self.browsedSourceDisplayName = url.lastPathComponent; } } [self performScan:url scanTitleNum:0]; } } }]; } /* Here we open the title selection sheet where we can specify an exact title to be scanned */ - (IBAction) showSourceTitleScanPanel: (id) sender { /* We default the title number to be scanned to "0" which results in a full source scan, unless the * user changes it */ [fScanSrcTitleNumField setStringValue: @"0"]; /* Show the panel */ [NSApp beginSheet:fScanSrcTitlePanel modalForWindow:fWindow modalDelegate:nil didEndSelector:NULL contextInfo:NULL]; } - (IBAction) closeSourceTitleScanPanel: (id) sender { [NSApp endSheet: fScanSrcTitlePanel]; [fScanSrcTitlePanel orderOut: self]; if (sender == fScanSrcTitleOpenButton) { // We setup the scan status in the main window to indicate a source title scan [fSrcDVD2Field setStringValue: @"Opening a new source title…"]; [fScanIndicator setHidden: NO]; [fScanHorizontalLine setHidden: YES]; [fScanIndicator setIndeterminate: YES]; [fScanIndicator startAnimation: nil]; // We use the performScan method to actually perform the specified scan passing the path and the title // to be scanned [self performScan:[NSURL fileURLWithPath:fScanSrcTitlePathField.stringValue] scanTitleNum:fScanSrcTitleNumField.intValue]; } } /* Here we actually tell hb_scan to perform the source scan, using the path to source and title number*/ - (void)performScan:(NSURL *)scanURL scanTitleNum:(NSInteger)scanTitleNum { // Save the current settings if (self.job) { self.selectedPreset = [self createPresetFromCurrentSettings]; } self.job = nil; [self enableUI:NO]; NSError *outError = NULL; BOOL suppressWarning = [[NSUserDefaults standardUserDefaults] boolForKey:@"suppresslibdvdcss"]; // Check if we can scan the source and if there is any warning. BOOL canScan = [self.core canScan:scanURL error:&outError]; // Notify the user that we don't support removal of copy proteciton. if (canScan && [outError code] == 101 && !suppressWarning) { // Only show the user this warning once. They may be using a solution we don't know about. Notifying them each time is annoying. [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"suppresslibdvdcss"]; // Compatible libdvdcss not found [HBUtilities writeToActivityLog: "libdvdcss.2.dylib not found for decrypting physical dvd"]; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"Please note that HandBrake does not support the removal of copy-protection from DVD Discs. You can if you wish install libdvdcss or any other 3rd party software for this function."]; [alert setInformativeText:@"Videolan.org provides libdvdcss if you are not currently using another solution."]; [alert addButtonWithTitle:@"Get libdvdcss.pkg"]; [alert addButtonWithTitle:@"Cancel Scan"]; [alert addButtonWithTitle:@"Attempt Scan Anyway"]; [NSApp requestUserAttention:NSCriticalRequest]; NSInteger status = [alert runModal]; [alert release]; if (status == NSAlertFirstButtonReturn) { /* User chose to go download vlc (as they rightfully should) so we send them to the vlc site */ [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://download.videolan.org/libdvdcss/1.2.12/macosx/"]]; canScan = NO; } else if (status == NSAlertSecondButtonReturn) { /* User chose to cancel the scan */ [HBUtilities writeToActivityLog: "Cannot open physical dvd, scan cancelled"]; canScan = NO; } else { /* User chose to override our warning and scan the physical dvd anyway, at their own peril. on an encrypted dvd this produces massive log files and fails */ [HBUtilities writeToActivityLog:"User overrode copy-protection warning - trying to open physical dvd without decryption"]; } } if (canScan) { int hb_num_previews = [[[NSUserDefaults standardUserDefaults] objectForKey:@"PreviewsNumber"] intValue]; int min_title_duration_seconds = [[[NSUserDefaults standardUserDefaults] objectForKey:@"MinTitleScanSeconds"] intValue]; [self.core scan:scanURL titleNum:scanTitleNum previewsNum:hb_num_previews minTitleDuration:min_title_duration_seconds]; } } - (void)showNewScan { if (!self.core.titles.count) { // We display a message if a valid source was not chosen fSrcDVD2Field.stringValue = @"No Valid Source Found"; } else { if (self.jobFromQueue) { // we are a rescan of an existing queue item and need to apply the queued settings to the scan [HBUtilities writeToActivityLog: "showNewScan: This is a queued item rescan"]; } else { [HBUtilities writeToActivityLog: "showNewScan: This is a new source item scan"]; } [fSrcTitlePopUp removeAllItems]; for (HBTitle *title in self.core.titles) { // Set Source Name at top of window with the browsedSourceDisplayName grokked right before -performScan if (!self.browsedSourceDisplayName) { self.browsedSourceDisplayName = @"NoNameDetected"; } fSrcDVD2Field.stringValue = self.browsedSourceDisplayName; [fSrcTitlePopUp addItemWithTitle:title.description]; // See if this is the main feature according if (title.isFeatured) { [fSrcTitlePopUp selectItemWithTitle:title.description]; } } // Select the first item is nothing is selected if (!fSrcTitlePopUp.selectedItem) { [fSrcTitlePopUp selectItemAtIndex:0]; } // Updates the main window ui [self enableUI:YES]; [self titlePopUpChanged:nil]; // Open preview window now if it was visible when HB was closed if ([[NSUserDefaults standardUserDefaults] boolForKey:@"PreviewWindowIsOpen"]) [self showPreviewWindow:nil]; // Open picture sizing window now if it was visible when HB was closed if ([[NSUserDefaults standardUserDefaults] boolForKey:@"PictureSizeWindowIsOpen"]) [self showPicturePanel:nil]; if (self.jobFromQueue) { [fPresetsView deselect]; self.jobFromQueue = nil; } } } #pragma mark - #pragma mark New Output Destination - (IBAction)browseFile:(id)sender { // Open a panel to let the user choose and update the text field NSSavePanel *panel = [NSSavePanel savePanel]; panel.directoryURL = self.job.destURL.URLByDeletingLastPathComponent; panel.nameFieldStringValue = self.job.destURL.lastPathComponent; [panel beginSheetModalForWindow:fWindow completionHandler:^(NSInteger result) { if (result == NSFileHandlingPanelOKButton) { self.job.destURL = panel.URL; // Save this path to the prefs so that on next browse destination window it opens there [[NSUserDefaults standardUserDefaults] setURL:panel.URL.URLByDeletingLastPathComponent forKey:@"HBLastDestinationDirectory"]; } }]; } #pragma mark - #pragma mark Main Window Control - (IBAction) openMainWindow: (id) sender { [fWindow makeKeyAndOrderFront:nil]; } - (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag { if( !flag ) { [fWindow makeKeyAndOrderFront:nil]; } return YES; } - (NSSize) drawerWillResizeContents:(NSDrawer *) drawer toSize:(NSSize) contentSize { [[NSUserDefaults standardUserDefaults] setObject:NSStringFromSize( contentSize ) forKey:@"HBDrawerSize"]; return contentSize; } #pragma mark - #pragma mark Queue File static void queueFSEventStreamCallback( ConstFSEventStreamRef streamRef, void *clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]) { if (numEvents >= 1) { // Reload the queue only if one of the following events happened. FSEventStreamEventFlags flags = eventFlags[0]; if (flags & (kFSEventStreamEventFlagItemCreated | kFSEventStreamEventFlagItemRemoved | kFSEventStreamEventFlagItemRenamed | kFSEventStreamEventFlagItemModified)) { HBController *hb = (HBController *)clientCallBackInfo; [hb reloadQueue]; } } } - (void)initQueueFSEvent { /* Define variables and create a CFArray object containing CFString objects containing paths to watch. */ CFStringRef mypath = (CFStringRef) [[HBUtilities appSupportPath] stringByAppendingPathComponent:@"Queue"]; CFArrayRef pathsToWatch = CFArrayCreate(NULL, (const void **)&mypath, 1, NULL); FSEventStreamContext callbackCtx; callbackCtx.version = 0; callbackCtx.info = self; callbackCtx.retain = NULL; callbackCtx.release = NULL; callbackCtx.copyDescription = NULL; CFAbsoluteTime latency = 0.5; /* Latency in seconds */ /* Create the stream, passing in a callback */ QueueStream = FSEventStreamCreate(NULL, &queueFSEventStreamCallback, &callbackCtx, pathsToWatch, kFSEventStreamEventIdSinceNow, latency, kFSEventStreamCreateFlagIgnoreSelf | kFSEventStreamCreateFlagMarkSelf ); CFRelease(pathsToWatch); /* Create the stream before calling this. */ FSEventStreamScheduleWithRunLoop(QueueStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); FSEventStreamStart(QueueStream); } - (void)closeQueueFSEvent { FSEventStreamStop(QueueStream); FSEventStreamInvalidate(QueueStream); FSEventStreamRelease(QueueStream); } - (void)loadQueueFile { // We declare the default NSFileManager into fileManager NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *appSupportPath = [HBUtilities appSupportPath]; // We define the location of the user presets file QueueFile = [[appSupportPath stringByAppendingPathComponent:@"Queue/Queue.hbqueue"] retain]; // We check for the Queue.plist if (![fileManager fileExistsAtPath:QueueFile]) { if (![fileManager fileExistsAtPath:[appSupportPath stringByAppendingPathComponent:@"Queue"]]) { [fileManager createDirectoryAtPath:[appSupportPath stringByAppendingPathComponent:@"Queue"] withIntermediateDirectories:YES attributes:nil error:NULL]; } [fileManager createFileAtPath:QueueFile contents:nil attributes:nil]; } @try { QueueFileArray = [[NSKeyedUnarchiver unarchiveObjectWithFile:QueueFile] retain]; } @catch (NSException *exception) { [HBUtilities writeToActivityLog:"failed to read the queue to disk"]; } // lets check to see if there is anything in the queue file .plist if (QueueFileArray == nil) { // if not, then lets initialize an empty array QueueFileArray = [[NSMutableArray alloc] init]; } else { // ONLY clear out encoded items if we are single instance if (hbInstanceNum == 1) { [self clearQueueEncodedItems]; } } } - (void)reloadQueue { [HBUtilities writeToActivityLog:"Queue reloaded"]; NSMutableArray *tempQueueArray = nil;; @try { tempQueueArray = [NSKeyedUnarchiver unarchiveObjectWithFile:QueueFile]; } @catch (NSException *exception) { tempQueueArray = nil; [HBUtilities writeToActivityLog:"failed to read the queue to disk"]; } [QueueFileArray setArray:tempQueueArray]; // Send Fresh QueueFileArray to fQueueController to update queue window [fQueueController setQueueArray:QueueFileArray]; [self getQueueStats]; } - (void)addQueueFileItem { [QueueFileArray addObject:[[self.job copy] autorelease]]; [self saveQueueFileItem]; } - (void)removeQueueFileItem:(NSUInteger)queueItemToRemove { [QueueFileArray removeObjectAtIndex:queueItemToRemove]; [self saveQueueFileItem]; } - (void)saveQueueFileItem { if (![NSKeyedArchiver archiveRootObject:QueueFileArray toFile:QueueFile]) { [HBUtilities writeToActivityLog:"failed to write the queue to disk"]; } [fQueueController setQueueArray: QueueFileArray]; [self getQueueStats]; } /** * Updates the queue status label on the main window. */ - (void)getQueueStats { // lets get the stats on the status of the queue array fPendingCount = 0; fWorkingCount = 0; int i = 0; for (HBJob *job in QueueFileArray) { if (job.state == HBJobStateWorking) // being encoded { fWorkingCount++; // check to see if we are the instance doing this encoding if (job.pidId == pidNum) { currentQueueEncodeIndex = i; } } if (job.state == HBJobStateReady) // pending { fPendingCount++; } i++; } // Set the queue status field in the main window NSString *string; if (fPendingCount == 0) { string = NSLocalizedString( @"No encode pending", @""); } else if (fPendingCount == 1) { string = [NSString stringWithFormat: NSLocalizedString( @"%d encode pending", @"" ), fPendingCount]; } else { string = [NSString stringWithFormat: NSLocalizedString( @"%d encodes pending", @"" ), fPendingCount]; } [fQueueStatus setStringValue:string]; } /** * Used to get the next pending queue item index and return it if found */ - (NSInteger)getNextPendingQueueIndex { // initialize nextPendingIndex to -1, this value tells incrementQueueItemDone that there are no pending items in the queue NSInteger nextPendingIndex = -1; BOOL nextPendingFound = NO; for (HBJob *job in QueueFileArray) { if (job.state == HBJobStateReady && nextPendingFound == NO) // pending { nextPendingFound = YES; nextPendingIndex = [QueueFileArray indexOfObject:job]; [HBUtilities writeToActivityLog: "getNextPendingQueueIndex next pending encode index is:%d", nextPendingIndex]; } } return nextPendingIndex; } /** * This method will set any item marked as encoding back to pending * currently used right after a queue reload */ - (void) setQueueEncodingItemsAsPending { for (HBJob *job in QueueFileArray) { // We want to keep any queue item that is pending or was previously being encoded if (job.state == HBJobStateWorking || job.state == HBJobStateReady) { // If the queue item is marked as "working" // then change its status back to ready which effectively // puts it back into the queue to be encoded if (job.state == HBJobStateWorking) { job.state = HBJobStateReady; } } } [self saveQueueFileItem]; } /** * 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) clearQueueEncodedItems { NSMutableArray *tempArray = [NSMutableArray array]; for (HBJob *job in QueueFileArray) { /* If the queue item is either completed (0) or cancelled (3) from the * last session, then we put it in tempArray to be deleted from QueueFileArray. * NOTE: this means we retain pending (2) and also an item that is marked as * still encoding (1). If the queue has an item that is still marked as encoding * from a previous session, we can conlude that HB was either shutdown, or crashed * during the encodes so we keep it and tell the user in the "Load Queue Alert" */ if (job.state == HBJobStateCompleted || job.state == HBJobStateCanceled) { [tempArray addObject:job]; } } [QueueFileArray removeObjectsInArray:tempArray]; [self saveQueueFileItem]; } /** * This method will clear the queue of all encodes. effectively creating an empty queue */ - (void) clearQueueAllItems { NSMutableArray *tempArray = [NSMutableArray array]; // we look here to see if the preset is we move on to the next one for (HBJob *job in QueueFileArray) { [tempArray addObject:job]; } [QueueFileArray removeObjectsInArray:tempArray]; [self saveQueueFileItem]; } /** * 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 { NSUInteger index = [indexSet lastIndex]; NSUInteger aboveInsertIndexCount = 0; NSUInteger removeIndex; if (index >= insertIndex) { removeIndex = index + aboveInsertIndexCount; aboveInsertIndexCount++; } else { removeIndex = index; insertIndex--; } id object = [[QueueFileArray objectAtIndex:removeIndex] retain]; [QueueFileArray removeObjectAtIndex:removeIndex]; [QueueFileArray 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 saveQueueFileItem]; } #pragma mark - #pragma mark Queue Job Processing - (void)incrementQueueItemDone:(NSInteger)queueItemDoneIndexNum { // Mark the encode just finished as done (status 0) HBJob *queueJob = QueueFileArray[currentQueueEncodeIndex]; queueJob.state = HBJobStateCompleted; // We save all of the Queue data here [self saveQueueFileItem]; /* Since we have now marked a queue item as done * we can go ahead and increment currentQueueEncodeIndex * so that if there is anything left in the queue we can * go ahead and move to the next item if we want to */ NSInteger queueItems = [QueueFileArray count]; /* Check to see if there are any more pending items in the queue */ NSInteger newQueueItemIndex = [self getNextPendingQueueIndex]; /* If we still have more pending items in our queue, lets go to the next one */ if (newQueueItemIndex >= 0 && newQueueItemIndex < queueItems) { // Set our currentQueueEncodeIndex now to the newly found Pending encode as we own it currentQueueEncodeIndex = newQueueItemIndex; // now we mark the queue item as Status = 1 ( being encoded ) so another instance can not come along and try to scan it while we are scanning queueJob = QueueFileArray[currentQueueEncodeIndex]; queueJob.state = HBJobStateWorking; [HBUtilities writeToActivityLog: "incrementQueueItemDone new pending items found: %d", currentQueueEncodeIndex]; [self saveQueueFileItem]; queueJob = QueueFileArray[currentQueueEncodeIndex]; // now we can go ahead and scan the new pending queue item [self performNewQueueScan:queueJob.fileURL.path scanTitleNum:queueJob.titleIdx]; } else { [HBUtilities writeToActivityLog: "incrementQueueItemDone 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]; } } /** * Here we actually tell hb_scan to perform the source scan, using the path to source and title number */ - (void) performNewQueueScan:(NSString *) scanPath scanTitleNum: (NSInteger) scanTitleNum { HBJob *queueJob = QueueFileArray[currentQueueEncodeIndex]; // Tell HB to output a new activity log file for this encode [outputPanel startEncodeLog:queueJob.destURL]; // We now flag the queue item as being owned by this instance of HB using the PID queueJob.pidId = pidNum; // Get the currentQueueEncodeNameString from the queue item to display in the status field */ [currentQueueEncodeNameString autorelease]; currentQueueEncodeNameString = [[queueJob.destURL.path lastPathComponent] retain]; // We save all of the Queue data here [self saveQueueFileItem]; // 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 NSURL *fileURL = [NSURL fileURLWithPath:scanPath]; [self.queueCore scan:fileURL titleNum:scanTitleNum previewsNum:10 minTitleDuration:0]; } /** * This assumes that we have re-scanned and loaded up a new queue item to send to libhb as fQueueEncodeLibhb */ - (void)processNewQueueEncode { HBJob *queueJob = QueueFileArray[currentQueueEncodeIndex]; if (self.queueCore.titles.count) { // Reset the title in the job. queueJob.title = self.queueCore.titles[0]; // We should be all setup so let 'er rip [self.queueCore encodeJob:queueJob]; fEncodeState = 1; // Lets mark our new encode as 1 or "Encoding" queueJob.state = HBJobStateWorking; // We are done using the title, remove it from the job queueJob.title = nil; [self saveQueueFileItem]; } else { [HBUtilities writeToActivityLog: "processNewQueueEncode WARNING nothing found in the title list"]; } } #pragma mark - #pragma mark Queue Item Editing /* Rescans the chosen queue item back into the main window */ - (void)rescanQueueItemToMainWindow:(NSUInteger)selectedQueueItem { [HBUtilities writeToActivityLog:"rescanQueueItemToMainWindow: Re-scanning queue item at index:%d", selectedQueueItem]; // Set the browsedSourceDisplayName for showNewScan self.jobFromQueue = QueueFileArray[selectedQueueItem]; self.browsedSourceDisplayName = self.jobFromQueue.fileURL.lastPathComponent; [self performScan:self.jobFromQueue.fileURL scanTitleNum:self.jobFromQueue.titleIdx]; // Now that source is loaded and settings applied, delete the queue item from the queue [HBUtilities writeToActivityLog: "applyQueueSettingsToMainWindow: deleting queue item:%d", selectedQueueItem]; [self removeQueueFileItem:selectedQueueItem]; } #pragma mark - #pragma mark Job Handling /* addToQueue: puts up an alert before ultimately calling doAddToQueue */ - (IBAction)addToQueue:(id)sender { // We get the destination directory from the destination field here NSString *destinationDirectory = self.job.destURL.path.stringByDeletingLastPathComponent; // We check for a valid destination here if ([[NSFileManager defaultManager] fileExistsAtPath:destinationDirectory] == 0) { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"Warning!"]; [alert setInformativeText:@"This is not a valid destination directory!"]; [alert runModal]; [alert release]; return; } BOOL fileExists; fileExists = NO; BOOL fileExistsInQueue; fileExistsInQueue = NO; // We check for and existing file here if([[NSFileManager defaultManager] fileExistsAtPath:self.job.destURL.path]) { fileExists = YES; } // We now run through the queue and make sure we are not overwriting an exisiting queue item for (HBJob *job in QueueFileArray) { if ([job.destURL isEqualTo:self.job.destURL]) { fileExistsInQueue = YES; } } if (fileExists == YES) { NSAlert *alert = [NSAlert alertWithMessageText:@"File already exists." defaultButton:@"Cancel" alternateButton:@"Overwrite" otherButton:nil informativeTextWithFormat:@"Do you want to overwrite %@?", self.job.destURL.path]; [alert setAlertStyle:NSCriticalAlertStyle]; [alert beginSheetModalForWindow:fWindow modalDelegate:self didEndSelector:@selector( overwriteAddToQueueAlertDone:returnCode:contextInfo: ) contextInfo:NULL]; } else if (fileExistsInQueue == YES) { NSAlert *alert = [NSAlert alertWithMessageText:@"There is already a queue item for this destination." defaultButton:@"Cancel" alternateButton:@"Overwrite" otherButton:nil informativeTextWithFormat:@"Do you want to overwrite %@?", self.job.destURL.path]; [alert setAlertStyle:NSCriticalAlertStyle]; [alert beginSheetModalForWindow:fWindow modalDelegate:self didEndSelector:@selector( overwriteAddToQueueAlertDone:returnCode:contextInfo: ) contextInfo:NULL]; } else { [self doAddToQueue]; } } /* overwriteAddToQueueAlertDone: called from the alert posted by addToQueue that asks the user if they want to overwrite an exiting movie file. */ - (void) overwriteAddToQueueAlertDone: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (void *) contextInfo { if( returnCode == NSAlertAlternateReturn ) [self doAddToQueue]; } - (void) doAddToQueue { [self addQueueFileItem]; } /* Rip: puts up an alert before ultimately calling doRip */ - (IBAction) Rip: (id) sender { [HBUtilities writeToActivityLog: "Rip: Pending queue count is %d", fPendingCount]; /* Rip or Cancel ? */ if (self.queueCore.state == HBStateWorking || self.queueCore.state == HBStatePaused) { [self Cancel: sender]; return; } // 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]; // If there are pending jobs in the queue, then this is a rip the queue if (fPendingCount > 0) { currentQueueEncodeIndex = [self getNextPendingQueueIndex]; // here lets start the queue with the first pending item HBJob *queueJob = QueueFileArray[currentQueueEncodeIndex]; [self performNewQueueScan:queueJob.fileURL.path scanTitleNum:queueJob.titleIdx]; return; } // Before adding jobs to the queue, check for a valid destination. NSString *destinationDirectory = self.job.destURL.path.stringByDeletingLastPathComponent; if ([[NSFileManager defaultManager] fileExistsAtPath:destinationDirectory] == 0) { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"Warning!"]; [alert setInformativeText:@"This is not a valid destination directory!"]; [alert runModal]; [alert release]; return; } // We check for duplicate name here if( [[NSFileManager defaultManager] fileExistsAtPath:self.job.destURL.path] ) { NSAlert *alert = [NSAlert alertWithMessageText:@"File already exists." defaultButton:@"Cancel" alternateButton:@"Overwrite" otherButton:nil informativeTextWithFormat:@"Do you want to overwrite %@?", self.job.destURL.path]; [alert setAlertStyle:NSCriticalAlertStyle]; [alert beginSheetModalForWindow:fWindow modalDelegate:self didEndSelector:@selector( overWriteAlertDone:returnCode:contextInfo: ) contextInfo:NULL]; // overWriteAlertDone: will be called when the alert is dismissed. It will call doRip. } else { /* if there are no pending jobs in the queue, then add this one to the queue and rip otherwise, just rip the queue */ if(fPendingCount == 0) { [self doAddToQueue]; } // go right to processing the new queue encode currentQueueEncodeIndex = [self getNextPendingQueueIndex]; HBJob *queueJob = QueueFileArray[currentQueueEncodeIndex]; [self performNewQueueScan:queueJob.fileURL.path scanTitleNum:queueJob.titleIdx]; } } /* overWriteAlertDone: called from the alert posted by Rip: that asks the user if they want to overwrite an exiting movie file. */ - (void) overWriteAlertDone: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (void *) contextInfo { if( returnCode == NSAlertAlternateReturn ) { // if there are no jobs in the queue, then add this one to the queue and rip // otherwise, just rip the queue if (fPendingCount == 0) { [self doAddToQueue]; } [[NSUserDefaults standardUserDefaults] setURL:self.job.destURL.URLByDeletingLastPathComponent forKey:@"HBLastDestinationDirectory"]; currentQueueEncodeIndex = [self getNextPendingQueueIndex]; HBJob *queueJob = QueueFileArray[currentQueueEncodeIndex]; [self performNewQueueScan:queueJob.fileURL.path scanTitleNum:queueJob.titleIdx]; } } - (void) remindUserOfSleepOrShutdown { if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"AlertWhenDone"] isEqualToString: @"Put Computer To Sleep"]) { /*Warn that computer will sleep after encoding*/ NSBeep(); [NSApp requestUserAttention:NSCriticalRequest]; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"The computer will sleep after encoding is done."]; [alert setInformativeText:@"You have selected to sleep the computer after encoding. To turn off sleeping, go to the HandBrake preferences."]; [alert addButtonWithTitle:@"OK"]; [alert addButtonWithTitle:@"Preferences…"]; NSInteger reminduser = [alert runModal]; [alert release]; if (reminduser == NSAlertSecondButtonReturn) { [self showPreferencesWindow:nil]; } } else if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"AlertWhenDone"] isEqualToString: @"Shut Down Computer"]) { /*Warn that computer will shut down after encoding*/ NSBeep(); [NSApp requestUserAttention:NSCriticalRequest]; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"The computer will shut down after encoding is done."]; [alert setInformativeText:@"You have selected to shut down the computer after encoding. To turn off shut down, go to the HandBrake preferences."]; [alert addButtonWithTitle:@"OK"]; [alert addButtonWithTitle:@"Preferences…"]; NSInteger reminduser = [alert runModal]; if (reminduser == NSAlertSecondButtonReturn) { [self showPreferencesWindow:nil]; } [alert release]; } } //------------------------------------------------------------------------------------ // 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.queueCore pause]; // Which window to attach the sheet to? NSWindow * docWindow; if ([sender respondsToSelector: @selector(window)]) { docWindow = [sender window]; } else { docWindow = fWindow; } 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:docWindow modalDelegate:self didEndSelector:@selector(didDimissCancel:returnCode:contextInfo:) contextInfo:nil]; [alert release]; } - (void) didDimissCancel: (NSWindow *)sheet returnCode: (int)returnCode contextInfo: (void *)contextInfo { /* No need to prevent system sleep here as we didn't allow it in Cancel: */ [self.queueCore resume]; if (returnCode == NSAlertSecondButtonReturn) { [self doCancelCurrentJobAndStop]; } else if (returnCode == NSAlertThirdButtonReturn) { [self doCancelCurrentJob]; // <- this also stops libhb } } //------------------------------------------------------------------------------------ // Cancels and deletes the current job and stops libhb from processing the remaining // encodes. //------------------------------------------------------------------------------------ - (void) doCancelCurrentJob { // Stop the current job. [self.queueCore stop]; fEncodeState = 2; // don't alert at end of processing since this was a cancel // now that we've stopped the currently encoding job, lets mark it as cancelled [QueueFileArray[currentQueueEncodeIndex] setState:HBJobStateCanceled]; // and as always, save it in Queue.plist /* We save all of the Queue data here */ [self saveQueueFileItem]; // and see if there are more items left in our queue NSInteger queueItems = [QueueFileArray count]; /* If we still have more items in our queue, lets go to the next one */ /* Check to see if there are any more pending items in the queue */ NSInteger newQueueItemIndex = [self getNextPendingQueueIndex]; /* If we still have more pending items in our queue, lets go to the next one */ if (newQueueItemIndex >= 0 && newQueueItemIndex < queueItems) { /*Set our currentQueueEncodeIndex now to the newly found Pending encode as we own it */ currentQueueEncodeIndex = newQueueItemIndex; /* now we mark the queue item as worling so another instance can not come along and try to scan it while we are scanning */ [QueueFileArray[currentQueueEncodeIndex] setState:HBJobStateWorking]; [HBUtilities writeToActivityLog: "incrementQueueItemDone new pending items found: %d", currentQueueEncodeIndex]; [self saveQueueFileItem]; HBJob *queueJob = QueueFileArray[currentQueueEncodeIndex]; // now we can go ahead and scan the new pending queue item [self performNewQueueScan:queueJob.fileURL.path scanTitleNum:queueJob.titleIdx]; } else { [HBUtilities writeToActivityLog: "incrementQueueItemDone there are no more pending encodes"]; } } - (void) doCancelCurrentJobAndStop { [self.queueCore stop]; fEncodeState = 2; // don't alert at end of processing since this was a cancel // now that we've stopped the currently encoding job, lets mark it as cancelled [QueueFileArray[currentQueueEncodeIndex] setState:HBJobStateCanceled]; // and as always, save it in Queue.plist /* We save all of the Queue data here */ [self saveQueueFileItem]; // so now lets move to currentQueueEncodeIndex++ ; [HBUtilities writeToActivityLog: "cancelling current job and stopping the queue"]; } - (IBAction) Pause: (id) sender { if (self.queueCore.state == HBStatePaused) { [self.queueCore resume]; } else { [self.queueCore pause]; } } #pragma mark - #pragma mark Batch Queue Titles Methods - (IBAction) addAllTitlesToQueue: (id) sender { NSAlert *alert = [NSAlert alertWithMessageText:@"You are about to add ALL titles to the queue!" defaultButton:@"Cancel" alternateButton:@"Yes, I want to add all titles to the queue" otherButton:nil informativeTextWithFormat:@"Current preset will be applied to all %ld titles. Are you sure you want to do this?", (long)[fSrcTitlePopUp numberOfItems]]; [alert setAlertStyle:NSCriticalAlertStyle]; [alert beginSheetModalForWindow:fWindow modalDelegate:self didEndSelector:@selector( addAllTitlesToQueueAlertDone:returnCode:contextInfo: ) contextInfo:NULL]; } - (void) addAllTitlesToQueueAlertDone: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (void *) contextInfo { if( returnCode == NSAlertAlternateReturn ) [self doAddAllTitlesToQueue]; } - (void) doAddAllTitlesToQueue { // first get the currently selected index so we can choose it again after cycling through the available titles. NSInteger currentlySelectedTitle = [fSrcTitlePopUp indexOfSelectedItem]; /* For each title in the fSrcTitlePopUp, select it */ for (int i = 0; i < [fSrcTitlePopUp numberOfItems]; i++) { [fSrcTitlePopUp selectItemAtIndex:i]; // Now call titlePopUpChanged to load it up [self titlePopUpChanged:nil]; // now add the title to the queue [self addToQueue:nil]; } // Now that we are done, reselect the previously selected title. [fSrcTitlePopUp selectItemAtIndex: currentlySelectedTitle]; // Now call titlePopUpChanged to load it up [self titlePopUpChanged:nil]; } #pragma mark - #pragma mark GUI Controls Changed Methods - (void)updateFileName { if (!self.job) { return; } HBTitle *title = self.job.title; // Generate a new file name NSString *fileName = [HBUtilities automaticNameForSource:title.name title:title.hb_title->index chapters:NSMakeRange(self.job.range.chapterStart + 1, self.job.range.chapterStop + 1) quality:self.job.video.qualityType ? self.job.video.quality : 0 bitrate:!self.job.video.qualityType ? self.job.video.avgBitrate : 0 videoCodec:self.job.video.encoder]; // Swap the old one with the new one self.job.destURL = [[self.job.destURL URLByDeletingLastPathComponent] URLByAppendingPathComponent: [NSString stringWithFormat:@"%@.%@", fileName, self.job.destURL.pathExtension]]; } - (IBAction) titlePopUpChanged: (id) sender { // If there is already a title load, save the current settings to a preset if (self.job) { self.selectedPreset = [self createPresetFromCurrentSettings]; } HBTitle *title = self.core.titles[fSrcTitlePopUp.indexOfSelectedItem]; // Check if we are reapplying a job from the queue, or creating a new one if (self.jobFromQueue) { self.jobFromQueue.title = title; self.job = self.jobFromQueue; } else { self.job = [[[HBJob alloc] initWithTitle:title andPreset:self.selectedPreset] autorelease]; // Check to see if the last destination has been set,use if so, if not, use Desktop NSURL *destURL = [[NSUserDefaults standardUserDefaults] URLForKey:@"HBLastDestinationDirectory"]; if (!destURL || ![[NSFileManager defaultManager] fileExistsAtPath:destURL.path]) { destURL = [NSURL fileURLWithPath:[NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES) firstObject] isDirectory:YES]; } destURL = [destURL URLByAppendingPathComponent:title.name]; // use the correct extension based on the container const char *ext = hb_container_get_default_extension(self.job.container); destURL = [destURL URLByAppendingPathExtension:@(ext)]; self.job.destURL = destURL; // set m4v extension if necessary - do not override user-specified .mp4 extension if (self.job.container & HB_MUX_MASK_MP4) { [self autoSetM4vExtension:nil]; } } // If we are a stream type and a batch scan, grok the output file name from title->name upon title change if ((title.hb_title->type == HB_STREAM_TYPE || title.hb_title->type == HB_FF_STREAM_TYPE) && self.core.titles.count > 1) { // Change the source to read out the parent folder also fSrcDVD2Field.stringValue = [NSString stringWithFormat:@"%@/%@", self.browsedSourceDisplayName, title.name]; } // apply the current preset if (!self.jobFromQueue) { [self applyPreset:self.selectedPreset]; } } - (void)chapterPopUpChanged:(NSNotification *)notification { // We're changing the chapter range - we may need to flip the m4v/mp4 extension if ([[fDstFormatPopUp selectedItem] tag] & HB_MUX_MASK_MP4) { [self autoSetM4vExtension:notification]; } // If Auto Naming is on it might need to be update if it includes the chapters range if ([[NSUserDefaults standardUserDefaults] boolForKey:@"DefaultAutoNaming"]) { [self updateFileName]; } } - (void)formatChanged:(NSNotification *)notification { if (self.job) { int videoContainer = self.job.container; // set the file extension const char *ext = hb_container_get_default_extension(videoContainer); self.job.destURL = [[self.job.destURL URLByDeletingPathExtension] URLByAppendingPathExtension:@(ext)]; if (videoContainer & HB_MUX_MASK_MP4) { [self autoSetM4vExtension:notification]; } } } - (void) autoSetM4vExtension:(NSNotification *)notification { if (!(self.job.container & HB_MUX_MASK_MP4)) return; NSString *extension = @"mp4"; BOOL anyCodecAC3 = [fAudioController anyCodecMatches: HB_ACODEC_AC3] || [fAudioController anyCodecMatches: HB_ACODEC_AC3_PASS]; // Chapter markers are enabled if the checkbox is ticked and we are doing p2p or we have > 1 chapter BOOL chapterMarkers = (self.job.chaptersEnabled) && (self.job.range.type != HBRangeTypeChapters || self.job.range.chapterStart < self.job.range.chapterStop); if ([[[NSUserDefaults standardUserDefaults] objectForKey:@"DefaultMpegExtension"] isEqualToString: @".m4v"] || ((YES == anyCodecAC3 || YES == chapterMarkers) && [[[NSUserDefaults standardUserDefaults] objectForKey:@"DefaultMpegExtension"] isEqualToString: @"Auto"])) { extension = @"m4v"; } if ([extension isEqualTo:self.job.destURL.pathExtension]) { return; } else { self.job.destURL = [[self.job.destURL URLByDeletingPathExtension] URLByAppendingPathExtension:extension]; } } /** * Method to determine if we should change the UI * To reflect whether or not a Preset is being used or if * the user is using "Custom" settings by determining the sender */ - (void)customSettingUsed { // Deselect the currently selected Preset if there is one [fPresetsView deselect]; // Change UI to show "Custom" settings are being used self.job.presetName = NSLocalizedString(@"Custom", @""); // If Auto Naming is on it might need to be update if it includes the quality token if ([[NSUserDefaults standardUserDefaults] boolForKey:@"DefaultAutoNaming"]) { [self updateFileName]; } } #pragma mark - #pragma mark - Picture /** * Registers changes made in the Picture Settings Window. */ - (void)pictureSettingsDidChange { [fPreviewController reloadPreviews]; [self customSettingUsed]; } #pragma mark - #pragma mark Open New Windows - (IBAction) openHomepage: (id) sender { [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:@"http://handbrake.fr/"]]; } - (IBAction) openForums: (id) sender { [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:@"http://forum.handbrake.fr/"]]; } - (IBAction) openUserGuide: (id) sender { [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:@"http://trac.handbrake.fr/wiki/HandBrakeGuide"]]; } /** * Shows debug output window. */ - (IBAction)showDebugOutputPanel:(id)sender { [outputPanel showOutputPanel:sender]; } /** * Shows preferences window. */ - (IBAction) showPreferencesWindow: (id) sender { if (fPreferencesController == nil) { fPreferencesController = [[HBPreferencesController alloc] init]; } NSWindow *window = [fPreferencesController window]; if (![window isVisible]) [window center]; [window makeKeyAndOrderFront: nil]; } /** * Shows queue window. */ - (IBAction) showQueueWindow:(id)sender { [fQueueController showQueueWindow:sender]; } - (IBAction) toggleDrawer:(id)sender { if ([fPresetDrawer state] == NSDrawerClosedState) { [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"HBDefaultPresetsDrawerShow"]; } else { [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"HBDefaultPresetsDrawerShow"]; } [fPresetDrawer toggle:self]; } /** * Shows Picture Settings Window. */ - (IBAction) showPicturePanel: (id) sender { [fPictureController showPictureWindow]; } - (IBAction) showPreviewWindow: (id) sender { [fPreviewController showWindow:sender]; } #pragma mark - Preset Methods - (void)applyPreset:(HBPreset *)preset { if (preset != nil && self.job) { self.selectedPreset = preset; // Apply the preset to the current job [self.job applyPreset:preset]; // Have to apply the presets the audio and subtitles controllers too // because they haven't been moved to HBJob yer. NSDictionary *chosenPreset = preset.content; [fAudioController applySettingsFromPreset: chosenPreset]; [fSubtitlesViewController applySettingsFromPreset:chosenPreset]; // If Auto Naming is on. We create an output filename of dvd name - title number if ([[NSUserDefaults standardUserDefaults] boolForKey:@"DefaultAutoNaming"]) { [self updateFileName]; } [fPreviewController reloadPreviews]; } } #pragma mark - Presets View Controller Delegate - (void)selectionDidChange { [self applyPreset:fPresetsView.selectedPreset]; } #pragma mark - #pragma mark Manage Presets - (void) checkBuiltInsForUpdates { /* if we have built in presets to update, then do so AlertBuiltInPresetUpdate*/ if ([presetManager checkBuiltInsForUpdates]) { if( [[NSUserDefaults standardUserDefaults] boolForKey:@"AlertBuiltInPresetUpdate"] == YES) { /* Show an alert window that built in presets will be updated */ /*On Screen Notification*/ [NSApp requestUserAttention:NSCriticalRequest]; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"HandBrake has determined your built in presets are out of date…"]; [alert setInformativeText:@"HandBrake will now update your built-in presets."]; [alert runModal]; [alert release]; } /* when alert is dismissed, go ahead and update the built in presets */ [presetManager generateBuiltInPresets]; } } - (IBAction)showAddPresetPanel:(id)sender { /* Show the add panel */ HBAddPresetController *addPresetController = [[HBAddPresetController alloc] initWithPreset:[self createPresetFromCurrentSettings] videoSize:NSMakeSize(self.job.picture.width, self.job.picture.height)]; [NSApp beginSheet:addPresetController.window modalForWindow:fWindow modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:addPresetController]; } - (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { HBAddPresetController *addPresetController = (HBAddPresetController *)contextInfo; if (returnCode == NSModalResponseContinue) { [presetManager addPreset:addPresetController.preset]; } [addPresetController release]; } - (HBPreset *)createPresetFromCurrentSettings { NSMutableDictionary *preset = [NSMutableDictionary dictionary]; NSDictionary *currentPreset = self.selectedPreset.content; preset[@"PresetBuildNumber"] = [NSString stringWithFormat: @"%d", [[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"] intValue]]; preset[@"PresetName"] = self.job.presetName; preset[@"Folder"] = @NO; // Set whether or not this is a user preset or factory 0 is factory, 1 is user preset[@"Type"] = @1; preset[@"Default"] = @0; // Get the whether or not to apply pic Size and Cropping (includes Anamorphic) preset[@"UsesPictureSettings"] = currentPreset[@"UsesPictureSettings"]; // Get whether or not to use the current Picture Filter settings for the preset preset[@"UsesPictureFilters"] = currentPreset[@"UsesPictureFilters"]; preset[@"PictureWidth"] = currentPreset[@"PictureWidth"]; preset[@"PictureHeight"] = currentPreset[@"PictureHeight"]; preset[@"PresetDescription"] = currentPreset[@"PresetDescription"]; [self.job applyCurrentSettingsToPreset:preset]; return [[[HBPreset alloc] initWithName:preset[@"PresetName"] content:preset builtIn:NO] autorelease]; } #pragma mark - #pragma mark Import Export Preset(s) - (IBAction) browseExportPresetFile: (id) sender { /* Open a panel to let the user choose where and how to save the export file */ NSSavePanel *panel = [NSSavePanel savePanel]; /* We get the current file name and path from the destination field here */ NSString *defaultExportDirectory = [NSString stringWithFormat: @"%@/Desktop/", NSHomeDirectory()]; [panel setDirectoryURL:[NSURL fileURLWithPath:defaultExportDirectory]]; [panel setNameFieldStringValue:@"HB_Export.plist"]; [panel beginSheetModalForWindow:fWindow completionHandler:^(NSInteger result) { if( result == NSOKButton ) { NSURL *exportPresetsFile = [panel URL]; NSURL *presetExportDirectory = [exportPresetsFile URLByDeletingLastPathComponent]; [[NSUserDefaults standardUserDefaults] setURL:presetExportDirectory forKey:@"LastPresetExportDirectoryURL"]; /* We check for the presets.plist */ if ([[NSFileManager defaultManager] fileExistsAtPath:[exportPresetsFile path]] == 0) { [[NSFileManager defaultManager] createFileAtPath:[exportPresetsFile path] contents:nil attributes:nil]; } NSMutableArray *presetsToExport = [[[NSMutableArray alloc] initWithContentsOfURL:exportPresetsFile] autorelease]; if (presetsToExport == nil) { presetsToExport = [[NSMutableArray alloc] init]; /* now get and add selected presets to export */ } if (fPresetsView.selectedPreset != nil) { [presetsToExport addObject:[fPresetsView.selectedPreset content]]; [presetsToExport writeToURL:exportPresetsFile atomically:YES]; } } }]; } - (IBAction)browseImportPresetFile:(id)sender { NSOpenPanel *panel; panel = [NSOpenPanel openPanel]; [panel setAllowsMultipleSelection:NO]; [panel setCanChooseFiles:YES]; [panel setCanChooseDirectories:NO]; [panel setAllowedFileTypes:@[@"plist", @"xml"]]; NSURL *sourceDirectory; if ([[NSUserDefaults standardUserDefaults] URLForKey:@"LastPresetImportDirectoryURL"]) { sourceDirectory = [[NSUserDefaults standardUserDefaults] URLForKey:@"LastPresetImportDirectoryURL"]; } else { sourceDirectory = [[NSURL fileURLWithPath:NSHomeDirectory()] URLByAppendingPathComponent:@"Desktop"]; } /* we open up the browse sources sheet here and call for browseSourcesDone after the sheet is closed * to evaluate whether we want to specify a title, we pass the sender in the contextInfo variable */ /* set this for allowed file types, not sure if we should allow xml or not */ [panel setDirectoryURL:sourceDirectory]; [panel beginSheetModalForWindow:fWindow completionHandler:^(NSInteger result) { NSURL *importPresetsFile = [panel URL]; NSURL *importPresetsDirectory = nil;//[importPresetsFile URLByDeletingLastPathComponent]; [[NSUserDefaults standardUserDefaults] setURL:importPresetsDirectory forKey:@"LastPresetImportDirectoryURL"]; /* NOTE: here we need to do some sanity checking to verify we do not hose up our presets file */ NSMutableArray *presetsToImport = [[NSMutableArray alloc] initWithContentsOfURL:importPresetsFile]; /* iterate though the new array of presets to import and add them to our presets array */ for (NSMutableDictionary *tempObject in presetsToImport) { /* make any changes to the incoming preset we see fit */ /* make sure the incoming preset is not tagged as default */ [tempObject setObject:[NSNumber numberWithInt:0] forKey:@"Default"]; /* prepend "(imported) to the name of the incoming preset for clarification since it can be changed */ NSString *prependedName = [@"(import) " stringByAppendingString:[tempObject objectForKey:@"PresetName"]] ; [tempObject setObject:prependedName forKey:@"PresetName"]; /* actually add the new preset to our presets array */ [presetManager addPresetFromDictionary:tempObject]; } [presetsToImport autorelease]; }]; } #pragma mark - #pragma mark Preset Menu - (IBAction)selectDefaultPreset:(id)sender { [self applyPreset:presetManager.defaultPreset]; [fPresetsView setSelection:_selectedPreset]; } - (IBAction)insertFolder:(id)sender { [fPresetsView insertFolder:sender]; } - (IBAction)selectPresetFromMenu:(id)sender { // Retrieve the preset stored in the NSMenuItem HBPreset *preset = [sender representedObject]; [self applyPreset:preset]; [fPresetsView setSelection:preset]; } /** * Adds the presets list to the menu. */ - (void)buildPresetsMenu { // First we remove all the preset menu items // inserted previosly NSArray *menuItems = [presetsMenu.itemArray copy]; for (NSMenuItem *item in menuItems) { if (item.tag != -1) { [presetsMenu removeItem:item]; } } [menuItems release]; __block NSUInteger i = 0; __block BOOL builtInEnded = NO; [presetManager.root enumerateObjectsUsingBlock:^(id obj, NSIndexPath *idx, BOOL *stop) { if (idx.length) { NSMenuItem *item = [[NSMenuItem alloc] init]; item.title = [obj name]; item.tag = i++; // Set an action only to the actual presets, // not on the folders. if ([obj isLeaf]) { item.action = @selector(selectPresetFromMenu:); item.representedObject = obj; } // Make the default preset font bold. if ([obj isDefault]) { NSAttributedString *newTitle = [[NSAttributedString alloc] initWithString:[obj name] attributes:@{NSFontAttributeName: [NSFont boldSystemFontOfSize:14]}]; [item setAttributedTitle:newTitle]; [newTitle release]; } // Add a separator line after the last builtIn preset if ([obj isBuiltIn] == NO && builtInEnded == NO) { [presetsMenu addItem:[NSMenuItem separatorItem]]; builtInEnded = YES; } item.indentationLevel = idx.length - 1; [presetsMenu addItem:item]; [item release]; } }]; } /** * We use this method to recreate new, updated factory presets */ - (IBAction)addFactoryPresets:(id)sender { [presetManager generateBuiltInPresets]; } @end