/* HBQueueController This file is part of the HandBrake source code. Homepage: . It may be used under the terms of the GNU General Public License. */ #import "HBQueueController.h" #import "HBQueueItem.h" #import "HBController.h" #import "HBAppDelegate.h" #import "HBTableView.h" #import "HBQueueItemView.h" #import "NSArray+HBAdditions.h" #import "HBUtilities.h" #import "HBDockTile.h" #import "HBOutputRedirect.h" #import "HBJobOutputFileWriter.h" #import "HBPreferencesController.h" @import HandBrakeKit; // Pasteboard type for or drag operations #define DragDropSimplePboardType @"HBQueueCustomTableViewPboardType" // DockTile update frequency in total percent increment #define dockTileUpdateFrequency 0.1f static void *HBControllerQueueCoreContext = &HBControllerQueueCoreContext; @interface HBQueueController () /// Whether the window is visible or occluded, /// useful to avoid updating the UI needlessly @property (nonatomic) BOOL visible; // Progress @property (nonatomic, strong) NSAttributedString *progressInfo; @property (nonatomic, strong) NSDictionary *monospacedAttr; @property (nonatomic, readonly) HBDockTile *dockTile; @property (nonatomic, readwrite) double dockIconProgress; @property (unsafe_unretained) IBOutlet NSTextField *progressTextField; @property (unsafe_unretained) IBOutlet NSTextField *countTextField; @property (unsafe_unretained) IBOutlet HBTableView *tableView; @property (nonatomic) IBOutlet NSToolbarItem *ripToolbarItem; @property (nonatomic) IBOutlet NSToolbarItem *pauseToolbarItem; @property (nonatomic) NSTableCellView *dummyCell; @property (nonatomic) NSLayoutConstraint *dummyCellWidth; @property (nonatomic, readonly) HBDistributedArray *items; @property (nonatomic) HBQueueItem *currentItem; @property (nonatomic) HBJobOutputFileWriter *currentLog; @property (nonatomic, readwrite) BOOL stop; @property (nonatomic, readwrite) NSUInteger pendingItemsCount; @property (nonatomic, readwrite) NSUInteger completedItemsCount; @property (nonatomic) NSArray *dragNodesArray; @end @interface HBQueueController (TouchBar) - (void)_touchBar_updateButtonsStateForQueueCore:(HBState)state; - (void)_touchBar_validateUserInterfaceItems; @end @implementation HBQueueController - (instancetype)initWithURL:(NSURL *)queueURL; { NSParameterAssert(queueURL); if (self = [super initWithWindowNibName:@"Queue"]) { // Load the dockTile and instiante initial text fields _dockTile = [[HBDockTile alloc] initWithDockTile:[[NSApplication sharedApplication] dockTile] image:[[NSApplication sharedApplication] applicationIconImage]]; NSInteger loggingLevel = [[NSUserDefaults standardUserDefaults] integerForKey:@"LoggingLevel"]; // Init a separate instance of libhb for the queue _core = [[HBCore alloc] initWithLogLevel:loggingLevel name:@"QueueCore"]; _core.automaticallyPreventSleep = NO; // Progress _monospacedAttr = @{NSFontAttributeName: [NSFont monospacedDigitSystemFontOfSize:[NSFont smallSystemFontSize] weight:NSFontWeightRegular]}; _progressInfo = [[NSAttributedString alloc] initWithString:@""]; // Load the queue from disk. _items = [[HBDistributedArray alloc] initWithURL:queueURL class:[HBQueueItem class]]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reloadQueue) name:HBDistributedArrayChanged object:_items]; [NSUserNotificationCenter defaultUserNotificationCenter].delegate = self; } return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)windowDidLoad { if (@available (macOS 10.12, *)) { self.window.tabbingMode = NSWindowTabbingModeDisallowed; } // lets setup our queue list table view for drag and drop here [self.tableView registerForDraggedTypes:@[DragDropSimplePboardType]]; [self.tableView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:YES]; [self.tableView setVerticalMotionCanBeginDrag:YES]; [self updateQueueStats]; [self.core addObserver:self forKeyPath:@"state" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:HBControllerQueueCoreContext]; [self addObserver:self forKeyPath:@"pendingItemsCount" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:HBControllerQueueCoreContext]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == HBControllerQueueCoreContext) { HBState state = self.core.state; [self updateToolbarButtonsStateForQueueCore:state]; [self.window.toolbar validateVisibleItems]; if (@available(macOS 10.12.2, *)) { [self _touchBar_updateButtonsStateForQueueCore:state]; [self _touchBar_validateUserInterfaceItems]; } } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } #pragma mark Toolbar - (void)updateToolbarButtonsStateForQueueCore:(HBState)state { if (state == HBStatePaused) { _pauseToolbarItem.image = [NSImage imageNamed: @"encode"]; _pauseToolbarItem.label = NSLocalizedString(@"Resume", @"Toolbar Pause Item"); _pauseToolbarItem.toolTip = NSLocalizedString(@"Resume Encoding", @"Toolbar Pause Item"); } else { _pauseToolbarItem.image = [NSImage imageNamed:@"pauseencode"]; _pauseToolbarItem.label = NSLocalizedString(@"Pause", @"Toolbar Pause Item"); _pauseToolbarItem.toolTip = NSLocalizedString(@"Pause Encoding", @"Toolbar Pause Item"); } if (state == HBStateScanning || state == HBStateWorking || state == HBStateSearching || state == HBStateMuxing || state == HBStatePaused) { _ripToolbarItem.image = [NSImage imageNamed:@"stopencode"]; _ripToolbarItem.label = NSLocalizedString(@"Stop", @"Toolbar Start/Stop Item"); _ripToolbarItem.toolTip = NSLocalizedString(@"Stop Encoding", @"Toolbar Start/Stop Item"); } else { _ripToolbarItem.image = [NSImage imageNamed: @"encode"]; _ripToolbarItem.label = NSLocalizedString(@"Start", @"Toolbar Start/Stop Item"); _pauseToolbarItem.toolTip = NSLocalizedString(@"Start Encoding", @"Toolbar Start/Stop Item"); } } - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { SEL action = menuItem.action; if (action == @selector(rip:)) { if (self.core.state == HBStateIdle) { menuItem.title = NSLocalizedString(@"Start Encoding", @"Queue -> start/stop menu"); return (self.pendingItemsCount > 0); } else if (self.core.state != HBStateIdle) { menuItem.title = NSLocalizedString(@"Stop Encoding", @"Queue -> start/stop menu"); return YES; } } if (action == @selector(pause:)) { if (self.core.state != HBStatePaused) { menuItem.title = NSLocalizedString(@"Pause Encoding", @"Queue -> pause/resume menu"); } else { menuItem.title = NSLocalizedString(@"Resume Encoding", @"Queue -> pause/resume men"); } return (self.core.state == HBStateWorking || self.core.state == HBStatePaused); } if (action == @selector(editSelectedQueueItem:) || action == @selector(removeSelectedQueueItem:) || action == @selector(revealSelectedQueueItems:) || action == @selector(revealSelectedQueueItemsSources:)) { return (self.tableView.selectedRow != -1 || self.tableView.clickedRow != -1); } if (action == @selector(resetJobState:)) { return self.tableView.targetedRowIndexes.count > 0; } if (action == @selector(clearAll:)) { return self.items.count > 0; } if (action == @selector(clearCompleted:)) { return self.completedItemsCount > 0; } return YES; } - (BOOL)validateUserIterfaceItemForAction:(SEL)action { HBState s = self.core.state; if (action == @selector(toggleStartCancel:)) { if ((s == HBStateScanning) || (s == HBStatePaused) || (s == HBStateWorking) || (s == HBStateMuxing)) { return YES; } else { return (self.pendingItemsCount > 0); } } if (action == @selector(togglePauseResume:)) { if (s == HBStatePaused) { return YES; } else { return (s == HBStateWorking || s == HBStateMuxing); } } return NO; } - (BOOL)validateToolbarItem:(NSToolbarItem *)theItem { SEL action = theItem.action; return [self validateUserIterfaceItemForAction:action]; } #pragma mark - Public methods - (void)addJob:(HBJob *)item { NSParameterAssert(item); [self addJobsFromArray:@[item]]; } - (void)addJobsFromArray:(NSArray *)jobs; { NSParameterAssert(jobs); NSMutableArray *itemsToAdd = [NSMutableArray array]; for (HBJob *job in jobs) { HBQueueItem *item = [[HBQueueItem alloc] initWithJob:job]; [itemsToAdd addObject:item]; } if (itemsToAdd.count) { [self addQueueItems:itemsToAdd]; } } - (BOOL)jobExistAtURL:(NSURL *)url { NSParameterAssert(url); for (HBQueueItem *item in self.items) { if ((item.state == HBQueueItemStateReady || item.state == HBQueueItemStateWorking) && [item.completeOutputURL isEqualTo:url]) { return YES; } } return NO; } - (NSUInteger)count { return self.items.count; } /** * This method will clear the queue of any encodes that are not still pending * this includes both successfully completed encodes as well as canceled encodes */ - (void)removeCompletedJobs { [self.items beginTransaction]; NSIndexSet *indexes = [self.items indexesOfObjectsUsingBlock:^BOOL(HBQueueItem *item) { return (item.state == HBQueueItemStateCompleted || item.state == HBQueueItemStateCanceled); }]; [self removeQueueItemsAtIndexes:indexes]; [self.items commit]; } /** * This method will clear the queue of all encodes. effectively creating an empty queue */ - (void)removeAllJobs { [self.items beginTransaction]; [self removeQueueItemsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.items.count)]]; [self.items commit]; } /** * This method will set any item marked as encoding back to pending * currently used right after a queue reload */ - (void)setEncodingJobsAsPending { [self.items beginTransaction]; NSMutableIndexSet *indexes = [NSMutableIndexSet indexSet]; NSUInteger idx = 0; for (HBQueueItem *item in self.items) { // We want to keep any queue item that is pending or was previously being encoded if (item.state == HBQueueItemStateWorking) { item.state = HBQueueItemStateReady; [indexes addIndex:idx]; } idx++; } [self reloadQueueItemsAtIndexes:indexes]; [self.items commit]; } #pragma mark - Private queue editing methods /** * Reloads the queue, this is called * when another HandBrake instances modifies the queue */ - (void)reloadQueue { [self updateQueueStats]; [self.tableView reloadData]; [self.window.undoManager removeAllActions]; } - (void)reloadQueueItemAtIndex:(NSUInteger)idx { [self reloadQueueItemsAtIndexes:[NSIndexSet indexSetWithIndex:idx]]; } - (void)reloadQueueItemsAtIndexes:(NSIndexSet *)indexes { NSIndexSet *columnIndexes = [NSIndexSet indexSetWithIndex:0]; [self.tableView reloadDataForRowIndexes:indexes columnIndexes:columnIndexes]; [self updateQueueStats]; } - (void)addQueueItems:(NSArray *)items { NSParameterAssert(items); NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(self.items.count, items.count)]; [self addQueueItems:items atIndexes:indexes]; } - (void)addQueueItems:(NSArray *)items atIndexes:(NSIndexSet *)indexes { NSParameterAssert(items); NSParameterAssert(indexes); [self.items beginTransaction]; [self.tableView beginUpdates]; // Forward NSUInteger currentIndex = indexes.firstIndex; NSUInteger currentObjectIndex = 0; while (currentIndex != NSNotFound) { [self.items insertObject:items[currentObjectIndex] atIndex:currentIndex]; currentIndex = [indexes indexGreaterThanIndex:currentIndex]; currentObjectIndex++; } [self.tableView insertRowsAtIndexes:indexes withAnimation:NSTableViewAnimationSlideDown]; NSUndoManager *undo = self.window.undoManager; [[undo prepareWithInvocationTarget:self] removeQueueItemsAtIndexes:indexes]; if (!undo.isUndoing) { if (items.count == 1) { [undo setActionName:NSLocalizedString(@"Add Job To Queue", @"Queue undo action name")]; } else { [undo setActionName:NSLocalizedString(@"Add Jobs To Queue", @"Queue undo action name")]; } } [self.tableView endUpdates]; [self updateQueueStats]; [self.items commit]; } - (void)removeQueueItemAtIndex:(NSUInteger)index { [self removeQueueItemsAtIndexes:[NSIndexSet indexSetWithIndex:index]]; } - (void)removeQueueItemsAtIndexes:(NSIndexSet *)indexes { NSParameterAssert(indexes); if (indexes.count == 0) { return; } [self.items beginTransaction]; [self.tableView beginUpdates]; NSArray *removeItems = [self.items objectsAtIndexes:indexes]; if (self.items.count > indexes.lastIndex) { [self.items removeObjectsAtIndexes:indexes]; } [self.tableView removeRowsAtIndexes:indexes withAnimation:NSTableViewAnimationSlideUp]; [self.tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:indexes.firstIndex] byExtendingSelection:NO]; NSUndoManager *undo = self.window.undoManager; [[undo prepareWithInvocationTarget:self] addQueueItems:removeItems atIndexes:indexes]; if (!undo.isUndoing) { if (indexes.count == 1) { [undo setActionName:NSLocalizedString(@"Remove Job From Queue", @"Queue undo action name")]; } else { [undo setActionName:NSLocalizedString(@"Remove Jobs From Queue", @"Queue undo action name")]; } } [self.tableView endUpdates]; [self updateQueueStats]; [self.items commit]; } - (void)moveQueueItems:(NSArray *)items toIndex:(NSUInteger)index { [self.items beginTransaction]; [self.tableView beginUpdates]; NSMutableArray *source = [NSMutableArray array]; NSMutableArray *dest = [NSMutableArray array]; for (id object in items.reverseObjectEnumerator) { NSUInteger sourceIndex = [self.items indexOfObject:object]; [self.items removeObjectAtIndex:sourceIndex]; if (sourceIndex < index) { index--; } [self.items insertObject:object atIndex:index]; [source addObject:@(index)]; [dest addObject:@(sourceIndex)]; [self.tableView moveRowAtIndex:sourceIndex toIndex:index]; } NSUndoManager *undo = self.window.undoManager; [[undo prepareWithInvocationTarget:self] moveQueueItemsAtIndexes:source toIndexes:dest]; if (!undo.isUndoing) { if (items.count == 1) { [undo setActionName:NSLocalizedString(@"Move Job in Queue", @"Queue undo action name")]; } else { [undo setActionName:NSLocalizedString(@"Move Jobs in Queue", @"Queue undo action name")]; } } [self.tableView endUpdates]; [self.items commit]; } - (void)moveQueueItemsAtIndexes:(NSArray *)source toIndexes:(NSArray *)dest { [self.items beginTransaction]; [self.tableView beginUpdates]; NSMutableArray *newSource = [NSMutableArray array]; NSMutableArray *newDest = [NSMutableArray array]; for (NSInteger idx = source.count - 1; idx >= 0; idx--) { NSUInteger sourceIndex = [source[idx] integerValue]; NSUInteger destIndex = [dest[idx] integerValue]; [newSource addObject:@(destIndex)]; [newDest addObject:@(sourceIndex)]; id obj = [self.items objectAtIndex:sourceIndex]; [self.items removeObjectAtIndex:sourceIndex]; [self.items insertObject:obj atIndex:destIndex]; [self.tableView moveRowAtIndex:sourceIndex toIndex:destIndex]; } NSUndoManager *undo = self.window.undoManager; [[undo prepareWithInvocationTarget:self] moveQueueItemsAtIndexes:newSource toIndexes:newDest]; if (!undo.isUndoing) { if (source.count == 1) { [undo setActionName:NSLocalizedString(@"Move Job in Queue", @"Queue undo action name")]; } else { [undo setActionName:NSLocalizedString(@"Move Jobs in Queue", @"Queue undo action name")]; } } [self.tableView endUpdates]; [self.items commit]; } - (void)windowDidChangeOcclusionState:(NSNotification *)notification { if ([self.window occlusionState] & NSWindowOcclusionStateVisible) { self.visible = YES; self.progressTextField.attributedStringValue = self.progressInfo; } else { self.visible = NO; } } - (void)updateProgress:(NSString *)info progress:(double)progress hidden:(BOOL)hidden { self.progressInfo = [[NSAttributedString alloc] initWithString:info attributes:_monospacedAttr]; if (self.visible) { self.progressTextField.attributedStringValue = _progressInfo; } [self.controller setQueueInfo:_progressInfo progress:progress hidden:hidden]; } /** * Updates the queue status label. */ - (void)updateQueueStats { // lets get the stats on the status of the queue array NSUInteger pendingCount = 0; NSUInteger completedCount = 0; for (HBQueueItem *item in self.items) { if (item.state == HBQueueItemStateReady) { pendingCount++; } if (item.state == HBQueueItemStateCompleted) { completedCount++; } } NSString *string; if (pendingCount == 0) { string = NSLocalizedString(@"No encode pending", @"Queue status"); } else if (pendingCount == 1) { string = [NSString stringWithFormat: NSLocalizedString(@"%d encode pending", @"Queue status"), pendingCount]; } else { string = [NSString stringWithFormat: NSLocalizedString(@"%d encodes pending", @"Queue status"), pendingCount]; } self.countTextField.stringValue = string; self.pendingItemsCount = pendingCount; self.completedItemsCount = completedCount; } #pragma mark - Queue Job Processing - (BOOL)_isDiskSpaceLowAtURL:(NSURL *)url { if ([[NSUserDefaults standardUserDefaults] boolForKey:@"HBQueuePauseIfLowSpace"]) { NSURL *volumeURL = nil; NSDictionary *attrs = [url resourceValuesForKeys:@[NSURLIsVolumeKey, NSURLVolumeURLKey] error:NULL]; long long minCapacity = [[[NSUserDefaults standardUserDefaults] stringForKey:@"HBQueueMinFreeSpace"] longLongValue] * 1000000000; volumeURL = [attrs[NSURLIsVolumeKey] boolValue] ? url : attrs[NSURLVolumeURLKey]; if (volumeURL) { [volumeURL removeCachedResourceValueForKey:NSURLVolumeAvailableCapacityKey]; attrs = [volumeURL resourceValuesForKeys:@[NSURLVolumeAvailableCapacityKey] error:NULL]; if (attrs[NSURLVolumeAvailableCapacityKey]) { if ([attrs[NSURLVolumeAvailableCapacityKey] longLongValue] < minCapacity) { return YES; } } } } return NO; } /** * Used to get the next pending queue item and return it if found */ - (HBQueueItem *)getNextPendingQueueItem { for (HBQueueItem *item in self.items) { if (item.state == HBQueueItemStateReady) { return item; } } return nil; } /** * Starts the queue */ - (void)encodeNextQueueItem { [self.items beginTransaction]; self.currentItem = nil; // since we have completed an encode, we go to the next if (self.stop) { [HBUtilities writeToActivityLog:"Queue manually stopped"]; self.stop = NO; [self.core allowSleep]; } else { // Check to see if there are any more pending items in the queue HBQueueItem *nextItem = [self getNextPendingQueueItem]; if (nextItem && [self _isDiskSpaceLowAtURL:nextItem.outputURL]) { // Disk space is low, show an alert [HBUtilities writeToActivityLog:"Queue Stopped, low space on destination disk"]; [self queueLowDiskSpaceAlert]; } // If we still have more pending items in our queue, lets go to the next one else if (nextItem) { // now we mark the queue item as working so another instance can not come along and try to scan it while we are scanning nextItem.state = HBQueueItemStateWorking; // Tell HB to output a new activity log file for this encode self.currentLog = [[HBJobOutputFileWriter alloc] initWithJob:nextItem.job]; if (self.currentLog) { [[HBOutputRedirect stderrRedirect] addListener:self.currentLog]; [[HBOutputRedirect stdoutRedirect] addListener:self.currentLog]; } self.currentItem = nextItem; [self reloadQueueItemAtIndex:[self.items indexOfObject:nextItem]]; // now we can go ahead and scan the new pending queue item [self encodeItem:nextItem]; // erase undo manager history [self.window.undoManager removeAllActions]; } else { [HBUtilities writeToActivityLog:"Queue Done, there are no more pending encodes"]; // Since there are no more items to encode, go to queueCompletedAlerts // for user specified alerts after queue completed [self queueCompletedAlerts]; [self.core allowSleep]; } } [self.items commit]; } - (void)completedItem:(HBQueueItem *)item result:(HBCoreResult)result; { NSParameterAssert(item); [self.items beginTransaction]; // Since we are done with this encode, tell output to stop writing to the // individual encode log. [[HBOutputRedirect stderrRedirect] removeListener:self.currentLog]; [[HBOutputRedirect stdoutRedirect] removeListener:self.currentLog]; self.currentLog = nil; // Check to see if the encode state has not been canceled // to determine if we should send it to external app. if (result != HBCoreResultCanceled) { // Send to tagger [self sendToExternalApp:item]; } // Mark the encode just finished switch (result) { case HBCoreResultDone: item.state = HBQueueItemStateCompleted; break; case HBCoreResultCanceled: item.state = HBQueueItemStateCanceled; break; default: item.state = HBQueueItemStateFailed; break; } if ([self.items containsObject:item]) { [self reloadQueueItemAtIndex:[self.items indexOfObject:item]]; } [self.window.toolbar validateVisibleItems]; [self.items commit]; // Update UI NSString *info = nil; switch (result) { case HBCoreResultDone: info = NSLocalizedString(@"Encode Finished.", @"Queue status"); [self itemCompletedAlerts:item result:result]; break; case HBCoreResultCanceled: info = NSLocalizedString(@"Encode Canceled.", @"Queue status"); break; default: info = NSLocalizedString(@"Encode Failed.", @"Queue status"); [self itemCompletedAlerts:item result:result]; break; } [self updateProgress:info progress:1.0 hidden:YES]; // Restore dock icon [self.dockTile updateDockIcon:-1.0 withETA:@""]; self.dockIconProgress = 0; } /** * Here we actually tell hb_scan to perform the source scan, using the path to source and title number */ - (void)encodeItem:(HBQueueItem *)item { NSParameterAssert(item); // Progress handler void (^progressHandler)(HBState state, HBProgress progress, NSString *info) = ^(HBState state, HBProgress progress, NSString *info) { [self updateProgress:info progress:0 hidden:NO]; }; // Completion handler void (^completionHandler)(HBCoreResult result) = ^(HBCoreResult result) { if (result == HBCoreResultDone) { [self realEncodeItem:item]; } else { [self completedItem:item result:result]; [self encodeNextQueueItem]; } }; // Only scan 10 previews before an encode - additional previews are // only useful for autocrop and static previews, which are already taken care of at this point [self.core scanURL:item.fileURL titleIndex:item.job.titleIdx previews:10 minDuration:0 progressHandler:progressHandler completionHandler:completionHandler]; } /** * This assumes that we have re-scanned and loaded up a new queue item to send to libhb */ - (void)realEncodeItem:(HBQueueItem *)item { NSParameterAssert(item); HBJob *job = item.job; // Reset the title in the job. job.title = self.core.titles.firstObject; NSParameterAssert(job); HBStateFormatter *formatter = [[HBStateFormatter alloc] init]; formatter.title = job.outputFileName; self.core.stateFormatter = formatter; // Progress handler void (^progressHandler)(HBState state, HBProgress progress, NSString *info) = ^(HBState state, HBProgress progress, NSString *info) { if (state == HBStateWorking) { // Update dock icon if (self.dockIconProgress < 100.0 * progress.percent) { [self.dockTile updateDockIcon:progress.percent hours:progress.hours minutes:progress.minutes seconds:progress.seconds]; self.dockIconProgress += dockTileUpdateFrequency; } } else if (state == HBStateMuxing) { [self.dockTile updateDockIcon:1.0 withETA:@""]; } // Update UI [self updateProgress:info progress:progress.percent hidden:NO]; }; // Completion handler void (^completionHandler)(HBCoreResult result) = ^(HBCoreResult result) { [self completedItem:item result:result]; [self encodeNextQueueItem]; }; // We should be all setup so let 'er rip [self.core encodeJob:job progressHandler:progressHandler completionHandler:completionHandler]; // We are done using the title, remove it from the job job.title = nil; } /** * Cancels the current job */ - (void)doCancelCurrentItem { if (self.core.state == HBStateScanning) { [self.core cancelScan]; } else { [self.core cancelEncode]; } } /** * Cancels the current job and starts processing the next in queue. */ - (void)cancelCurrentItemAndContinue { [self doCancelCurrentItem]; } /** * Cancels the current job and stops libhb from processing the remaining encodes. */ - (void)cancelCurrentItemAndStop { self.stop = YES; [self doCancelCurrentItem]; } /** * Finishes the current job and stops libhb from processing the remaining encodes. */ - (void)finishCurrentAndStop { self.stop = YES; } #pragma mark - Encode Done Actions - (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification { // Show the file in Finder when a done notification was clicked. NSString *path = notification.userInfo[@"Path"]; if ([path isKindOfClass:[NSString class]] && path.length) { NSURL *fileURL = [NSURL fileURLWithPath:path]; [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:@[fileURL]]; } } - (void)showNotificationWithTitle:(NSString *)title description:(NSString *)description url:(NSURL *)fileURL playSound:(BOOL)playSound { NSUserNotification *notification = [[NSUserNotification alloc] init]; notification.title = title; notification.informativeText = description; notification.soundName = playSound ? NSUserNotificationDefaultSoundName : nil; notification.hasActionButton = YES; notification.actionButtonTitle = NSLocalizedString(@"Show", @"Notification -> Show in Finder"); notification.userInfo = @{ @"Path": fileURL.path }; [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification]; } /** * Sends the URL to the external app * selected in the preferences. * * @param job the job of the file to send */ - (void)sendToExternalApp:(HBQueueItem *)item { // This end of encode action is called as each encode rolls off of the queue if ([[NSUserDefaults standardUserDefaults] boolForKey:@"HBSendToAppEnabled"] == YES) { #ifdef __SANDBOX_ENABLED__ BOOL accessingSecurityScopedResource = [item.outputURL startAccessingSecurityScopedResource]; #endif NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; NSString *app = [workspace fullPathForApplication:[[NSUserDefaults standardUserDefaults] objectForKey:@"HBSendToApp"]]; if (app) { if (![workspace openFile:item.completeOutputURL.path withApplication:app]) { [HBUtilities writeToActivityLog:"Failed to send file to: %s", app]; } } else { [HBUtilities writeToActivityLog:"Send file to: app not found"]; } #ifdef __SANDBOX_ENABLED__ if (accessingSecurityScopedResource) { [item.outputURL stopAccessingSecurityScopedResource]; } #endif } } /** * Runs the alert for a single job */ - (void)itemCompletedAlerts:(HBQueueItem *)item result:(HBCoreResult)result { // Both the Notification and Sending to tagger can be done as encodes roll off the queue if ([[NSUserDefaults standardUserDefaults] integerForKey:@"HBAlertWhenDone"] == HBDoneActionNotification || [[NSUserDefaults standardUserDefaults] integerForKey:@"HBAlertWhenDone"] == HBDoneActionAlertAndNotification) { // If Play System Alert has been selected in Preferences bool playSound = [[NSUserDefaults standardUserDefaults] boolForKey:@"HBAlertWhenDoneSound"]; NSString *title; NSString *description; if (result == HBCoreResultDone) { title = NSLocalizedString(@"Put down that cocktail…", @"Queue notification alert message"); description = [NSString stringWithFormat:NSLocalizedString(@"Your encode %@ is done!", @"Queue done notification message"), item.outputFileName]; } else { title = NSLocalizedString(@"Encode failed", @"Queue done notification failed message"); description = [NSString stringWithFormat:NSLocalizedString(@"Your encode %@ couldn't be completed.", @"Queue done notification message"), item.outputFileName]; } [self showNotificationWithTitle:title description:description url:item.completeOutputURL playSound:playSound]; } } /** * Runs the global queue completed alerts */ - (void)queueCompletedAlerts { // If Play System Alert has been selected in Preferences if ([[NSUserDefaults standardUserDefaults] boolForKey:@"HBAlertWhenDoneSound"] == YES) { NSBeep(); } // If Alert Window or Window and Notification has been selected if ([[NSUserDefaults standardUserDefaults] integerForKey:@"HBAlertWhenDone"] == HBDoneActionAlert || [[NSUserDefaults standardUserDefaults] integerForKey:@"HBAlertWhenDone"] == HBDoneActionAlertAndNotification) { // On Screen Notification NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:NSLocalizedString(@"Put down that cocktail…", @"Queue done alert message")]; [alert setInformativeText:NSLocalizedString(@"Your HandBrake queue is done!", @"Queue done alert informative text")]; [NSApp requestUserAttention:NSCriticalRequest]; [alert runModal]; } // If sleep has been selected if ([[NSUserDefaults standardUserDefaults] integerForKey:@"HBAlertWhenDone"] == HBDoneActionSleep) { // Sleep NSDictionary *errorDict; NSAppleScript *scriptObject = [[NSAppleScript alloc] initWithSource: @"tell application \"System Events\" to sleep"]; [scriptObject executeAndReturnError: &errorDict]; } // If Shutdown has been selected if ([[NSUserDefaults standardUserDefaults] integerForKey:@"HBAlertWhenDone"] == HBDoneActionShutDown) { // Shut Down NSDictionary *errorDict; NSAppleScript *scriptObject = [[NSAppleScript alloc] initWithSource:@"tell application \"System Events\" to shut down"]; [scriptObject executeAndReturnError: &errorDict]; } } - (void)queueLowDiskSpaceAlert { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:NSLocalizedString(@"Your destination disk is almost full.", @"Queue -> disk almost full alert message")]; [alert setInformativeText:NSLocalizedString(@"You need to make more space available on your destination disk.",@"Queue -> disk almost full alert informative text")]; [NSApp requestUserAttention:NSCriticalRequest]; [alert runModal]; } #pragma mark - Queue Item Controls /** * Delete encodes from the queue window and accompanying array * Also handling first cancelling the encode if in fact its currently encoding. */ - (IBAction)removeSelectedQueueItem:(id)sender { if ([self.items beginTransaction] == HBDistributedArrayContentReload) { // Do not execture the action if the array changed. [self.items commit]; return; } NSMutableIndexSet *targetedRows = [[self.tableView targetedRowIndexes] mutableCopy]; if (targetedRows.count) { // if this is a currently encoding job, we need to be sure to alert the user, // to let them decide to cancel it first, then if they do, we can come back and // remove it NSIndexSet *workingIndexes = [self.items indexesOfObjectsUsingBlock:^BOOL(HBQueueItem *item) { return item.state == HBQueueItemStateWorking; }]; if ([targetedRows containsIndexes:workingIndexes]) { [targetedRows removeIndexes:workingIndexes]; NSArray *workingItems = [self.items filteredArrayUsingBlock:^BOOL(HBQueueItem *item) { return item.state == HBQueueItemStateWorking; }]; if ([workingItems containsObject:self.currentItem]) { NSString *alertTitle = [NSString stringWithFormat:NSLocalizedString(@"Stop This Encode and Remove It?", @"Queue Stop Alert -> stop and remove message")]; // Which window to attach the sheet to? NSWindow *targetWindow = self.window; if ([sender respondsToSelector: @selector(window)]) { targetWindow = [sender window]; } NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:alertTitle]; [alert setInformativeText:NSLocalizedString(@"Your movie will be lost if you don't continue encoding.", @"Queue Stop Alert -> stop and remove informative text")]; [alert addButtonWithTitle:NSLocalizedString(@"Keep Encoding", @"Queue Stop Alert -> stop and remove first button")]; [alert addButtonWithTitle:NSLocalizedString(@"Stop Encoding and Delete", @"Queue Stop Alert -> stop and remove second button")]; [alert setAlertStyle:NSAlertStyleCritical]; [alert beginSheetModalForWindow:targetWindow completionHandler:^(NSModalResponse returnCode) { if (returnCode == NSAlertSecondButtonReturn) { [self.items beginTransaction]; NSInteger index = [self.items indexOfObject:self.currentItem]; [self cancelCurrentItemAndContinue]; [self removeQueueItemAtIndex:index]; [self.items commit]; } }]; } } // remove the non working items immediately [self removeQueueItemsAtIndexes:targetedRows]; } [self.items commit]; } /** * Show the finished encode in the finder */ - (IBAction)revealSelectedQueueItems:(id)sender { NSIndexSet *targetedRows = [self.tableView targetedRowIndexes]; NSMutableArray *urls = [[NSMutableArray alloc] init]; NSUInteger currentIndex = [targetedRows firstIndex]; while (currentIndex != NSNotFound) { NSURL *url = [[self.items objectAtIndex:currentIndex] completeOutputURL]; [urls addObject:url]; currentIndex = [targetedRows indexGreaterThanIndex:currentIndex]; } [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:urls]; } - (IBAction)revealSelectedQueueItemsSources:(id)sender { NSIndexSet *targetedRows = [self.tableView targetedRowIndexes]; NSMutableArray *urls = [[NSMutableArray alloc] init]; NSUInteger currentIndex = [targetedRows firstIndex]; while (currentIndex != NSNotFound) { NSURL *url = [[self.items objectAtIndex:currentIndex] fileURL]; [urls addObject:url]; currentIndex = [targetedRows indexGreaterThanIndex:currentIndex]; } [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:urls]; } - (void)remindUserOfSleepOrShutdown { if ([[NSUserDefaults standardUserDefaults] integerForKey:@"HBAlertWhenDone"] == HBDoneActionSleep) { // Warn that computer will sleep after encoding NSBeep(); [NSApp requestUserAttention:NSCriticalRequest]; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:NSLocalizedString(@"The computer will sleep after encoding is done.", @"Queue Done Alert -> sleep message")]; [alert setInformativeText:NSLocalizedString(@"You have selected to sleep the computer after encoding. To turn off sleeping, go to the HandBrake preferences.", @"Queue Done Alert -> sleep informative text")]; [alert addButtonWithTitle:NSLocalizedString(@"OK", @"Queue Done Alert -> sleep first button")]; [alert addButtonWithTitle:NSLocalizedString(@"Preferences…", @"Queue Done Alert -> sleep second button")]; NSInteger response = [alert runModal]; if (response == NSAlertSecondButtonReturn) { [self.delegate showPreferencesWindow:nil]; } [self promptForAppleEventAuthorization]; } else if ([[NSUserDefaults standardUserDefaults] integerForKey:@"HBAlertWhenDone"] == HBDoneActionShutDown) { // Warn that computer will shut down after encoding NSBeep(); [NSApp requestUserAttention:NSCriticalRequest]; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:NSLocalizedString(@"The computer will shut down after encoding is done.", @"Queue Done Alert -> shut down message")]; [alert setInformativeText:NSLocalizedString(@"You have selected to shut down the computer after encoding. To turn off shut down, go to the HandBrake preferences.", @"Queue Done Alert -> shut down informative text")]; [alert addButtonWithTitle:NSLocalizedString(@"OK", @"Queue Done Alert -> shut down first button")]; [alert addButtonWithTitle:NSLocalizedString(@"Preferences…", @"Queue Done Alert -> shut down second button")]; NSInteger response = [alert runModal]; if (response == NSAlertSecondButtonReturn) { [self.delegate showPreferencesWindow:nil]; } [self promptForAppleEventAuthorization]; } } - (void)promptForAppleEventAuthorization { HBPrivacyConsentState result = [HBUtilities determinePermissionToAutomateTarget:@"com.apple.systemevents" promptIfNeeded:YES]; if (result != HBPrivacyConsentStateGranted) { [HBUtilities writeToActivityLog:"Failed to get permission to automate system events"]; } } /** * Rip: puts up an alert before ultimately calling doRip */ - (IBAction)rip:(id)sender { // Rip or Cancel ? if (self.core.state == HBStateWorking || self.core.state == HBStatePaused || self.core.state == HBStateSearching) { [self cancelRip:sender]; } // If there are pending items in the queue, then this is a rip the queue else if (self.pendingItemsCount > 0) { // We check to see if we need to warn the user that the computer will go to sleep // or shut down when encoding is finished [self remindUserOfSleepOrShutdown]; [self.core preventSleep]; [self encodeNextQueueItem]; } } /** * Displays an alert asking user if the want to cancel encoding of current item. * Cancel: returns immediately after posting the alert. Later, when the user * acknowledges the alert, doCancelCurrentItem is called. */ - (IBAction)cancelRip:(id)sender { // Which window to attach the sheet to? NSWindow *window = self.window; if ([sender respondsToSelector:@selector(window)]) { window = [sender window]; } NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:NSLocalizedString(@"You are currently encoding. What would you like to do?", @"Queue Alert -> cancel rip message")]; [alert setInformativeText:NSLocalizedString(@"Select Continue Encoding to dismiss this dialog without making changes.", @"Queue Alert -> cancel rip informative text")]; [alert addButtonWithTitle:NSLocalizedString(@"Continue Encoding", @"Queue Alert -> cancel rip first button")]; [alert addButtonWithTitle:NSLocalizedString(@"Skip Current Job", @"Queue Alert -> cancel rip second button")]; [alert addButtonWithTitle:NSLocalizedString(@"Stop After Current Job", @"Queue Alert -> cancel rip third button")]; [alert addButtonWithTitle:NSLocalizedString(@"Stop All", @"Queue Alert -> cancel rip fourth button")]; [alert setAlertStyle:NSAlertStyleCritical]; [alert beginSheetModalForWindow:window completionHandler:^(NSModalResponse returnCode) { if (returnCode == NSAlertSecondButtonReturn) { [self cancelCurrentItemAndContinue]; } else if (returnCode == NSAlertThirdButtonReturn) { [self finishCurrentAndStop]; } else if (returnCode == NSAlertThirdButtonReturn + 1) { [self cancelCurrentItemAndStop]; } }]; } /** * Starts or cancels the processing of items depending on the current state */ - (IBAction)toggleStartCancel:(id)sender { HBState s = self.core.state; if ((s == HBStatePaused) || (s == HBStateWorking) || (s == HBStateMuxing)) { [self cancelRip:self]; } else if (self.pendingItemsCount > 0) { [self rip:self]; } } /** * Toggles the pause/resume state of libhb */ - (IBAction)togglePauseResume:(id)sender { HBState s = self.core.state; if (s == HBStatePaused) { [self.core resume]; [self.core preventSleep]; } else if (s == HBStateWorking || s == HBStateMuxing) { [self.core pause]; [self.core allowSleep]; } } /** * Resets the item state to ready. */ - (IBAction)resetJobState:(id)sender { if ([self.items beginTransaction] == HBDistributedArrayContentReload) { // Do not execture the action if the array changed. [self.items commit]; return; } NSIndexSet *targetedRows = [self.tableView targetedRowIndexes]; NSMutableIndexSet *updatedIndexes = [NSMutableIndexSet indexSet]; NSUInteger currentIndex = [targetedRows firstIndex]; while (currentIndex != NSNotFound) { HBQueueItem *item = self.items[currentIndex]; if (item.state == HBQueueItemStateCanceled || item.state == HBQueueItemStateCompleted || item.state == HBQueueItemStateFailed) { item.state = HBQueueItemStateReady; [updatedIndexes addIndex:currentIndex]; } currentIndex = [targetedRows indexGreaterThanIndex:currentIndex]; } [self reloadQueueItemsAtIndexes:updatedIndexes]; [self.items commit]; } - (void)editQueueItem:(HBQueueItem *)item { NSParameterAssert(item); [self.items beginTransaction]; if (item != self.currentItem) { item.state = HBQueueItemStateWorking; NSUInteger row = [self.items indexOfObject:item]; [self reloadQueueItemAtIndex:row]; [self.controller openJob:[item.job copy] completionHandler:^(BOOL result) { [self.items beginTransaction]; if (result) { // Now that source is loaded and settings applied, delete the queue item from the queue NSInteger index = [self.items indexOfObject:item]; item.state = HBQueueItemStateReady; [self removeQueueItemAtIndex:index]; } else { item.state = HBQueueItemStateFailed; NSBeep(); } [self.items commit]; }]; } else { NSBeep(); } [self.items commit]; } /** * Send the selected queue item back to the main window for rescan and possible edit. */ - (IBAction)editSelectedQueueItem:(id)sender { if ([self.items beginTransaction] == HBDistributedArrayContentReload) { // Do not execture the action if the array changed. [self.items commit]; return; } NSInteger row = self.tableView.clickedRow; if (row != NSNotFound) { // if this is a currently encoding item, we need to be sure to alert the user, // to let them decide to cancel it first, then if they do, we can come back and // remove it HBQueueItem *item = self.items[row]; if (item == self.currentItem) { NSString *alertTitle = [NSString stringWithFormat:NSLocalizedString(@"Stop This Encode and Edit It?", @"Queue Edit Alert -> stop and edit message")]; // Which window to attach the sheet to? NSWindow *docWindow = self.window; if ([sender respondsToSelector: @selector(window)]) { docWindow = [sender window]; } NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:alertTitle]; [alert setInformativeText:NSLocalizedString(@"Your movie will be lost if you don't continue encoding.", @"Queue Edit Alert -> stop and edit informative text")]; [alert addButtonWithTitle:NSLocalizedString(@"Keep Encoding", @"Queue Edit Alert -> stop and edit first button")]; [alert addButtonWithTitle:NSLocalizedString(@"Stop Encoding and Edit", @"Queue Edit Alert -> stop and edit second button")]; [alert setAlertStyle:NSAlertStyleCritical]; [alert beginSheetModalForWindow:docWindow completionHandler:^(NSModalResponse returnCode) { if (returnCode == NSAlertSecondButtonReturn) { [self editQueueItem:item]; } }]; } else if (item.state != HBQueueItemStateWorking) { [self editQueueItem:item]; } } [self.items commit]; } - (IBAction)clearAll:(id)sender { [self.items beginTransaction]; NSIndexSet *indexes = [self.items indexesOfObjectsUsingBlock:^BOOL(HBQueueItem *item) { return (item.state != HBQueueItemStateWorking); }]; [self removeQueueItemsAtIndexes:indexes]; [self.items commit]; } - (IBAction)clearCompleted:(id)sender { [self.items beginTransaction]; NSIndexSet *indexes = [self.items indexesOfObjectsUsingBlock:^BOOL(HBQueueItem *item) { return (item.state == HBQueueItemStateCompleted); }]; [self removeQueueItemsAtIndexes:indexes]; [self.items commit]; } #pragma mark - NSTableView data source - (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { HBQueueItemView *view = [tableView makeViewWithIdentifier:@"MainCell" owner:self]; HBQueueItem *item = self.items[row]; view.delegate = self; view.item = item; return view; } - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { return self.items.count; } - (NSTableCellView *)dummyCell { if (!_dummyCell) { _dummyCell = [self.tableView makeViewWithIdentifier:@"MainCellForSizing" owner: self]; _dummyCellWidth = [NSLayoutConstraint constraintWithItem:_dummyCell attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:500]; [_dummyCell addConstraint:_dummyCellWidth]; } return _dummyCell; } - (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row { HBQueueItem *item = self.items[row]; if (item.expanded) { CGFloat width = tableView.frame.size.width; self.dummyCellWidth.constant = width; self.dummyCell.textField.preferredMaxLayoutWidth = width - 60; self.dummyCell.textField.attributedStringValue = item.attributedDescription; CGFloat height = self.dummyCell.fittingSize.height; return height; } else { return 20; } } - (void)toggleRowsAtIndexes:(NSIndexSet *)rowIndexes expand:(BOOL)expand { NSMutableIndexSet *rowsToExpand = [NSMutableIndexSet indexSet]; [rowIndexes enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) { HBQueueItem *item = self.items[index]; BOOL expanded = item.expanded; if (expanded != expand) { item.expanded = !expanded; [rowsToExpand addIndex:index]; } HBQueueItemView *itemView = (HBQueueItemView *)[self.tableView viewAtColumn:0 row:index makeIfNecessary:NO]; if (expand) { [itemView expand]; } else { [itemView collapse]; } }]; [self.tableView noteHeightOfRowsWithIndexesChanged:rowsToExpand]; } #pragma mark NSQueueItemView delegate - (void)removeQueueItem:(nonnull HBQueueItem *)item { NSUInteger index = [self.items indexOfObject:item]; [self removeQueueItemAtIndex:index]; } - (void)revealQueueItem:(nonnull HBQueueItem *)item { [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:@[item.completeOutputURL]]; } - (void)toggleQueueItemHeight:(nonnull HBQueueItem *)item { NSInteger row = [self.items indexOfObject:item]; [self toggleRowsAtIndexes:[NSIndexSet indexSetWithIndex:row] expand:!item.expanded]; } #pragma mark NSTableView delegate - (void)HB_deleteSelectionFromTableView:(NSTableView *)tableView { [self removeSelectedQueueItem:tableView]; } - (void)HB_expandSelectionFromTableView:(NSTableView *)tableView { NSIndexSet *rowIndexes = [self.tableView selectedRowIndexes]; [self toggleRowsAtIndexes:rowIndexes expand:YES]; } - (void)HB_collapseSelectionFromTableView:(NSTableView *)tableView; { NSIndexSet *rowIndexes = [self.tableView selectedRowIndexes]; [self toggleRowsAtIndexes:rowIndexes expand:NO]; } - (BOOL)tableView:(NSTableView *)tableView writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard *)pboard; { NSArray *items = [self.items objectsAtIndexes:rowIndexes]; // Dragging is only allowed of the pending items. if (items[0].state != HBQueueItemStateReady) { return NO; } self.dragNodesArray = items; // Provide data for our custom type, and simple NSStrings. [pboard declareTypes:@[DragDropSimplePboardType] owner:self]; // the actual data doesn't matter since DragDropSimplePboardType drags aren't recognized by anyone but us!. [pboard setData:[NSData data] forType:DragDropSimplePboardType]; return YES; } - (NSDragOperation)tableView:(NSTableView *)tableView validateDrop:(id)info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)dropOperation { // Don't allow dropping ONTO an item since they can't really contain any children. BOOL isOnDropTypeProposal = dropOperation == NSTableViewDropOn; if (isOnDropTypeProposal) { return NSDragOperationNone; } // We do not let the user drop a pending item before or *above* // already finished or currently encoding items. NSInteger encodingRow = [self.items indexOfObject:self.currentItem]; if (encodingRow != NSNotFound && row <= encodingRow) { return NSDragOperationNone; row = MAX(row, encodingRow); } return NSDragOperationMove; } - (BOOL)tableView:(NSTableView *)tableView acceptDrop:(id)info row:(NSInteger)row dropOperation:(NSTableViewDropOperation)dropOperation { [self moveQueueItems:self.dragNodesArray toIndex:row]; return YES; } @end @implementation HBQueueController (TouchBar) @dynamic touchBar; static NSTouchBarItemIdentifier HBTouchBarMain = @"fr.handbrake.queueWindowTouchBar"; static NSTouchBarItemIdentifier HBTouchBarRip = @"fr.handbrake.rip"; static NSTouchBarItemIdentifier HBTouchBarPause = @"fr.handbrake.pause"; - (NSTouchBar *)makeTouchBar { NSTouchBar *bar = [[NSTouchBar alloc] init]; bar.delegate = self; bar.defaultItemIdentifiers = @[HBTouchBarRip, HBTouchBarPause]; bar.customizationIdentifier = HBTouchBarMain; bar.customizationAllowedItemIdentifiers = @[HBTouchBarRip, HBTouchBarPause]; return bar; } - (NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier { if ([identifier isEqualTo:HBTouchBarRip]) { NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; item.customizationLabel = NSLocalizedString(@"Start/Stop Encoding", @"Touch bar"); NSButton *button = [NSButton buttonWithImage:[NSImage imageNamed:NSImageNameTouchBarPlayTemplate] target:self action:@selector(toggleStartCancel:)]; item.view = button; return item; } else if ([identifier isEqualTo:HBTouchBarPause]) { NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; item.customizationLabel = NSLocalizedString(@"Pause/Resume Encoding", @"Touch bar"); NSButton *button = [NSButton buttonWithImage:[NSImage imageNamed:NSImageNameTouchBarPauseTemplate] target:self action:@selector(togglePauseResume:)]; item.view = button; return item; } return nil; } - (void)_touchBar_updateButtonsStateForQueueCore:(HBState)state; { NSButton *ripButton = (NSButton *)[[self.touchBar itemForIdentifier:HBTouchBarRip] view]; NSButton *pauseButton = (NSButton *)[[self.touchBar itemForIdentifier:HBTouchBarPause] view]; if (state == HBStateScanning || state == HBStateWorking || state == HBStateSearching || state == HBStateMuxing) { ripButton.image = [NSImage imageNamed:NSImageNameTouchBarRecordStopTemplate]; pauseButton.image = [NSImage imageNamed:NSImageNameTouchBarPauseTemplate]; } else if (state == HBStatePaused) { ripButton.image = [NSImage imageNamed:NSImageNameTouchBarRecordStopTemplate]; pauseButton.image = [NSImage imageNamed:NSImageNameTouchBarPlayTemplate]; } else if (state == HBStateIdle) { ripButton.image = [NSImage imageNamed:NSImageNameTouchBarPlayTemplate]; pauseButton.image = [NSImage imageNamed:NSImageNameTouchBarPauseTemplate]; } } - (void)_touchBar_validateUserInterfaceItems { for (NSTouchBarItemIdentifier identifier in self.touchBar.itemIdentifiers) { NSTouchBarItem *item = [self.touchBar itemForIdentifier:identifier]; NSView *view = item.view; if ([view isKindOfClass:[NSButton class]]) { NSButton *button = (NSButton *)view; BOOL enabled = [self validateUserIterfaceItemForAction:button.action]; button.enabled = enabled; } } } @end