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