/* $Id: HBSubtitles.m This file is part of the HandBrake source code. Homepage: . It may be used under the terms of the GNU General Public License. */ #import "HBSubtitlesController.h" #import "HBSubtitlesDefaultsController.h" #import "HBSubtitles.h" #import "HBSubtitlesDefaults.h" #include "hb.h" #include "lang.h" static void *HBSubtitlesControllerContext = &HBSubtitlesControllerContext; @interface HBSubtitlesController () // IBOutles @property (unsafe_unretained) IBOutlet NSTableView *fTableView; // Defaults @property (nonatomic, readwrite, strong) HBSubtitlesDefaultsController *defaultsController; // Cached table view's cells @property (nonatomic, readonly) NSPopUpButtonCell *languagesCell; @property (nonatomic, readonly) NSPopUpButtonCell *encodingsCell; @end @implementation HBSubtitlesController - (instancetype)init { self = [super initWithNibName:@"Subtitles" bundle:nil]; [self addObserver:self forKeyPath:@"self.subtitles.tracks" options:NSKeyValueObservingOptionInitial context:HBSubtitlesControllerContext]; return self; } #pragma mark - KVO - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == HBSubtitlesControllerContext) { // We use KVO to update the table manually // because this table isn't using bindings if ([keyPath isEqualToString:@"self.subtitles.tracks"]) { [self.fTableView reloadData]; } } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } - (void)setSubtitles:(HBSubtitles *)subtitles { _subtitles = subtitles; [self.fTableView reloadData]; } #pragma mark - Actions - (BOOL)validateUserInterfaceItem:(id < NSValidatedUserInterfaceItem >)anItem { return (self.subtitles != nil); } /** * Add every subtitles track that still isn't in the subtitles array. */ - (IBAction)addAll:(id)sender { [self.subtitles addAllTracks]; [self.fTableView reloadData]; } /** * Remove all the subtitles tracks. */ - (IBAction)removeAll:(id)sender { [self.subtitles removeAll]; [self.fTableView reloadData]; } /** * Remove all the subtitles tracks and * add new ones based on the defaults settings */ - (IBAction)addTracksFromDefaults:(id)sender { [self.subtitles reloadDefaults]; [self.fTableView reloadData]; } - (IBAction)showSettingsSheet:(id)sender { self.defaultsController = [[HBSubtitlesDefaultsController alloc] initWithSettings:self.subtitles.defaults]; [NSApp beginSheet:[self.defaultsController window] modalForWindow:[[self view] window] modalDelegate:self didEndSelector:@selector(sheetDidEnd) contextInfo:NULL]; } - (void)sheetDidEnd { self.defaultsController = nil; } #pragma mark - Subtitles tracks creation and validation /** * Checks whether any subtitles in the list cannot be passed through. * Set the first of any such subtitles to burned-in, remove the others. */ - (void)validatePassthru { [self.subtitles validatePassthru]; [self.fTableView reloadData]; } - (void)validateBurned:(NSInteger)index { [self.subtitles.tracks enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { if (idx != index) { obj[keySubTrackBurned] = @0; } }]; [self validatePassthru]; } - (void)validateDefault:(NSInteger)index { [self.subtitles.tracks enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { if (idx != index) { obj[keySubTrackDefault] = @0; } }]; } #pragma mark - #pragma mark Subtitle Table Data Source Methods - (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView { return self.subtitles.tracks.count; } - (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex { NSDictionary *track = self.subtitles.tracks[rowIndex]; if ([[aTableColumn identifier] isEqualToString:@"track"]) { NSNumber *index = track[keySubTrackSelectionIndex]; if (index) return index; else return @0; } else if ([[aTableColumn identifier] isEqualToString:@"forced"]) { return track[keySubTrackForced]; } else if ([[aTableColumn identifier] isEqualToString:@"burned"]) { return track[keySubTrackBurned]; } else if ([[aTableColumn identifier] isEqualToString:@"default"]) { return track[keySubTrackDefault]; } /* These next three columns only apply to srt's. they are disabled for source subs */ else if ([[aTableColumn identifier] isEqualToString:@"srt_lang"]) { if ([track[keySubTrackType] intValue] == SRTSUB) { return track[keySubTrackLanguageIndex]; } else { return @0; } } else if ([[aTableColumn identifier] isEqualToString:@"srt_charcode"]) { if ([track[keySubTrackType] intValue] == SRTSUB) { return track[keySubTrackSrtCharCodeIndex]; } else { return @0; } } else if ([[aTableColumn identifier] isEqualToString:@"srt_offset"]) { if (track[keySubTrackSrtOffset]) { return [track[keySubTrackSrtOffset] stringValue]; } else { return @"0"; } } return nil; } /** * Called whenever a widget in the table is edited or changed, we use it to record the change in the controlling array * including removing and adding new tracks via the "None" ("track" index of 0) */ - (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex { if ([[aTableColumn identifier] isEqualToString:@"track"]) { /* Set the array to track if we are vobsub (picture sub) */ if ([anObject intValue] > 0) { NSMutableDictionary *newTrack = [self.subtitles trackFromSourceTrackIndex:[anObject integerValue] - 1 - (rowIndex == 0)]; // Selection index calculation newTrack[keySubTrackSelectionIndex] = @([anObject integerValue]); self.subtitles.tracks[rowIndex] = newTrack; } } else if ([[aTableColumn identifier] isEqualToString:@"forced"]) { self.subtitles.tracks[rowIndex][keySubTrackForced] = @([anObject intValue]); } else if ([[aTableColumn identifier] isEqualToString:@"burned"]) { self.subtitles.tracks[rowIndex][keySubTrackBurned] = @([anObject intValue]); if([anObject intValue] == 1) { /* Burned In and Default are mutually exclusive */ self.subtitles.tracks[rowIndex][keySubTrackDefault] = @0; } /* now we need to make sure no other tracks are set to burned if we have set burned */ if ([anObject intValue] == 1) { [self validateBurned:rowIndex]; } } else if ([[aTableColumn identifier] isEqualToString:@"default"]) { self.subtitles.tracks[rowIndex][keySubTrackDefault] = @([anObject intValue]); if([anObject intValue] == 1) { /* Burned In and Default are mutually exclusive */ self.subtitles.tracks[rowIndex][keySubTrackBurned] = @0; } /* now we need to make sure no other tracks are set to default */ if ([anObject intValue] == 1) { [self validateDefault:rowIndex]; } } /* These next three columns only apply to srt's. they are disabled for source subs */ else if ([[aTableColumn identifier] isEqualToString:@"srt_lang"]) { self.subtitles.tracks[rowIndex][keySubTrackLanguageIndex] = @([anObject intValue]); self.subtitles.tracks[rowIndex][keySubTrackLanguageIsoCode] = self.subtitles.languagesArray[[anObject intValue]][1]; } else if ([[aTableColumn identifier] isEqualToString:@"srt_charcode"]) { /* charCodeArray */ self.subtitles.tracks[rowIndex][keySubTrackSrtCharCodeIndex] = @([anObject intValue]); self.subtitles.tracks[rowIndex][keySubTrackSrtCharCode] = self.subtitles.charCodeArray[[anObject intValue]]; } else if ([[aTableColumn identifier] isEqualToString:@"srt_offset"]) { self.subtitles.tracks[rowIndex][keySubTrackSrtOffset] = @([anObject integerValue]); } /* now lets do a bit of logic to add / remove tracks as necessary via the "None" track (index 0) */ if ([[aTableColumn identifier] isEqualToString:@"track"]) { /* Since currently no quicktime based playback devices support soft vobsubs in mp4, we make sure "burned in" is specified * by default to avoid massive confusion and anarchy. However we also want to guard against multiple burned in subtitle tracks * as libhb would ignore all but the first one anyway. Plus it would probably be stupid. */ if ((self.subtitles.container & HB_MUX_MASK_MP4) && ([anObject intValue] != 0)) { if ([self.subtitles.tracks[rowIndex][keySubTrackType] intValue] == VOBSUB) { /* lets see if there are currently any burned in subs specified */ BOOL subtrackBurnedInFound = NO; for (id tempObject in self.subtitles.tracks) { if ([tempObject[keySubTrackBurned] intValue] == 1) { subtrackBurnedInFound = YES; } } /* if we have no current vobsub set to burn it in ... burn it in by default */ if (!subtrackBurnedInFound) { self.subtitles.tracks[rowIndex][keySubTrackBurned] = @1; /* Burned In and Default are mutually exclusive */ self.subtitles.tracks[rowIndex][keySubTrackDefault] = @0; } } } /* We use the track popup index number (presumes index 0 is "None" which is ignored and only used to remove tracks if need be) * to determine whether to 1 modify an existing track, 2. add a new empty "None" track or 3. remove an existing track. */ if ([anObject intValue] != 0 && rowIndex == [self.subtitles.tracks count] - 1) // if we have a last track which != "None" { /* add a new empty None track */ [self.subtitles.tracks addObject:[self.subtitles createSubtitleTrack]]; } else if ([anObject intValue] == 0 && rowIndex != ([self.subtitles.tracks count] -1))// if this track is set to "None" and not the last track displayed { /* we know the user chose to remove this track by setting it to None, so remove it from the array */ /* However,if this is the first track we have to reset the selected index of the next track by + 1, since it will now become * the first track, which has to account for the extra "Foreign Language Search" index. */ if (rowIndex == 0 && [self.subtitles.tracks[1][keySubTrackSelectionIndex] intValue] != 0) { /* get the index of the selection in row one (which is track two) */ int trackOneSelectedIndex = [self.subtitles.tracks[1][keySubTrackSelectionIndex] intValue]; /* increment the index of the subtitle menu item by one, to account for Foreign Language Search which is unique to the first track */ self.subtitles.tracks[1][keySubTrackSelectionIndex] = @(trackOneSelectedIndex + 1); } /* now that we have made the adjustment for track one (index 0) go ahead and delete the track */ [self.subtitles.tracks removeObjectAtIndex: rowIndex]; } // Validate the current passthru tracks. [self validatePassthru]; } [aTableView reloadData]; } #pragma mark - #pragma mark Subtitle Table Delegate Methods - (NSCell *)tableView:(NSTableView *)tableView dataCellForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex { if ([[tableColumn identifier] isEqualToString:@"track"]) { // 'track' is a popup of all available source subtitle tracks for the given title NSPopUpButtonCell *cellTrackPopup = [[NSPopUpButtonCell alloc] init]; [cellTrackPopup setControlSize:NSSmallControlSize]; [cellTrackPopup setFont:[NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSSmallControlSize]]]; // Add our initial "None" track which we use to add source tracks or remove tracks. // "None" is always index 0. [[cellTrackPopup menu] addItemWithTitle:@"None" action:NULL keyEquivalent:@""]; // Foreign Audio Search (index 1 in the popup) is only available for the first track if (rowIndex == 0) { [[cellTrackPopup menu] addItemWithTitle:self.subtitles.foreignAudioSearchTrackName action:NULL keyEquivalent:@""]; } for (NSDictionary *track in self.subtitles.masterTrackArray) { [[cellTrackPopup menu] addItemWithTitle:track[keySubTrackName] action:NULL keyEquivalent:@""]; } return cellTrackPopup; } else if ([[tableColumn identifier] isEqualToString:@"srt_lang"]) { return self.languagesCell; } else if ([[tableColumn identifier] isEqualToString:@"srt_charcode"]) { return self.encodingsCell; } return nil; } /** * Enables/Disables the table view cells. */ - (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex { if ([[aTableColumn identifier] isEqualToString:@"track"]) { return; } // If the Track is None, we disable the other cells as None is an empty track if ([self.subtitles.tracks[rowIndex][keySubTrackSelectionIndex] intValue] == 0) { [aCell setEnabled:NO]; } else { // Since we have a valid track, we go ahead and enable the rest of the widgets and set them according to the controlling array */ [aCell setEnabled:YES]; } if ([[aTableColumn identifier] isEqualToString:@"forced"]) { // Disable the "Forced Only" checkbox if a) the track is "None" or b) the subtitle track doesn't support forced flags if (![self.subtitles.tracks[rowIndex][keySubTrackSelectionIndex] intValue] || !hb_subtitle_can_force([self.subtitles.tracks[rowIndex][keySubTrackType] intValue])) { [aCell setEnabled:NO]; } else { [aCell setEnabled:YES]; } } else if ([[aTableColumn identifier] isEqualToString:@"burned"]) { /* * Disable the "Burned In" checkbox if: * a) the track is "None" OR * b) the subtitle track can't be burned in OR * c) the subtitle track can't be passed through (e.g. PGS w/MP4) */ int subtitleTrackType = [self.subtitles.tracks[rowIndex][keySubTrackType] intValue]; if (![self.subtitles.tracks[rowIndex][keySubTrackSelectionIndex] intValue] || !hb_subtitle_can_burn(subtitleTrackType) || !hb_subtitle_can_pass(subtitleTrackType, self.subtitles.container)) { [aCell setEnabled:NO]; } else { [aCell setEnabled:YES]; } } else if ([[aTableColumn identifier] isEqualToString:@"default"]) { /* * Disable the "Default" checkbox if: * a) the track is "None" OR * b) the subtitle track can't be passed through (e.g. PGS w/MP4) */ if (![self.subtitles.tracks[rowIndex][keySubTrackSelectionIndex] intValue] || !hb_subtitle_can_pass([self.subtitles.tracks[rowIndex][keySubTrackType] intValue], self.subtitles.container)) { [aCell setEnabled:NO]; } else { [aCell setEnabled:YES]; } } /* These next three columns only apply to srt's. they are disabled for source subs */ else if ([[aTableColumn identifier] isEqualToString:@"srt_lang"]) { /* We have an srt file so set the track type (Source or SRT, and the srt file path ) kvp's*/ if ([self.subtitles.tracks[rowIndex][keySubTrackType] intValue] == SRTSUB) { [aCell setEnabled:YES]; } else { [aCell setEnabled:NO]; } } else if ([[aTableColumn identifier] isEqualToString:@"srt_charcode"]) { /* We have an srt file so set the track type (Source or SRT, and the srt file path ) kvp's*/ if ([self.subtitles.tracks[rowIndex][keySubTrackType] intValue] == SRTSUB) { [aCell setEnabled:YES]; } else { [aCell setEnabled:NO]; } } else if ([[aTableColumn identifier] isEqualToString:@"srt_offset"]) { if ([self.subtitles.tracks[rowIndex][keySubTrackType] intValue] == SRTSUB) { [aCell setEnabled:YES]; } else { [aCell setEnabled:NO]; } } } #pragma mark - Srt import /** * Imports a srt file. * * @param sender */ - (IBAction)browseImportSrtFile:(id)sender { NSOpenPanel *panel = [NSOpenPanel openPanel]; [panel setAllowsMultipleSelection:NO]; [panel setCanChooseFiles:YES]; [panel setCanChooseDirectories:NO]; NSURL *sourceDirectory; if ([[NSUserDefaults standardUserDefaults] URLForKey:@"LastSrtImportDirectoryURL"]) { sourceDirectory = [[NSUserDefaults standardUserDefaults] URLForKey:@"LastSrtImportDirectoryURL"]; } else { sourceDirectory = [[NSURL fileURLWithPath:NSHomeDirectory()] URLByAppendingPathComponent:@"Desktop"]; } /* we open up the browse srt sheet here and call for browseImportSrtFileDone after the sheet is closed */ NSArray *fileTypes = @[@"plist", @"srt"]; [panel setDirectoryURL:sourceDirectory]; [panel setAllowedFileTypes:fileTypes]; [panel beginSheetModalForWindow:[[self view] window] completionHandler:^(NSInteger result) { if (result == NSOKButton) { NSURL *importSrtFileURL = [panel URL]; NSURL *importSrtDirectory = [importSrtFileURL URLByDeletingLastPathComponent]; [[NSUserDefaults standardUserDefaults] setURL:importSrtDirectory forKey:@"LastSrtImportDirectoryURL"]; /* Create a new entry for the subtitle source array so it shows up in our subtitle source list */ NSString *displayname = [importSrtFileURL lastPathComponent];// grok an appropriate display name from the srt subtitle */ /* create a dictionary of source subtitle information to store in our array */ [self.subtitles.masterTrackArray addObject:@{keySubTrackIndex: @(self.subtitles.masterTrackArray.count), keySubTrackName: displayname, keySubTrackType: @(SRTSUB), keySubTrackSrtFilePath: importSrtFileURL.path}]; // Now create a new srt subtitle dictionary assuming the user wants to add it to their list NSMutableDictionary *newSubtitleSrtTrack = [self.subtitles trackFromSourceTrackIndex:self.subtitles.masterTrackArray.count - 1]; // Calculate the pop up selection index newSubtitleSrtTrack[keySubTrackSelectionIndex] = @(self.subtitles.masterTrackArray.count + (self.subtitles.tracks.count == 1)); [self.subtitles.tracks insertObject:newSubtitleSrtTrack atIndex:self.subtitles.tracks.count - 1]; [self.fTableView reloadData]; } }]; } #pragma mark - UI cells @synthesize languagesCell = _languagesCell; - (NSPopUpButtonCell *)languagesCell { if (!_languagesCell) { // 'srt_lang' is a popup of commonly used languages to be matched to the source srt file _languagesCell = [[NSPopUpButtonCell alloc] init]; // Set the Popups properties [_languagesCell setControlSize:NSSmallControlSize]; [_languagesCell setFont:[NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSSmallControlSize]]]; // list our languages as per the languagesArray for (NSArray *lang in self.subtitles.languagesArray) { [[_languagesCell menu] addItemWithTitle:lang[0] action:NULL keyEquivalent:@""]; } } return _languagesCell; } @synthesize encodingsCell = _encodingsCell; - (NSPopUpButtonCell *)encodingsCell { if (!_encodingsCell) { // 'srt_charcode' is a popup of commonly used character codes to be matched to the source srt file _encodingsCell = [[NSPopUpButtonCell alloc] init]; // Set the Popups properties [_encodingsCell setControlSize:NSSmallControlSize]; [_encodingsCell setFont:[NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSSmallControlSize]]]; // list our character codes, as per charCodeArray for (NSString *charCode in self.subtitles.charCodeArray) { [[_encodingsCell menu] addItemWithTitle:charCode action: NULL keyEquivalent: @""]; } } return _encodingsCell; } @end