1/* $Id: Controller.mm,v 1.79 2005/11/04 19:41:32 titer Exp $ 2 3 This file is part of the HandBrake source code. 4 Homepage: <http://handbrake.fr/>. 5 It may be used under the terms of the GNU General Public License. */ 6 7#import "HBController.h" 8#import "HBAppDelegate.h" 9#import "HBFocusRingView.h" 10#import "HBToolbarBadgedItem.h" 11#import "HBQueueController.h" 12#import "HBTitleSelectionController.h" 13#import "NSWindow+HBAdditions.h" 14 15#import "HBQueue.h" 16#import "HBQueueWorker.h" 17 18#import "HBPresetsMenuBuilder.h" 19 20#import "HBSummaryViewController.h" 21#import "HBPictureViewController.h" 22#import "HBFiltersViewController.h" 23#import "HBVideoController.h" 24#import "HBAudioController.h" 25#import "HBSubtitlesController.h" 26#import "HBChapterTitlesController.h" 27 28#import "HBPreviewController.h" 29#import "HBPreviewGenerator.h" 30 31#import "HBPresetsViewController.h" 32#import "HBAddPresetController.h" 33#import "HBRenamePresetController.h" 34 35#import "HBAutoNamer.h" 36#import "HBJob+HBAdditions.h" 37#import "HBAttributedStringAdditions.h" 38 39#import "HBPreferencesKeys.h" 40 41static void *HBControllerScanCoreContext = &HBControllerScanCoreContext; 42static void *HBControllerLogLevelContext = &HBControllerLogLevelContext; 43 44@interface HBController () <HBPresetsViewControllerDelegate, HBTitleSelectionDelegate, NSMenuItemValidation, NSDraggingDestination, NSPopoverDelegate> 45 46@property (nonatomic, readonly, strong) HBCore *core; 47@property (nonatomic, readonly, strong) HBAppDelegate *delegate; 48 49@property (nonatomic, weak) IBOutlet NSTextField *sourceLabel; 50@property (nonatomic, weak) IBOutlet NSPopUpButton *titlePopUp; 51 52@property (nonatomic, strong) IBOutlet NSLayoutConstraint *bottomConstrain; 53@property (nonatomic, readwrite) NSColor *labelColor; 54 55/// Whether the window is visible or occluded, 56/// useful to avoid updating the UI needlessly 57@property (nonatomic) BOOL visible; 58@property (nonatomic) BOOL suppressCopyProtectionWarning; 59 60#pragma mark - Scan UI 61 62@property (nonatomic, weak) IBOutlet NSProgressIndicator *scanIndicator; 63@property (nonatomic, weak) IBOutlet NSBox *scanHorizontalLine; 64 65#pragma mark - Controllers 66 67@property (nonatomic, readonly, strong) HBSummaryViewController *summaryController; 68@property (nonatomic, readonly, strong) HBPictureViewController *pictureViewController; 69@property (nonatomic, readonly, strong) HBFiltersViewController *filtersViewController; 70@property (nonatomic, readonly, strong) HBVideoController *videoController; 71@property (nonatomic, readonly, strong) HBAudioController *audioController; 72@property (nonatomic, readonly, strong) HBSubtitlesController *subtitlesViewController; 73@property (nonatomic, readonly, strong) HBChapterTitlesController *chapterTitlesController; 74 75@property (nonatomic, strong) IBOutlet NSTabView *mainTabView; 76@property (nonatomic, strong) IBOutlet NSTabViewItem *summaryTab; 77@property (nonatomic, strong) IBOutlet NSTabViewItem *pictureTab; 78@property (nonatomic, strong) IBOutlet NSTabViewItem *filtersTab; 79@property (nonatomic, strong) IBOutlet NSTabViewItem *videoTab; 80@property (nonatomic, strong) IBOutlet NSTabViewItem *audioTab; 81@property (nonatomic, strong) IBOutlet NSTabViewItem *subtitlesTab; 82@property (nonatomic, strong) IBOutlet NSTabViewItem *chaptersTab; 83 84@property (nonatomic, readonly, strong) HBPreviewController *previewController; 85@property (nonatomic, strong) HBTitleSelectionController *titlesSelectionController; 86 87#pragma mark - Presets 88 89@property (nonatomic, readonly, strong) HBPresetsManager *presetManager; 90@property (nonatomic, readonly, strong) HBPresetsMenuBuilder *presetsMenuBuilder; 91@property (nonatomic, readonly, strong) HBPresetsViewController *presetView; 92 93@property (nonatomic, readonly, strong) NSPopover *presetsPopover; 94@property (nonatomic, strong) IBOutlet NSPopUpButton *presetsPopup; 95 96@property (nonatomic, nullable, strong) HBPreset *selectedPreset; 97@property (nonatomic, strong) HBPreset *currentPreset; 98 99#pragma mark - Open panel accessory view 100 101@property (nonatomic, weak) IBOutlet NSView *openTitleView; 102@property (nonatomic) BOOL scanSpecificTitle; 103@property (nonatomic) NSInteger scanSpecificTitleIdx; 104 105#pragma mark - Job 106 107@property (nonatomic, strong) NSURL *destinationURL; 108 109@property (nonatomic, nullable) HBJob *job; 110@property (nonatomic, nullable) HBAutoNamer *autoNamer; 111 112#pragma mark - Queue 113 114@property (nonatomic, readonly, weak) HBQueue *queue; 115@property (nonatomic) id observerToken; 116 117#define WINDOW_HEIGHT_OFFSET 30 118@property (nonatomic) IBOutlet NSTextField *statusField; 119@property (nonatomic) IBOutlet NSTextField *progressField; 120@property (nonatomic, copy) NSString *progress; 121 122#pragma mark - Toolbar 123 124@property (nonatomic) IBOutlet NSToolbarItem *openSourceToolbarItem; 125@property (nonatomic) IBOutlet NSToolbarItem *ripToolbarItem; 126@property (nonatomic) IBOutlet NSToolbarItem *pauseToolbarItem; 127@property (nonatomic) IBOutlet NSToolbarItem *presetsItem; 128 129@property (nonatomic, weak) IBOutlet HBToolbarBadgedItem *showQueueToolbarItem; 130 131@end 132 133@interface HBController (TouchBar) <NSTouchBarProvider, NSTouchBarDelegate> 134- (void)_touchBar_updateButtonsStateForScanCore:(HBState)state; 135- (void)_touchBar_updateQueueButtonsState; 136- (void)_touchBar_validateUserInterfaceItems; 137@end 138 139@implementation HBController 140 141- (instancetype)initWithDelegate:(HBAppDelegate *)delegate queue:(HBQueue *)queue presetsManager:(HBPresetsManager *)manager 142{ 143 self = [super initWithWindowNibName:@"MainWindow"]; 144 if (self) 145 { 146 // Init libhb 147 NSInteger loggingLevel = [NSUserDefaults.standardUserDefaults integerForKey:HBLoggingLevel]; 148 _core = [[HBCore alloc] initWithLogLevel:loggingLevel name:@"ScanCore"]; 149 150 // Inits the controllers 151 _previewController = [[HBPreviewController alloc] init]; 152 _previewController.documentController = self; 153 154 _delegate = delegate; 155 _queue = queue; 156 157 _presetManager = manager; 158 _selectedPreset = manager.defaultPreset; 159 _currentPreset = manager.defaultPreset; 160 161 _scanSpecificTitleIdx = 1; 162 _progress = @""; 163 164 // Check to see if the last destination has been set, use if so, if not, use Movies 165#ifdef __SANDBOX_ENABLED__ 166 NSData *bookmark = [NSUserDefaults.standardUserDefaults objectForKey:HBLastDestinationDirectoryBookmark]; 167 if (bookmark) 168 { 169 _destinationURL = [HBUtilities URLFromBookmark:bookmark]; 170 } 171#else 172 _destinationURL = [NSUserDefaults.standardUserDefaults URLForKey:HBLastDestinationDirectoryURL]; 173#endif 174 if (!_destinationURL || [NSFileManager.defaultManager fileExistsAtPath:_destinationURL.path isDirectory:nil] == NO) 175 { 176 _destinationURL = HBUtilities.defaultDestinationURL; 177 } 178 179#ifdef __SANDBOX_ENABLED__ 180 [_destinationURL startAccessingSecurityScopedResource]; 181#endif 182 } 183 184 return self; 185} 186 187- (void)windowDidLoad 188{ 189 if (@available (macOS 10.12, *)) 190 { 191 self.window.tabbingMode = NSWindowTabbingModeDisallowed; 192 } 193 194#if defined(__MAC_11_0) 195 if (@available (macOS 11, *)) 196 { 197 self.window.toolbarStyle = NSWindowToolbarStyleExpanded; 198 } 199#endif 200 201 [self enableUI:NO]; 202 203 // Bottom 204 self.statusField.stringValue = @""; 205 self.progressField.font = [NSFont monospacedDigitSystemFontOfSize:NSFont.smallSystemFontSize weight:NSFontWeightRegular]; 206 [self updateProgress]; 207 208 // Register HBController's Window as a receiver for files/folders drag & drop operations 209 [self.window registerForDraggedTypes:@[(NSString *)kUTTypeFileURL]]; 210 [self.mainTabView registerForDraggedTypes:@[(NSString *)kUTTypeFileURL]]; 211 212 _presetView = [[HBPresetsViewController alloc] initWithPresetManager:self.presetManager]; 213 _presetView.delegate = self; 214 215 // Set up the presets popover 216 _presetsPopover = [[NSPopover alloc] init]; 217 218 _presetsPopover.contentViewController = self.presetView; 219 _presetsPopover.contentSize = NSMakeSize(300, 580); 220 _presetsPopover.animates = YES; 221 222 // AppKit will close the popover when the user interacts with a user interface element outside the popover. 223 // note that interacting with menus or panels that become key only when needed will not cause a transient popover to close. 224 _presetsPopover.behavior = NSPopoverBehaviorSemitransient; 225 _presetsPopover.delegate = self; 226 227 [self.presetView view]; 228 229 // Setup the view controllers 230 _summaryController = [[HBSummaryViewController alloc] init]; 231 self.summaryTab.view = self.summaryController.view; 232 233 _pictureViewController = [[HBPictureViewController alloc] init]; 234 self.pictureTab.view = self.pictureViewController.view; 235 236 _filtersViewController = [[HBFiltersViewController alloc] init]; 237 self.filtersTab.view = self.filtersViewController.view; 238 239 _videoController = [[HBVideoController alloc] init]; 240 self.videoTab.view = self.videoController.view; 241 242 _audioController = [[HBAudioController alloc] init]; 243 self.audioTab.view = self.audioController.view; 244 245 _subtitlesViewController = [[HBSubtitlesController alloc] init]; 246 self.subtitlesTab.view = self.subtitlesViewController.view; 247 248 _chapterTitlesController = [[HBChapterTitlesController alloc] init]; 249 self.chaptersTab.view = self.chapterTitlesController.view; 250 251 // Add the observers 252 [self.core addObserver:self forKeyPath:@"state" 253 options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial 254 context:HBControllerScanCoreContext]; 255 256 [NSNotificationCenter.defaultCenter addObserverForName:HBQueueDidStartNotification 257 object:_queue queue:NSOperationQueue.mainQueue 258 usingBlock:^(NSNotification * _Nonnull note) { 259 self.bottomConstrain.animator.constant = 0; 260 }]; 261 262 [NSNotificationCenter.defaultCenter addObserverForName:HBQueueDidCompleteNotification 263 object:_queue queue:NSOperationQueue.mainQueue 264 usingBlock:^(NSNotification * _Nonnull note) { 265 self.bottomConstrain.animator.constant = -WINDOW_HEIGHT_OFFSET; 266 self.statusField.stringValue = @""; 267 self.progress = @""; 268 [self updateProgress]; 269 }]; 270 271 [NSNotificationCenter.defaultCenter addObserverForName:HBQueueDidStartItemNotification 272 object:_queue queue:NSOperationQueue.mainQueue 273 usingBlock:^(NSNotification * _Nonnull note) { [self setUpQueueObservers]; }]; 274 275 [NSNotificationCenter.defaultCenter addObserverForName:HBQueueDidCompleteItemNotification 276 object:_queue queue:NSOperationQueue.mainQueue 277 usingBlock:^(NSNotification * _Nonnull note) { [self setUpQueueObservers]; }]; 278 279 [NSNotificationCenter.defaultCenter addObserverForName:HBQueueDidChangeStateNotification 280 object:_queue queue:NSOperationQueue.mainQueue 281 usingBlock:^(NSNotification * _Nonnull note) { 282 [self updateQueueUI]; 283 }]; 284 285 [self updateQueueUI]; 286 287 // Presets menu 288 _presetsMenuBuilder = [[HBPresetsMenuBuilder alloc] initWithMenu:self.presetsPopup.menu 289 action:@selector(selectPresetFromMenu:) 290 size:[NSFont smallSystemFontSize] 291 presetsManager:self.presetManager]; 292 [self.presetsMenuBuilder build]; 293 294 // Log level 295 [NSUserDefaultsController.sharedUserDefaultsController addObserver:self forKeyPath:@"values.LoggingLevel" 296 options:0 context:HBControllerLogLevelContext]; 297 298 self.bottomConstrain.constant = -WINDOW_HEIGHT_OFFSET; 299 300 [self.window recalculateKeyViewLoop]; 301} 302 303#pragma mark - Drag & drop handling 304 305- (nullable NSArray<NSURL *> *)fileURLsFromPasteboard:(NSPasteboard *)pasteboard 306{ 307 NSDictionary *options = @{NSPasteboardURLReadingFileURLsOnlyKey: @YES}; 308 return [pasteboard readObjectsForClasses:@[[NSURL class]] options:options]; 309} 310 311- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender 312{ 313 NSArray<NSURL *> *fileURLs = [self fileURLsFromPasteboard:[sender draggingPasteboard]]; 314 [self.window.contentView setShowFocusRing:YES]; 315 return fileURLs.count ? NSDragOperationGeneric : NSDragOperationNone; 316} 317 318- (BOOL)prepareForDragOperation:(id<NSDraggingInfo>)sender 319{ 320 return YES; 321} 322 323- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender 324{ 325 NSArray<NSURL *> *fileURLs = [self fileURLsFromPasteboard:[sender draggingPasteboard]]; 326 327 if (fileURLs.count) 328 { 329 [self openURL:fileURLs.firstObject]; 330 } 331 332 [self.window.contentView setShowFocusRing:NO]; 333 return YES; 334} 335 336- (void)draggingExited:(nullable id <NSDraggingInfo>)sender 337{ 338 [self.window.contentView setShowFocusRing:NO]; 339} 340 341#pragma mark - KVO 342 343- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 344{ 345 if (context == HBControllerScanCoreContext) 346 { 347 HBState state = [change[NSKeyValueChangeNewKey] intValue]; 348 [self updateToolbarButtonsStateForScanCore:state]; 349 if (@available(macOS 10.12.2, *)) 350 { 351 [self _touchBar_updateButtonsStateForScanCore:state]; 352 [self _touchBar_validateUserInterfaceItems]; 353 } 354 } 355 else if (context == HBControllerLogLevelContext) 356 { 357 self.core.logLevel = [NSUserDefaults.standardUserDefaults integerForKey:HBLoggingLevel]; 358 } 359 else 360 { 361 [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 362 } 363} 364 365- (void)updateQueueUI 366{ 367 [self updateToolbarButtonsState]; 368 [self.window.toolbar validateVisibleItems]; 369 370 if (@available(macOS 10.12.2, *)) 371 { 372 [self _touchBar_updateQueueButtonsState]; 373 [self _touchBar_validateUserInterfaceItems]; 374 } 375 376 NSUInteger count = self.queue.pendingItemsCount; 377 self.showQueueToolbarItem.badgeValue = count ? @(count).stringValue : @""; 378} 379 380- (void)updateToolbarButtonsStateForScanCore:(HBState)state 381{ 382 if (state == HBStateIdle) 383 { 384 _openSourceToolbarItem.image = [NSImage imageNamed: @"source"]; 385 _openSourceToolbarItem.label = NSLocalizedString(@"Open Source", @"Toolbar Open/Cancel Item"); 386 _openSourceToolbarItem.toolTip = NSLocalizedString(@"Open Source", @"Toolbar Open/Cancel Item"); 387 } 388 else 389 { 390 _openSourceToolbarItem.image = [NSImage imageNamed: @"stopencode"]; 391 _openSourceToolbarItem.label = NSLocalizedString(@"Cancel Scan", @"Toolbar Open/Cancel Item"); 392 _openSourceToolbarItem.toolTip = NSLocalizedString(@"Cancel Scanning Source", @"Toolbar Open/Cancel Item"); 393 } 394} 395 396- (void)updateToolbarButtonsState 397{ 398 if (self.queue.canResume) 399 { 400 _pauseToolbarItem.image = [NSImage imageNamed: @"encode"]; 401 _pauseToolbarItem.label = NSLocalizedString(@"Resume", @"Toolbar Pause Item"); 402 _pauseToolbarItem.toolTip = NSLocalizedString(@"Resume Encoding", @"Toolbar Pause Item"); 403 } 404 else 405 { 406 _pauseToolbarItem.image = [NSImage imageNamed:@"pauseencode"]; 407 _pauseToolbarItem.label = NSLocalizedString(@"Pause", @"Toolbar Pause Item"); 408 _pauseToolbarItem.toolTip = NSLocalizedString(@"Pause Encoding", @"Toolbar Pause Item"); 409 410 } 411 if (self.queue.isEncoding) 412 { 413 _ripToolbarItem.image = [NSImage imageNamed:@"stopencode"]; 414 _ripToolbarItem.label = NSLocalizedString(@"Stop", @"Toolbar Start/Stop Item"); 415 _ripToolbarItem.toolTip = NSLocalizedString(@"Stop Encoding", @"Toolbar Start/Stop Item"); 416 } 417 else 418 { 419 _ripToolbarItem.image = [NSImage imageNamed: @"encode"]; 420 _ripToolbarItem.label = _queue.pendingItemsCount > 0 ? NSLocalizedString(@"Start Queue", @"Toolbar Start/Stop Item") : NSLocalizedString(@"Start", @"Toolbar Start/Stop Item"); 421 _ripToolbarItem.toolTip = NSLocalizedString(@"Start Encoding", @"Toolbar Start/Stop Item"); 422 } 423} 424 425- (void)enableUI:(BOOL)enabled 426{ 427 if (enabled) 428 { 429 self.labelColor = [NSColor controlTextColor]; 430 } 431 else 432 { 433 self.labelColor = [NSColor disabledControlTextColor]; 434 } 435 436 self.presetView.enabled = enabled; 437} 438 439- (void)setNilValueForKey:(NSString *)key 440{ 441 if ([key isEqualToString:@"scanSpecificTitleIdx"]) 442 { 443 [self setValue:@0 forKey:key]; 444 } 445} 446 447#pragma mark - Queue progress 448 449- (void)windowDidChangeOcclusionState:(NSNotification *)notification 450{ 451 if (self.window.occlusionState & NSWindowOcclusionStateVisible) 452 { 453 self.visible = YES; 454 [self updateProgress]; 455 } 456 else 457 { 458 self.visible = NO; 459 } 460} 461 462- (void)updateProgress 463{ 464 self.progressField.stringValue = self.progress; 465} 466 467- (void)setUpQueueObservers 468{ 469 [self removeQueueObservers]; 470 471 if (self->_queue.workingItemsCount > 1) 472 { 473 [self setUpForMultipleWorkers]; 474 } 475 else if (self->_queue.workingItemsCount == 1) 476 { 477 [self setUpForSingleWorker]; 478 } 479} 480 481- (void)setUpForMultipleWorkers 482{ 483 self.statusField.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Encoding %lu Jobs", @""), self.queue.workingItemsCount]; 484 self.progress = NSLocalizedString(@"Working", @""); 485 [self updateProgress]; 486} 487 488- (void)setUpForSingleWorker 489{ 490 HBQueueJobItem *firstWorkingItem = nil; 491 for (HBQueueJobItem *item in self.queue.items) 492 { 493 if (item.state == HBQueueItemStateWorking) 494 { 495 firstWorkingItem = item; 496 break; 497 } 498 } 499 500 if (firstWorkingItem) 501 { 502 HBQueueWorker *worker = [self.queue workerForItem:firstWorkingItem]; 503 504 if (worker) 505 { 506 self.observerToken = [NSNotificationCenter.defaultCenter addObserverForName:HBQueueWorkerProgressNotification 507 object:worker queue:NSOperationQueue.mainQueue 508 usingBlock:^(NSNotification * _Nonnull note) { 509 self.progress = note.userInfo[HBQueueWorkerProgressNotificationInfoKey]; 510 511 if (self->_visible) 512 { 513 [self updateProgress]; 514 } 515 }]; 516 } 517 } 518 519 self.statusField.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Encoding Job: %@", @""), firstWorkingItem.outputFileName]; 520} 521 522- (void)removeQueueObservers 523{ 524 if (self.observerToken) 525 { 526 [NSNotificationCenter.defaultCenter removeObserver:self.observerToken]; 527 self.observerToken = nil; 528 } 529} 530 531#pragma mark - UI Validation 532 533- (BOOL)validateUserIterfaceItemForAction:(SEL)action 534{ 535 if (self.core.state == HBStateScanning) 536 { 537 if (action == @selector(browseSources:)) 538 { 539 return YES; 540 } 541 if (action == @selector(toggleStartCancel:) || action == @selector(addToQueue:)) 542 { 543 return NO; 544 } 545 } 546 else if (action == @selector(browseSources:)) 547 { 548 return YES; 549 } 550 551 if (action == @selector(toggleStartCancel:)) 552 { 553 if (self.queue.isEncoding) 554 { 555 return YES; 556 } 557 else 558 { 559 return (self.job != nil || self.queue.canEncode); 560 } 561 } 562 563 if (action == @selector(togglePauseResume:)) { 564 return self.queue.canPause || self.queue.canResume; 565 } 566 567 if (action == @selector(addToQueue:)) 568 { 569 return (self.job != nil); 570 } 571 572 return YES; 573} 574 575- (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)anItem 576{ 577 return [self validateUserIterfaceItemForAction:anItem.action]; 578} 579 580- (BOOL)validateMenuItem:(NSMenuItem *)menuItem 581{ 582 SEL action = menuItem.action; 583 584 if (action == @selector(addToQueue:) || action == @selector(addAllTitlesToQueue:) || 585 action == @selector(addTitlesToQueue:) || action == @selector(showAddPresetPanel:)) 586 { 587 return self.job && self.window.attachedSheet == nil; 588 } 589 if (action == @selector(selectDefaultPreset:)) 590 { 591 return self.window.attachedSheet == nil; 592 } 593 if (action == @selector(togglePauseResume:)) 594 { 595 return [self.delegate validateMenuItem:menuItem]; 596 } 597 if (action == @selector(toggleStartCancel:)) 598 { 599 BOOL result = [self.delegate validateMenuItem:menuItem]; 600 601 if ([menuItem.title isEqualToString:NSLocalizedString(@"Start Encoding", @"Menu Start/Stop Item")]) 602 { 603 if (!result && self.job) 604 { 605 return YES; 606 } 607 } 608 609 return result; 610 } 611 if (action == @selector(browseSources:)) 612 { 613 if (self.core.state == HBStateScanning) { 614 return NO; 615 } 616 else 617 { 618 return self.window.attachedSheet == nil; 619 } 620 } 621 if (action == @selector(selectPresetFromMenu:)) 622 { 623 if ([menuItem.representedObject isEqualTo:self.selectedPreset]) 624 { 625 menuItem.state = NSControlStateValueOn; 626 } 627 else 628 { 629 menuItem.state = NSControlStateValueOff; 630 } 631 return (self.job != nil); 632 } 633 if (action == @selector(exportPreset:) || 634 action == @selector(selectDefaultPreset:)) 635 { 636 return self.job != nil; 637 } 638 if (action == @selector(deletePreset:) || 639 action == @selector(setDefaultPreset:)) 640 { 641 return self.job != nil && self.selectedPreset; 642 } 643 if (action == @selector(savePreset:)) 644 { 645 return self.job != nil && self.selectedPreset && self.selectedPreset.isBuiltIn == NO; 646 } 647 if (action == @selector(showRenamePresetPanel:)) 648 { 649 return self.selectedPreset && self.selectedPreset.isBuiltIn == NO; 650 } 651 if (action == @selector(switchToNextTitle:) || 652 action == @selector(switchToPreviousTitle:)) 653 { 654 return self.core.titles.count > 1 && self.job != nil; 655 } 656 657 return YES; 658} 659 660#pragma mark - Get New Source 661 662- (void)launchAction 663{ 664 if (self.core.state != HBStateScanning && !self.job) 665 { 666 if ([NSUserDefaults.standardUserDefaults boolForKey:HBShowOpenPanelAtLaunch]) 667 { 668 [self browseSources:self]; 669 } 670 } 671} 672 673- (NSModalResponse)runCopyProtectionAlert 674{ 675 NSAlert *alert = [[NSAlert alloc] init]; 676 [alert setMessageText:NSLocalizedString(@"Copy-Protected sources are not supported.", @"Copy Protection Alert -> message")]; 677 [alert setInformativeText:NSLocalizedString(@"Please note that HandBrake does not support the removal of copy-protection from DVD Discs. You can if you wish use any other 3rd party software for this function. This warning will be shown only once each time HandBrake is run.", @"Copy Protection Alert -> informative text")]; 678 [alert addButtonWithTitle:NSLocalizedString(@"Attempt Scan Anyway", @"Copy Protection Alert -> first button")]; 679 [alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"Copy Protection Alert -> second button")]; 680 681 [NSApp requestUserAttention:NSCriticalRequest]; 682 683 return [alert runModal]; 684} 685 686/** 687 * Here we actually tell hb_scan to perform the source scan, using the path to source and title number 688 */ 689- (void)scanURL:(NSURL *)fileURL titleIndex:(NSUInteger)index completionHandler:(void(^)(NSArray<HBTitle *> *titles))completionHandler 690{ 691 // Save the current settings 692 [self updateCurrentPreset]; 693 694 self.job = nil; 695 [self.titlePopUp removeAllItems]; 696 self.window.representedURL = nil; 697 self.window.title = NSLocalizedString(@"HandBrake", @"Main Window -> title"); 698 699 // Clear the undo manager, we can't undo this action 700 [self.window.undoManager removeAllActions]; 701 702 NSURL *mediaURL = [HBUtilities mediaURLFromURL:fileURL]; 703 704 NSError *outError = NULL; 705 706 // Check if we can scan the source and if there is any warning. 707 BOOL canScan = [self.core canScan:mediaURL error:&outError]; 708 709 // Notify the user that we don't support removal of copy protection. 710 if (canScan && outError.code == 101 && !self.suppressCopyProtectionWarning) 711 { 712 self.suppressCopyProtectionWarning = YES; 713 if ([self runCopyProtectionAlert] == NSAlertFirstButtonReturn) 714 { 715 // 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 716 [HBUtilities writeToActivityLog:"User overrode copy-protection warning - trying to open physical dvd without decryption"]; 717 } 718 else 719 { 720 // User chose to cancel the scan 721 [HBUtilities writeToActivityLog:"Cannot open physical dvd, scan canceled"]; 722 canScan = NO; 723 } 724 } 725 726 if (canScan) 727 { 728 NSUInteger hb_num_previews = [NSUserDefaults.standardUserDefaults integerForKey:HBPreviewsNumber]; 729 NSUInteger min_title_duration_seconds = [NSUserDefaults.standardUserDefaults integerForKey:HBMinTitleScanSeconds]; 730 731 [self.core scanURL:mediaURL 732 titleIndex:index 733 previews:hb_num_previews minDuration:min_title_duration_seconds keepPreviews:YES 734 progressHandler:^(HBState state, HBProgress progress, NSString *info) 735 { 736 self.sourceLabel.stringValue = info; 737 self.scanIndicator.hidden = NO; 738 self.scanHorizontalLine.hidden = YES; 739 self.scanIndicator.doubleValue = progress.percent; 740 } 741 completionHandler:^(HBCoreResult result) 742 { 743 self.scanHorizontalLine.hidden = NO; 744 self.scanIndicator.hidden = YES; 745 self.scanIndicator.indeterminate = NO; 746 self.scanIndicator.doubleValue = 0.0; 747 748 if (result == HBCoreResultDone) 749 { 750 for (HBTitle *title in self.core.titles) 751 { 752 [self.titlePopUp addItemWithTitle:title.description]; 753 } 754 self.window.representedURL = mediaURL; 755 self.window.title = mediaURL.lastPathComponent; 756 } 757 else 758 { 759 // We display a message if a valid source was not chosen 760 self.sourceLabel.stringValue = NSLocalizedString(@"No Valid Source Found", @"Main Window -> Info text"); 761 } 762 763 // Set the last searched source directory in the prefs here 764 if ([NSWorkspace.sharedWorkspace isFilePackageAtPath:mediaURL.URLByDeletingLastPathComponent.path]) 765 { 766 [NSUserDefaults.standardUserDefaults setURL:mediaURL.URLByDeletingLastPathComponent.URLByDeletingLastPathComponent forKey:HBLastSourceDirectoryURL]; 767 } 768 else 769 { 770 [NSUserDefaults.standardUserDefaults setURL:mediaURL.URLByDeletingLastPathComponent forKey:HBLastSourceDirectoryURL]; 771 } 772 773 completionHandler(self.core.titles); 774 775 // Clear the undo manager, the completion handle 776 // set the job in the main window 777 // and don't want to make it undoable 778 [self.window.undoManager removeAllActions]; 779 [self.window.toolbar validateVisibleItems]; 780 if (@available(macOS 10.12.2, *)) 781 { 782 [self _touchBar_validateUserInterfaceItems]; 783 } 784 }]; 785 } 786 else 787 { 788 completionHandler(@[]); 789 } 790} 791 792- (void)openURL:(NSURL *)fileURL titleIndex:(NSUInteger)index 793{ 794 [self showWindow:self]; 795 796 [self scanURL:fileURL titleIndex:index completionHandler:^(NSArray<HBTitle *> *titles) 797 { 798 if (titles.count) 799 { 800 [NSDocumentController.sharedDocumentController noteNewRecentDocumentURL:fileURL]; 801 802 HBTitle *featuredTitle = titles.firstObject; 803 for (HBTitle *title in titles) 804 { 805 if (title.isFeatured) 806 { 807 featuredTitle = title; 808 } 809 } 810 811 HBJob *job = [self jobFromTitle:featuredTitle]; 812 if (job) 813 { 814 self.job = job; 815 } 816 else 817 { 818 self.job = nil; 819 [self.titlePopUp removeAllItems]; 820 self.sourceLabel.stringValue = NSLocalizedString(@"No Valid Preset", @"Main Window -> Info text"); 821 } 822 } 823 }]; 824} 825 826- (void)openURL:(NSURL *)fileURL 827{ 828 if (self.core.state != HBStateScanning) 829 { 830 [self openURL:fileURL titleIndex:0]; 831 } 832} 833 834/** 835 * Rescans the a job back into the main window 836 */ 837- (void)openJob:(HBJob *)job completionHandler:(void (^)(BOOL result))handler 838{ 839 if (self.core.state != HBStateScanning) 840 { 841 [job refreshSecurityScopedResources]; 842 [self scanURL:job.fileURL titleIndex:job.titleIdx completionHandler:^(NSArray<HBTitle *> *titles) 843 { 844 if (titles.count) 845 { 846 // If the scan was cached, reselect 847 // the original title 848 for (HBTitle *title in titles) 849 { 850 if (title.index == job.titleIdx) 851 { 852 job.title = title; 853 break; 854 } 855 } 856 857 // Else just one title or a title specific rescan 858 // select the first title 859 if (!job.title) 860 { 861 job.title = titles.firstObject; 862 } 863 864 self.job = job; 865 job.undo = self.window.undoManager; 866 867 self.currentPreset = [self createPresetFromCurrentSettings]; 868 self.selectedPreset = nil; 869 870 handler(YES); 871 } 872 else 873 { 874 handler(NO); 875 } 876 }]; 877 } 878 else 879 { 880 handler(NO); 881 } 882} 883 884- (HBJob *)jobFromTitle:(HBTitle *)title 885{ 886 // If there is already a title load, save the current settings to a preset 887 // Save the current settings 888 [self updateCurrentPreset]; 889 890 HBJob *job = [[HBJob alloc] initWithTitle:title preset:self.currentPreset]; 891 if (job) 892 { 893 job.outputURL = self.destinationURL; 894 895 // If the source is not a stream, and autonaming is disabled, 896 // keep the existing file name. 897 if (self.job.outputFileName.length == 0 || title.isStream || [NSUserDefaults.standardUserDefaults boolForKey:HBDefaultAutoNaming]) 898 { 899 job.outputFileName = job.defaultName; 900 } 901 else 902 { 903 job.outputFileName = self.job.outputFileName; 904 } 905 906 job.undo = self.window.undoManager; 907 } 908 909 return job; 910} 911 912- (void)removeJobObservers 913{ 914 if (self.job) 915 { 916 NSNotificationCenter *center = NSNotificationCenter.defaultCenter; 917 [center removeObserver:self name:HBContainerChangedNotification object:_job]; 918 [center removeObserver:self name:HBPictureChangedNotification object:_job.picture]; 919 [center removeObserver:self name:HBFiltersChangedNotification object:_job.filters]; 920 [center removeObserver:self name:HBVideoChangedNotification object:_job.video]; 921 self.autoNamer = nil; 922 } 923} 924 925/** 926 * Observe the job settings changes. 927 * This is used to update the file name and extension 928 * and the custom preset string. 929 */ 930- (void)addJobObservers 931{ 932 if (self.job) 933 { 934 NSNotificationCenter *center = NSNotificationCenter.defaultCenter; 935 [center addObserver:self selector:@selector(formatChanged:) name:HBContainerChangedNotification object:_job]; 936 [center addObserver:self selector:@selector(customSettingUsed) name:HBPictureChangedNotification object:_job.picture]; 937 [center addObserver:self selector:@selector(customSettingUsed) name:HBFiltersChangedNotification object:_job.filters]; 938 [center addObserver:self selector:@selector(customSettingUsed) name:HBVideoChangedNotification object:_job.video]; 939 self.autoNamer = [[HBAutoNamer alloc] initWithJob:self.job]; 940 } 941} 942 943- (void)setJob:(HBJob *)job 944{ 945 if (job != _job) 946 { 947 [[self.window.undoManager prepareWithInvocationTarget:self] setJob:_job]; 948 } 949 950 [self removeJobObservers]; 951 _job = job; 952 953 // Set the jobs info to the view controllers 954 self.summaryController.job = job; 955 self.pictureViewController.picture = job.picture; 956 self.filtersViewController.filters = job.filters; 957 self.videoController.video = job.video; 958 self.audioController.audio = job.audio; 959 self.subtitlesViewController.subtitles = job.subtitles; 960 self.chapterTitlesController.job = job; 961 962 if (job) 963 { 964 HBPreviewGenerator *generator = [[HBPreviewGenerator alloc] initWithCore:self.core job:job]; 965 self.previewController.generator = generator; 966 self.summaryController.generator = generator; 967 968 HBTitle *title = job.title; 969 970 // Update the title selection popup. 971 [self.titlePopUp selectItemWithTitle:title.description]; 972 973 // Grok the output file name from title.name upon title change 974 if (title.isStream && self.core.titles.count > 1) 975 { 976 // Change the source to read out the parent folder also 977 self.sourceLabel.stringValue = [NSString stringWithFormat:@"%@/%@, %@", title.url.URLByDeletingLastPathComponent.lastPathComponent, 978 title.name, title.shortFormatDescription]; 979 } 980 else 981 { 982 self.sourceLabel.stringValue = [NSString stringWithFormat:@"%@, %@", title.name, title.shortFormatDescription]; 983 } 984 } 985 else 986 { 987 [self.previewController.generator invalidate]; 988 self.previewController.generator = nil; 989 self.summaryController.generator = nil; 990 } 991 self.previewController.picture = job.picture; 992 993 [self enableUI:(job != nil)]; 994 [self addJobObservers]; 995} 996 997/** 998 * Opens the source browse window, called from Open Source widgets 999 */ 1000- (IBAction)browseSources:(id)sender 1001{ 1002 if (self.core.state == HBStateScanning) 1003 { 1004 [self.core cancelScan]; 1005 return; 1006 } 1007 1008 NSOpenPanel *panel = [NSOpenPanel openPanel]; 1009 [panel setAllowsMultipleSelection:NO]; 1010 [panel setCanChooseFiles:YES]; 1011 [panel setCanChooseDirectories:YES]; 1012 1013 NSURL *sourceDirectory; 1014 if ([NSUserDefaults.standardUserDefaults URLForKey:HBLastSourceDirectoryURL]) 1015 { 1016 sourceDirectory = [NSUserDefaults.standardUserDefaults URLForKey:HBLastSourceDirectoryURL]; 1017 } 1018 else 1019 { 1020 sourceDirectory = [NSURL fileURLWithPath:[NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES) firstObject] 1021 isDirectory:YES]; 1022 } 1023 1024 panel.directoryURL = sourceDirectory; 1025 panel.accessoryView = self.openTitleView; 1026 panel.accessoryViewDisclosed = YES; 1027 1028 [panel beginSheetModalForWindow:self.window completionHandler: ^(NSInteger result) 1029 { 1030 if (result == NSModalResponseOK) 1031 { 1032 NSInteger titleIdx = self.scanSpecificTitle ? self.scanSpecificTitleIdx : 0; 1033 [self openURL:panel.URL titleIndex:titleIdx]; 1034 } 1035 }]; 1036} 1037 1038#pragma mark - GUI Controls Changed Methods 1039 1040- (IBAction)browseDestination:(id)sender 1041{ 1042 // Open a panel to let the user choose and update the text field 1043 NSOpenPanel *panel = [NSOpenPanel openPanel]; 1044 panel.canChooseFiles = NO; 1045 panel.canChooseDirectories = YES; 1046 panel.canCreateDirectories = YES; 1047 panel.prompt = NSLocalizedString(@"Choose", @"Main Window -> Destination open panel"); 1048 1049 if (self.job.outputURL) 1050 { 1051 panel.directoryURL = self.job.outputURL; 1052 } 1053 1054 [panel beginSheetModalForWindow:self.window completionHandler:^(NSInteger result) 1055 { 1056 if (result == NSModalResponseOK) 1057 { 1058 self.job.outputURL = panel.URL; 1059 self.destinationURL = panel.URL; 1060 1061 // Save this path to the prefs so that on next browse destination window it opens there 1062 [NSUserDefaults.standardUserDefaults setObject:[HBUtilities bookmarkFromURL:panel.URL] 1063 forKey:HBLastDestinationDirectoryBookmark]; 1064 [NSUserDefaults.standardUserDefaults setURL:panel.URL 1065 forKey:HBLastDestinationDirectoryURL]; 1066 1067 } 1068 }]; 1069} 1070 1071- (IBAction)titlePopUpChanged:(NSPopUpButton *)sender 1072{ 1073 HBTitle *title = self.core.titles[sender.indexOfSelectedItem]; 1074 HBJob *job = [self jobFromTitle:title]; 1075 self.job = job; 1076} 1077 1078- (void)formatChanged:(NSNotification *)notification 1079{ 1080 [self customSettingUsed]; 1081} 1082 1083/** 1084 * Method to determine if we should change the UI 1085 * To reflect whether or not a Preset is being used or if 1086 * the user is using "Custom" settings by determining the sender 1087 */ 1088- (void)customSettingUsed 1089{ 1090 // Update the preset and file name only if we are not 1091 // undoing or redoing, because if so it's already stored 1092 // in the undo manager. 1093 NSUndoManager *undo = self.window.undoManager; 1094 if (!(undo.isUndoing || undo.isRedoing)) 1095 { 1096 // Change UI to show "Custom" settings are being used 1097 if (![self.job.presetName hasSuffix:NSLocalizedString(@"(Modified)", @"Main Window -> preset modified")]) 1098 { 1099 self.job.presetName = [NSString stringWithFormat:@"%@ %@", self.job.presetName, NSLocalizedString(@"(Modified)", @"Main Window -> preset modified")]; 1100 } 1101 } 1102} 1103 1104- (IBAction)switchToNextTitle:(id)sender 1105{ 1106 NSArray<HBTitle *> *titles = self.core.titles; 1107 if (titles && self.job) 1108 { 1109 NSUInteger index = [titles indexOfObject:self.job.title]; 1110 if (index != NSNotFound && index < titles.count - 1) 1111 { 1112 HBTitle *title = titles[index + 1]; 1113 HBJob *job = [self jobFromTitle:title]; 1114 self.job = job; 1115 } 1116 } 1117} 1118 1119- (IBAction)switchToPreviousTitle:(id)sender 1120{ 1121 NSArray<HBTitle *> *titles = self.core.titles; 1122 if (titles && self.job) 1123 { 1124 NSUInteger index = [titles indexOfObject:self.job.title]; 1125 if (index != NSNotFound && index > 0) 1126 { 1127 HBTitle *title = titles[index - 1]; 1128 HBJob *job = [self jobFromTitle:title]; 1129 self.job = job; 1130 } 1131 } 1132} 1133 1134#pragma mark - Job Handling 1135 1136/** 1137 Check if the job destination if a valid one, 1138 if so, call the handler 1139 @param job the job 1140 @param completionHandler the block to call if the check is successful 1141 */ 1142- (void)runDestinationAlerts:(HBJob *)job completionHandler:(void (^ __nullable)(NSModalResponse returnCode))handler 1143{ 1144 if ([NSFileManager.defaultManager fileExistsAtPath:job.outputURL.path] == NO) 1145 { 1146 NSAlert *alert = [[NSAlert alloc] init]; 1147 [alert setMessageText:NSLocalizedString(@"Warning!", @"Invalid destination alert -> message")]; 1148 [alert setInformativeText:NSLocalizedString(@"This is not a valid destination directory!", @"Invalid destination alert -> informative text")]; 1149 [alert setAlertStyle:NSAlertStyleCritical]; 1150 [alert beginSheetModalForWindow:self.window completionHandler:handler]; 1151 } 1152 else if ([job.fileURL isEqual:job.completeOutputURL]|| 1153 [job.fileURL.absoluteString.lowercaseString isEqualToString:job.completeOutputURL.absoluteString.lowercaseString]) 1154 { 1155 NSAlert *alert = [[NSAlert alloc] init]; 1156 [alert setMessageText:NSLocalizedString(@"A file already exists at the selected destination.", @"Destination same as source alert -> message")]; 1157 [alert setInformativeText:NSLocalizedString(@"The destination is the same as the source, you can not overwrite your source file!", @"Destination same as source alert -> informative text")]; 1158 [alert setAlertStyle:NSAlertStyleCritical]; 1159 [alert beginSheetModalForWindow:self.window completionHandler:handler]; 1160 } 1161 else if ([NSFileManager.defaultManager fileExistsAtPath:job.completeOutputURL.path]) 1162 { 1163 NSAlert *alert = [[NSAlert alloc] init]; 1164 [alert setMessageText:NSLocalizedString(@"A file already exists at the selected destination.", @"File already exists alert -> message")]; 1165 [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(@"Do you want to overwrite %@?", @"File already exists alert -> informative text"), job.completeOutputURL.path]]; 1166 [alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"File already exists alert -> first button")]; 1167 [alert addButtonWithTitle:NSLocalizedString(@"Overwrite", @"File already exists alert -> second button")]; 1168#if defined(__MAC_11_0) 1169 if (@available(macOS 11, *)) 1170 { 1171 alert.buttons.lastObject.hasDestructiveAction = true; 1172 } 1173#endif 1174 [alert setAlertStyle:NSAlertStyleCritical]; 1175 1176 [alert beginSheetModalForWindow:self.window completionHandler:handler]; 1177 } 1178 else if ([_queue itemExistAtURL:job.completeOutputURL]) 1179 { 1180 NSAlert *alert = [[NSAlert alloc] init]; 1181 [alert setMessageText:NSLocalizedString(@"There is already a queue item for this destination.", @"File already exists in queue alert -> message")]; 1182 [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(@"Do you want to overwrite %@?", @"File already exists in queue alert -> informative text"), job.completeOutputURL.path]]; 1183 [alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"File already exists in queue alert -> first button")]; 1184 [alert addButtonWithTitle:NSLocalizedString(@"Overwrite", @"File already exists in queue alert -> second button")]; 1185#if defined(__MAC_11_0) 1186 if (@available(macOS 11, *)) 1187 { 1188 alert.buttons.lastObject.hasDestructiveAction = true; 1189 } 1190#endif 1191 [alert setAlertStyle:NSAlertStyleCritical]; 1192 1193 [alert beginSheetModalForWindow:self.window completionHandler:handler]; 1194 } 1195 else 1196 { 1197 handler(NSAlertSecondButtonReturn); 1198 } 1199} 1200 1201/** 1202 * Actually adds a job to the queue 1203 */ 1204- (void)doAddToQueue 1205{ 1206 [_queue addJob:[self.job copy]]; 1207} 1208 1209/** 1210 * Puts up an alert before ultimately calling doAddToQueue 1211 */ 1212- (IBAction)addToQueue:(id)sender 1213{ 1214 if ([self.window HB_endEditing]) 1215 { 1216 [self runDestinationAlerts:self.job completionHandler:^(NSModalResponse returnCode) { 1217 if (returnCode == NSAlertSecondButtonReturn) 1218 { 1219 [self doAddToQueue]; 1220 } 1221 }]; 1222 } 1223} 1224 1225- (void)doRip 1226{ 1227 // if there are no jobs in the queue, then add this one to the queue and rip 1228 // otherwise, just rip the queue 1229 if (_queue.pendingItemsCount == 0) 1230 { 1231 [self doAddToQueue]; 1232 } 1233 1234 [_delegate toggleStartCancel:self]; 1235} 1236 1237/** 1238 * Puts up an alert before ultimately calling doRip 1239 */ 1240- (IBAction)toggleStartCancel:(id)sender 1241{ 1242 // Rip or Cancel ? 1243 if (_queue.isEncoding || _queue.canEncode) 1244 { 1245 // Displays an alert asking user if the want to cancel encoding of current job. 1246 [_delegate toggleStartCancel:self]; 1247 } 1248 else 1249 { 1250 if ([self.window HB_endEditing]) 1251 { 1252 [self runDestinationAlerts:self.job completionHandler:^(NSModalResponse returnCode) { 1253 if (returnCode == NSAlertSecondButtonReturn) 1254 { 1255 [self doRip]; 1256 } 1257 }]; 1258 } 1259 } 1260} 1261 1262- (IBAction)togglePauseResume:(id)sender 1263{ 1264 [_delegate togglePauseResume:sender]; 1265} 1266 1267#pragma mark - 1268#pragma mark Batch Queue Titles Methods 1269 1270- (IBAction)addTitlesToQueue:(id)sender 1271{ 1272 [self.window HB_endEditing]; 1273 1274 self.titlesSelectionController = [[HBTitleSelectionController alloc] initWithTitles:self.core.titles 1275 presetName:self.job.presetName 1276 delegate:self]; 1277 1278 [self.window beginSheet:self.titlesSelectionController.window completionHandler:nil]; 1279} 1280 1281- (void)didSelectTitles:(NSArray<HBTitle *> *)titles 1282{ 1283 [self.window endSheet:self.titlesSelectionController.window]; 1284 1285 [self doAddTitlesToQueue:titles]; 1286} 1287 1288- (void)doAddTitlesToQueue:(NSArray<HBTitle *> *)titles 1289{ 1290 NSMutableArray<HBJob *> *jobs = [[NSMutableArray alloc] init]; 1291 BOOL fileExists = NO; 1292 BOOL fileOverwritesSource = NO; 1293 1294 // Get the preset from the loaded job. 1295 HBPreset *preset = [self createPresetFromCurrentSettings]; 1296 1297 for (HBTitle *title in titles) 1298 { 1299 HBJob *job = [[HBJob alloc] initWithTitle:title preset:preset]; 1300 job.outputURL = self.destinationURL; 1301 job.outputFileName = job.defaultName; 1302 job.title = nil; 1303 if (job) 1304 { 1305 [jobs addObject:job]; 1306 } 1307 } 1308 1309 NSMutableSet<NSURL *> *destinations = [[NSMutableSet alloc] init]; 1310 for (HBJob *job in jobs) 1311 { 1312 if ([destinations containsObject:job.completeOutputURL]) 1313 { 1314 fileExists = YES; 1315 break; 1316 } 1317 else 1318 { 1319 [destinations addObject:job.completeOutputURL]; 1320 } 1321 1322 if ([[NSFileManager defaultManager] fileExistsAtPath:job.completeOutputURL.path] || [_queue itemExistAtURL:job.completeOutputURL]) 1323 { 1324 fileExists = YES; 1325 break; 1326 } 1327 } 1328 1329 for (HBJob *job in jobs) 1330 { 1331 if ([job.fileURL isEqual:job.completeOutputURL]) { 1332 fileOverwritesSource = YES; 1333 break; 1334 } 1335 } 1336 1337 if (fileOverwritesSource) 1338 { 1339 NSAlert *alert = [[NSAlert alloc] init]; 1340 [alert setMessageText:NSLocalizedString(@"A file already exists at the selected destination.", @"Destination same as source alert -> message")]; 1341 [alert setInformativeText:NSLocalizedString(@"The destination is the same as the source, you can not overwrite your source file!", @"Destination same as source alert -> informative text")]; 1342 [alert beginSheetModalForWindow:self.window completionHandler:nil]; 1343 } 1344 else if (fileExists) 1345 { 1346 // File exist, warn user 1347 NSAlert *alert = [[NSAlert alloc] init]; 1348 [alert setMessageText:NSLocalizedString(@"File already exists.", @"File already exists alert -> message")]; 1349 [alert setInformativeText:NSLocalizedString(@"One or more file already exists. Do you want to overwrite?", @"File already exists alert -> informative text")]; 1350 [alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"File already exists alert -> first button")]; 1351 [alert addButtonWithTitle:NSLocalizedString(@"Overwrite", @"File already exists alert -> second button")]; 1352#if defined(__MAC_11_0) 1353 if (@available(macOS 11, *)) 1354 { 1355 alert.buttons.lastObject.hasDestructiveAction = true; 1356 } 1357#endif 1358 [alert setAlertStyle:NSAlertStyleCritical]; 1359 1360 [alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) { 1361 if (returnCode == NSAlertSecondButtonReturn) 1362 { 1363 [self->_queue addJobs:jobs]; 1364 } 1365 }]; 1366 } 1367 else 1368 { 1369 [_queue addJobs:jobs]; 1370 } 1371} 1372 1373- (IBAction)addAllTitlesToQueue:(id)sender 1374{ 1375 [self doAddTitlesToQueue:self.core.titles]; 1376} 1377 1378#pragma mark - Picture 1379 1380- (IBAction)showPreviewWindow:(id)sender 1381{ 1382 [self.previewController showWindow:sender]; 1383} 1384 1385- (IBAction)showTabView:(id)sender 1386{ 1387 NSInteger tag = [sender tag]; 1388 [self.mainTabView selectTabViewItemAtIndex:tag]; 1389} 1390 1391#pragma mark - Presets View Controller Delegate 1392 1393- (void)selectionDidChange 1394{ 1395 if (self.job) 1396 { 1397 BOOL success = [self doApplyPreset:self.presetView.selectedPreset]; 1398 if (success == YES) 1399 { 1400 self.selectedPreset = self.presetView.selectedPreset; 1401 } 1402 } 1403 else 1404 { 1405 self.currentPreset = self.presetView.selectedPreset; 1406 self.selectedPreset = self.presetView.selectedPreset; 1407 [self.window.undoManager removeAllActions]; 1408 } 1409} 1410 1411#pragma mark - Presets 1412 1413- (BOOL)popoverShouldDetach:(NSPopover *)popover 1414{ 1415 if (popover == self.presetsPopover) { 1416 return YES; 1417 } 1418 1419 return NO; 1420} 1421 1422- (IBAction)togglePresets:(id)sender 1423{ 1424 if (self.presetsPopover) 1425 { 1426 if (!self.presetsPopover.isShown) 1427 { 1428 NSView *target = [sender isKindOfClass:[NSView class]] ? (NSView *)sender : self.presetsItem.view.window ? self.presetsItem.view : self.window.contentView; 1429 [self.presetsPopover showRelativeToRect:target.bounds ofView:target preferredEdge:NSMaxYEdge]; 1430 } 1431 else 1432 { 1433 [self.presetsPopover close]; 1434 } 1435 } 1436} 1437 1438- (void)setSelectedPreset:(HBPreset *)selectedPreset 1439{ 1440 if (selectedPreset != _selectedPreset) 1441 { 1442 [[self.window.undoManager prepareWithInvocationTarget:self] setSelectedPreset:_selectedPreset]; 1443 _selectedPreset = selectedPreset; 1444 } 1445} 1446 1447- (void)setCurrentPreset:(HBPreset *)currentPreset 1448{ 1449 NSParameterAssert(currentPreset); 1450 1451 if (currentPreset != _currentPreset) 1452 { 1453 [[self.window.undoManager prepareWithInvocationTarget:self] setCurrentPreset:_currentPreset]; 1454 _currentPreset = currentPreset; 1455 } 1456} 1457 1458- (IBAction)reloadPreset:(id)sender 1459{ 1460 HBPreset *preset = self.selectedPreset ? self.selectedPreset : self.currentPreset; 1461 if (preset) 1462 { 1463 [self doApplyPreset:preset]; 1464 } 1465} 1466 1467- (void)applyPreset:(HBPreset *)preset 1468{ 1469 BOOL success = [self doApplyPreset:preset]; 1470 if (success == YES) 1471 { 1472 self.selectedPreset = preset; 1473 self.presetView.selectedPreset = preset; 1474 } 1475} 1476 1477- (BOOL)doApplyPreset:(HBPreset *)preset 1478{ 1479 BOOL success = NO; 1480 1481 // Remove the job observer so we don't update the file name 1482 // too many times while the preset is being applied 1483 [self removeJobObservers]; 1484 1485 NSError *error = nil; 1486 success = [self.job applyPreset:preset error:&error]; 1487 1488 if (success == NO) 1489 { 1490 [self presentError:error]; 1491 } 1492 else 1493 { 1494 self.currentPreset = preset; 1495 1496 [self.autoNamer updateFileExtension]; 1497 1498 // If Auto Naming is on, update the destination 1499 [self.autoNamer updateFileName]; 1500 } 1501 1502 [self addJobObservers]; 1503 1504 return success; 1505} 1506 1507- (IBAction)showAddPresetPanel:(id)sender 1508{ 1509 [self.window HB_endEditing]; 1510 1511 // Show the add panel 1512 HBAddPresetController *addPresetController = [[HBAddPresetController alloc] initWithPreset:[self createPresetFromCurrentSettings] 1513 presetManager:self.presetManager 1514 customWidth:self.job.picture.maxWidth 1515 customHeight:self.job.picture.maxHeight 1516 resolutionLimitMode:self.job.picture.resolutionLimitMode]; 1517 1518 [self.window beginSheet:addPresetController.window completionHandler:^(NSModalResponse returnCode) { 1519 if (returnCode == NSModalResponseOK) 1520 { 1521 [self applyPreset:addPresetController.preset]; 1522 } 1523 }]; 1524} 1525 1526- (HBMutablePreset *)createPresetFromCurrentSettings 1527{ 1528 HBMutablePreset *preset = [self.currentPreset mutableCopy]; 1529 [self.job writeToPreset:preset]; 1530 return preset; 1531} 1532 1533- (void)updateCurrentPreset 1534{ 1535 if (self.job) 1536 { 1537 if ([NSUserDefaults.standardUserDefaults boolForKey:HBKeepPresetEdits] == NO) 1538 { 1539 if (self.selectedPreset) 1540 { 1541 self.currentPreset = self.selectedPreset; 1542 } 1543 } 1544 else 1545 { 1546 self.currentPreset = [self createPresetFromCurrentSettings]; 1547 } 1548 } 1549} 1550 1551- (IBAction)showRenamePresetPanel:(id)sender 1552{ 1553 [self.window HB_endEditing]; 1554 1555 __block HBRenamePresetController *renamePresetController = [[HBRenamePresetController alloc] initWithPreset:self.selectedPreset 1556 presetManager:self.presetManager]; 1557 [self.window beginSheet:renamePresetController.window completionHandler:^(NSModalResponse returnCode) { 1558 if (returnCode == NSModalResponseOK) 1559 { 1560 self.job.presetName = renamePresetController.preset.name; 1561 } 1562 renamePresetController = nil; 1563 }]; 1564} 1565 1566#pragma mark - Import Export Preset(s) 1567 1568- (IBAction)exportPreset:(id)sender 1569{ 1570 self.presetView.selectedPreset = self.selectedPreset; 1571 [self.presetView exportPreset:sender]; 1572} 1573 1574- (IBAction)importPreset:(id)sender 1575{ 1576 [self.presetView importPreset:sender]; 1577} 1578 1579#pragma mark - Preset Menu 1580 1581- (IBAction)selectDefaultPreset:(id)sender 1582{ 1583 [self applyPreset:self.presetManager.defaultPreset]; 1584} 1585 1586- (IBAction)setDefaultPreset:(id)sender 1587{ 1588 [self.presetManager setDefaultPreset:self.selectedPreset]; 1589} 1590 1591- (IBAction)savePreset:(id)sender 1592{ 1593 [self.window HB_endEditing]; 1594 1595 NSIndexPath *indexPath = [self.presetManager indexPathOfPreset:self.selectedPreset]; 1596 if (indexPath) 1597 { 1598 HBMutablePreset *preset = [self createPresetFromCurrentSettings]; 1599 preset.name = self.selectedPreset.name; 1600 preset.isDefault = self.selectedPreset.isDefault; 1601 1602 [self.presetManager replacePresetAtIndexPath:indexPath withPreset:preset]; 1603 1604 self.job.presetName = preset.name; 1605 self.selectedPreset = preset; 1606 self.presetView.selectedPreset = preset; 1607 1608 [self.presetManager savePresets]; 1609 [self.window.undoManager removeAllActions]; 1610 } 1611} 1612 1613- (IBAction)deletePreset:(id)sender 1614{ 1615 self.presetView.selectedPreset = self.selectedPreset; 1616 [self.presetView deletePreset:self]; 1617} 1618 1619- (IBAction)insertCategory:(id)sender 1620{ 1621 [self.presetView insertCategory:sender]; 1622} 1623 1624- (IBAction)selectPresetFromMenu:(id)sender 1625{ 1626 // Retrieve the preset stored in the NSMenuItem 1627 HBPreset *preset = [sender representedObject]; 1628 [self applyPreset:preset]; 1629} 1630 1631@end 1632 1633@implementation HBController (TouchBar) 1634 1635@dynamic touchBar; 1636 1637static NSTouchBarItemIdentifier HBTouchBarMain = @"fr.handbrake.mainWindowTouchBar"; 1638 1639static NSTouchBarItemIdentifier HBTouchBarOpen = @"fr.handbrake.openSource"; 1640static NSTouchBarItemIdentifier HBTouchBarAddToQueue = @"fr.handbrake.addToQueue"; 1641static NSTouchBarItemIdentifier HBTouchBarAddTitlesToQueue = @"fr.handbrake.addTitlesToQueue"; 1642static NSTouchBarItemIdentifier HBTouchBarRip = @"fr.handbrake.rip"; 1643static NSTouchBarItemIdentifier HBTouchBarPause = @"fr.handbrake.pause"; 1644static NSTouchBarItemIdentifier HBTouchBarPreview = @"fr.handbrake.preview"; 1645static NSTouchBarItemIdentifier HBTouchBarActivity = @"fr.handbrake.activity"; 1646 1647- (NSTouchBar *)makeTouchBar 1648{ 1649 NSTouchBar *bar = [[NSTouchBar alloc] init]; 1650 bar.delegate = self; 1651 1652 bar.defaultItemIdentifiers = @[HBTouchBarOpen, NSTouchBarItemIdentifierFixedSpaceSmall, HBTouchBarAddToQueue, NSTouchBarItemIdentifierFixedSpaceLarge, HBTouchBarRip, HBTouchBarPause, NSTouchBarItemIdentifierFixedSpaceLarge, HBTouchBarPreview, HBTouchBarActivity, NSTouchBarItemIdentifierOtherItemsProxy]; 1653 1654 bar.customizationIdentifier = HBTouchBarMain; 1655 bar.customizationAllowedItemIdentifiers = @[HBTouchBarOpen, HBTouchBarAddToQueue, HBTouchBarAddTitlesToQueue, HBTouchBarRip, HBTouchBarPause, HBTouchBarPreview, HBTouchBarActivity, NSTouchBarItemIdentifierFlexibleSpace]; 1656 1657 return bar; 1658} 1659 1660- (NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier 1661{ 1662 if ([identifier isEqualTo:HBTouchBarOpen]) 1663 { 1664 NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; 1665 item.customizationLabel = NSLocalizedString(@"Open Source", @"Touch bar"); 1666 1667 NSButton *button = [NSButton buttonWithTitle:NSLocalizedString(@"Open Source", @"Touch bar") target:self action:@selector(browseSources:)]; 1668 1669 item.view = button; 1670 return item; 1671 } 1672 else if ([identifier isEqualTo:HBTouchBarAddToQueue]) 1673 { 1674 NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; 1675 item.customizationLabel = NSLocalizedString(@"Add To Queue", @"Touch bar"); 1676 1677 NSButton *button = [NSButton buttonWithTitle:NSLocalizedString(@"Add To Queue", @"Touch bar") target:self action:@selector(addToQueue:)]; 1678 1679 item.view = button; 1680 return item; 1681 } 1682 else if ([identifier isEqualTo:HBTouchBarAddTitlesToQueue]) 1683 { 1684 NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; 1685 item.customizationLabel = NSLocalizedString(@"Add Titles To Queue", @"Touch bar"); 1686 1687 NSButton *button = [NSButton buttonWithTitle:NSLocalizedString(@"Add Titles To Queue", @"Touch bar") target:self action:@selector(addTitlesToQueue:)]; 1688 1689 item.view = button; 1690 return item; 1691 } 1692 else if ([identifier isEqualTo:HBTouchBarRip]) 1693 { 1694 NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; 1695 item.customizationLabel = NSLocalizedString(@"Start/Stop Encoding", @"Touch bar"); 1696 1697 NSButton *button = [NSButton buttonWithImage:[NSImage imageNamed:NSImageNameTouchBarPlayTemplate] target:self action:@selector(toggleStartCancel:)]; 1698 1699 item.view = button; 1700 return item; 1701 } 1702 else if ([identifier isEqualTo:HBTouchBarPause]) 1703 { 1704 NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; 1705 item.customizationLabel = NSLocalizedString(@"Pause Encoding", @"Touch bar"); 1706 1707 NSButton *button = [NSButton buttonWithImage:[NSImage imageNamed:NSImageNameTouchBarPauseTemplate] target:self action:@selector(togglePauseResume:)]; 1708 1709 item.view = button; 1710 return item; 1711 } 1712 else if ([identifier isEqualTo:HBTouchBarPreview]) 1713 { 1714 NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; 1715 item.customizationLabel = NSLocalizedString(@"Show Preview Window", @"Touch bar"); 1716 1717 NSButton *button = [NSButton buttonWithImage:[NSImage imageNamed:NSImageNameTouchBarQuickLookTemplate] target:self action:@selector(showPreviewWindow:)]; 1718 1719 item.view = button; 1720 return item; 1721 } 1722 else if ([identifier isEqualTo:HBTouchBarActivity]) 1723 { 1724 NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; 1725 item.customizationLabel = NSLocalizedString(@"Show Activity Window", @"Touch bar"); 1726 1727 NSButton *button = [NSButton buttonWithImage:[NSImage imageNamed:NSImageNameTouchBarGetInfoTemplate] target:nil action:@selector(showOutputPanel:)]; 1728 1729 item.view = button; 1730 return item; 1731 } 1732 1733 return nil; 1734} 1735 1736- (void)_touchBar_updateButtonsStateForScanCore:(HBState)state 1737{ 1738 NSButton *openButton = (NSButton *)[[self.touchBar itemForIdentifier:HBTouchBarOpen] view]; 1739 1740 if (state == HBStateIdle) 1741 { 1742 openButton.title = NSLocalizedString(@"Open Source", @"Touch bar"); 1743 openButton.bezelColor = nil; 1744 } 1745 else 1746 { 1747 openButton.title = NSLocalizedString(@"Cancel Scan", @"Touch bar"); 1748 openButton.bezelColor = [NSColor systemRedColor]; 1749 } 1750} 1751 1752- (void)_touchBar_updateQueueButtonsState 1753{ 1754 NSButton *ripButton = (NSButton *)[[self.touchBar itemForIdentifier:HBTouchBarRip] view]; 1755 NSButton *pauseButton = (NSButton *)[[self.touchBar itemForIdentifier:HBTouchBarPause] view]; 1756 1757 if (self.queue.isEncoding) 1758 { 1759 ripButton.image = [NSImage imageNamed:NSImageNameTouchBarRecordStopTemplate]; 1760 } 1761 else 1762 { 1763 ripButton.image = [NSImage imageNamed:NSImageNameTouchBarPlayTemplate]; 1764 } 1765 1766 if (self.queue.canResume) 1767 { 1768 pauseButton.image = [NSImage imageNamed:NSImageNameTouchBarPlayTemplate]; 1769 } 1770 else 1771 { 1772 pauseButton.image = [NSImage imageNamed:NSImageNameTouchBarPauseTemplate]; 1773 } 1774} 1775 1776- (void)_touchBar_validateUserInterfaceItems 1777{ 1778 for (NSTouchBarItemIdentifier identifier in self.touchBar.itemIdentifiers) { 1779 NSTouchBarItem *item = [self.touchBar itemForIdentifier:identifier]; 1780 NSView *view = item.view; 1781 if ([view isKindOfClass:[NSButton class]]) { 1782 NSButton *button = (NSButton *)view; 1783 BOOL enabled = [self validateUserIterfaceItemForAction:button.action]; 1784 button.enabled = enabled; 1785 } 1786 } 1787} 1788 1789@end 1790