// --------------------------------------------------------------------------------------------------------------------
//
// This file is part of the HandBrake source code - It may be used under the terms of the GNU General Public License.
//
//
// The Subtitles View Model
//
// --------------------------------------------------------------------------------------------------------------------
namespace HandBrakeWPF.ViewModels
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows;
using Caliburn.Micro;
using HandBrake.Interop.Utilities;
using HandBrakeWPF.EventArgs;
using HandBrakeWPF.Model.Subtitles;
using HandBrakeWPF.Properties;
using HandBrakeWPF.Services.Interfaces;
using HandBrakeWPF.Services.Presets.Model;
using HandBrakeWPF.Services.Scan.Model;
using HandBrakeWPF.ViewModels.Interfaces;
using HandBrakeWPF.Views;
using Microsoft.Win32;
using EncodeTask = HandBrakeWPF.Services.Encode.Model.EncodeTask;
using OutputFormat = HandBrakeWPF.Services.Encode.Model.Models.OutputFormat;
using SubtitleTrack = HandBrakeWPF.Services.Encode.Model.Models.SubtitleTrack;
using SubtitleType = HandBrakeWPF.Services.Encode.Model.Models.SubtitleType;
///
/// The Subtitles View Model
///
public class SubtitlesViewModel : ViewModelBase, ISubtitlesViewModel
{
private readonly IErrorService errorService;
private readonly IWindowManager windowManager;
#region Constants and Fields
private readonly Subtitle foreignAudioSearchTrack;
private IList sourceTracks;
#endregion
#region Constructors and Destructors
///
/// Initializes a new instance of the class.
///
///
/// The Error Service
///
///
/// The window Manager.
///
public SubtitlesViewModel(IErrorService errorService, IWindowManager windowManager)
{
this.errorService = errorService;
this.windowManager = windowManager;
this.SubtitleDefaultsViewModel = new SubtitlesDefaultsViewModel();
this.Task = new EncodeTask();
this.Langauges = LanguageUtilities.MapLanguages().Keys;
this.CharacterCodes = CharCodesUtilities.GetCharacterCodes();
this.foreignAudioSearchTrack = new Subtitle { SubtitleType = SubtitleType.ForeignAudioSearch, Language = Resources.SubtitleViewModel_ForeignAudioSearch };
this.SourceTracks = new List { this.foreignAudioSearchTrack };
}
#endregion
public event EventHandler TabStatusChanged;
#region Properties
///
/// Gets or sets the audio defaults view model.
///
public ISubtitlesDefaultsViewModel SubtitleDefaultsViewModel { get; set; }
///
/// Gets or sets CharacterCodes.
///
public IEnumerable CharacterCodes { get; set; }
///
/// Gets or sets Langauges.
///
public IEnumerable Langauges { get; set; }
///
/// Gets or sets SourceTracks.
///
public IList SourceTracks
{
get
{
return this.sourceTracks;
}
set
{
this.sourceTracks = value;
this.NotifyOfPropertyChange(() => this.SourceTracks);
}
}
///
/// Gets or sets Task.
///
public EncodeTask Task { get; set; }
///
/// Gets the panel title.
///
public string PanelTitle
{
get
{
return Resources.SubtitlesViewModel_SubTracks;
}
}
///
/// Gets the switch display title.
///
public string SwitchDisplayTitle
{
get
{
return Resources.SubtitlesViewModel_ConfigureDefaults;
}
}
///
/// Gets the default audio behaviours.
///
public SubtitleBehaviours SubtitleBehaviours
{
get
{
return this.SubtitleDefaultsViewModel.SubtitleBehaviours;
}
}
public bool IsBurnableOnly
{
get
{
return this.Task.OutputFormat == OutputFormat.WebM;
}
}
#endregion
#region Public Methods
///
/// Add a new Track
///
public void Add()
{
this.Add(null);
}
///
/// Add all closed captions not already on the list.
///
public void AddAllClosedCaptions()
{
foreach (Subtitle subtitle in this.SourceTitlesSubset(null).Where(s => s.SubtitleType == SubtitleType.CC))
{
this.Add(subtitle);
}
}
///
/// Add all the remaining subtitle tracks.
///
public void AddAllRemaining()
{
foreach (Subtitle subtitle in this.SourceTitlesSubset(null))
{
this.Add(subtitle);
}
}
///
/// Add all remaining tracks for the users preferred and selected languages
///
public void AddAllRemainingForSelectedLanguages()
{
List availableTracks = this.GetSelectedLanguagesTracks();
foreach (Subtitle subtitle in this.SourceTitlesSubset(availableTracks))
{
this.Add(subtitle);
}
}
///
/// The add first for selected languages.
///
public void AddFirstForSelectedLanguages()
{
bool anyLanguageSelected = this.SubtitleBehaviours.SelectedLangauges.Contains(Constants.Any);
foreach (Subtitle sourceTrack in this.GetSelectedLanguagesTracks())
{
// Step 2: Check if the track list already contains this track
bool found = this.Task.SubtitleTracks.Any(track => Equals(track.SourceTrack, sourceTrack));
if (!found)
{
// Check if we are already using this language
bool foundLanguage = false;
Subtitle track = sourceTrack;
foreach (var item in this.Task.SubtitleTracks)
{
if (item.SourceTrack != null && item.SourceTrack.LanguageCode != null && track.LanguageCode.Contains(item.SourceTrack.LanguageCode))
{
foundLanguage = true;
}
}
if (foundLanguage)
{
continue;
}
// If it doesn't, add it.
this.Add(sourceTrack);
// If we are using "(Any)" then break here. We only add the first track in this instance.
if (anyLanguageSelected)
{
break;
}
}
}
}
///
/// Import an SRT File.
///
public void Import()
{
OpenFileDialog dialog = new OpenFileDialog
{
Filter = "Subtitle files (*.srt, *.ssa, *.ass)|*.srt;*.ssa;*.ass",
CheckFileExists = true,
Multiselect = true
};
if (this.Task != null && this.Task.Source != null)
{
string path = Path.GetDirectoryName(this.Task.Source);
if (Directory.Exists(path))
{
dialog.InitialDirectory = path;
}
}
dialog.ShowDialog();
this.AddInputSubtitles(dialog.FileNames);
}
public void Import(string[] subtitleFiles)
{
if (subtitleFiles != null && subtitleFiles.Any())
{
this.AddInputSubtitles(subtitleFiles);
}
}
///
/// Remove a Track
///
///
/// The track.
///
public void Remove(SubtitleTrack track)
{
this.Task.SubtitleTracks.Remove(track);
}
///
/// Clear all Tracks
///
public void Clear()
{
this.Task.SubtitleTracks.Clear();
}
///
/// Select the default subtitle track.
///
///
/// The subtitle.
///
public void SelectDefaultTrack(SubtitleTrack subtitle)
{
foreach (SubtitleTrack track in this.Task.SubtitleTracks)
{
if (track == subtitle)
{
continue; // Skip the track the user selected.
}
track.Default = false;
}
this.NotifyOfPropertyChange(() => this.Task);
}
///
/// Select the burned in track.
///
///
/// The subtitle.
///
public void SetBurnedToFalseForAllExcept(SubtitleTrack subtitle)
{
foreach (SubtitleTrack track in this.Task.SubtitleTracks)
{
if (track == subtitle)
{
continue; // Skip the track the user selected.
}
track.Burned = false;
}
this.NotifyOfPropertyChange(() => this.Task);
}
///
/// Automatic Subtitle Selection based on user preferences.
///
public void AutomaticSubtitleSelection()
{
this.Task.SubtitleTracks.Clear();
// Add Foreign Audio Scan
if (this.SubtitleBehaviours.AddForeignAudioScanTrack)
{
this.Add(foreignAudioSearchTrack);
}
// Add Track Behaviours
switch (this.SubtitleBehaviours.SelectedBehaviour)
{
case SubtitleBehaviourModes.FirstMatch: // Adding all remaining tracks
this.AddFirstForSelectedLanguages();
break;
case SubtitleBehaviourModes.AllMatching: // Add Langauges tracks for the additional languages selected, in-order.
this.AddAllRemainingForSelectedLanguages();
break;
}
// Burn In Behaviour
if (this.Task.SubtitleTracks.Count >= 1)
{
bool burnInSet = false;
switch (this.SubtitleBehaviours.SelectedBurnInBehaviour)
{
case SubtitleBurnInBehaviourModes.None:
foreach (var track in this.Task.SubtitleTracks)
{
if (track.SourceTrack.SubtitleType == SubtitleType.ForeignAudioSearch)
{
track.Forced = true;
break;
}
}
break;
case SubtitleBurnInBehaviourModes.ForeignAudio:
foreach (var track in this.Task.SubtitleTracks)
{
// Set the Foreign Audio Track to burned-in
if (track.SourceTrack.SubtitleType == SubtitleType.ForeignAudioSearch)
{
track.Burned = true;
track.Forced = true;
this.SetBurnedToFalseForAllExcept(track);
break;
}
}
break;
case SubtitleBurnInBehaviourModes.FirstTrack:
foreach (var track in this.Task.SubtitleTracks)
{
// Foreign Audio Search is always first in the list.
if (track.SourceTrack.SubtitleType == SubtitleType.ForeignAudioSearch)
{
track.Forced = true;
continue;
}
if (!burnInSet)
{
burnInSet = true;
track.Burned = true;
this.SetBurnedToFalseForAllExcept(track);
}
}
break;
case SubtitleBurnInBehaviourModes.ForeignAudioPreferred:
foreach (var track in this.Task.SubtitleTracks)
{
// Set the first track.
if (!burnInSet)
{
burnInSet = true;
track.Burned = true;
this.SetBurnedToFalseForAllExcept(track);
}
// But if there is a foreign audio track, prefer this to the first.
if (track.SourceTrack.SubtitleType == SubtitleType.ForeignAudioSearch)
{
track.Burned = true;
track.Forced = true;
this.SetBurnedToFalseForAllExcept(track);
break;
}
}
break;
}
}
// Add all closed captions if enabled.
if (this.SubtitleBehaviours.AddClosedCaptions)
{
this.AddAllClosedCaptions();
}
}
///
/// The show audio defaults.
///
public void ShowSubtitleDefaultsPanel()
{
SubtitlesDefaultsView view = new SubtitlesDefaultsView();
view.DataContext = this.SubtitleDefaultsViewModel;
if (view.ShowDialog() == true)
{
this.OnTabStatusChanged(null);
}
}
///
/// Reload the audio tracks based on the defaults.
///
public void ReloadDefaults()
{
this.AutomaticSubtitleSelection();
}
///
/// Trigger a Notify Property Changed on the Task to force various UI elements to update.
///
public void RefreshTask()
{
this.NotifyOfPropertyChange(() => this.Task);
this.NotifyOfPropertyChange(() => this.IsBurnableOnly);
if (this.IsBurnableOnly)
{
foreach (var subtitleTrack in this.Task.SubtitleTracks)
{
if (subtitleTrack.Default)
{
subtitleTrack.Default = false;
}
}
}
}
#endregion
#region Implemented Interfaces
///
/// Setup this tab for the specified preset.
///
///
/// The preset.
///
///
/// The task.
///
public void SetPreset(Preset preset, EncodeTask task)
{
// Note, We don't support Subtitles in presets yet.
this.Task = task;
this.NotifyOfPropertyChange(() => this.Task);
this.SubtitleDefaultsViewModel.SetupLanguages(preset);
this.AutomaticSubtitleSelection();
}
///
/// Update all the UI controls based on the encode task passed in.
///
///
/// The task.
///
public void UpdateTask(EncodeTask task)
{
this.Task = task;
this.NotifyOfPropertyChange(() => this.Task.SubtitleTracks);
this.NotifyOfPropertyChange(() => this.Task);
}
public bool MatchesPreset(Preset preset)
{
// Check the default behaviours.
if (preset.SubtitleTrackBehaviours.AddClosedCaptions != this.SubtitleBehaviours.AddClosedCaptions)
{
return false;
}
if (preset.SubtitleTrackBehaviours.AddForeignAudioScanTrack != this.SubtitleBehaviours.AddForeignAudioScanTrack)
{
return false;
}
if (preset.SubtitleTrackBehaviours.SelectedBehaviour != this.SubtitleBehaviours.SelectedBehaviour)
{
return false;
}
if (preset.SubtitleTrackBehaviours.SelectedBurnInBehaviour != this.SubtitleBehaviours.SelectedBurnInBehaviour)
{
return false;
}
foreach (var item in this.SubtitleBehaviours.SelectedLangauges)
{
if (!preset.SubtitleTrackBehaviours.SelectedLangauges.Contains(item))
{
return false;
}
}
return true;
}
///
/// Setup this window for a new source
///
///
/// The source.
///
///
/// The title.
///
///
/// The preset.
///
///
/// The task.
///
public void SetSource(Source source, Title title, Preset preset, EncodeTask task)
{
this.SourceTracks.Clear();
this.SourceTracks.Add(foreignAudioSearchTrack);
foreach (Subtitle subtitle in title.Subtitles)
{
this.SourceTracks.Add(subtitle);
}
this.Task = task;
this.NotifyOfPropertyChange(() => this.Task);
this.AutomaticSubtitleSelection();
}
///
/// Checks the configuration of the subtitles and warns the user about any potential issues.
///
public bool ValidateSubtitles()
{
var nonBurnedSubtitles = this.Task.SubtitleTracks.Where(subtitleTrack => !subtitleTrack.Burned).ToList();
if (nonBurnedSubtitles.Count > 0 && this.IsBurnableOnly)
{
MessageBoxResult result = this.errorService.ShowMessageBox(
Resources.Subtitles_WebmSubtitleIncompatibilityError,
Resources.Subtitles_WebmSubtitleIncompatibilityHeader,
MessageBoxButton.OKCancel,
MessageBoxImage.Warning);
if (result == MessageBoxResult.OK)
{
foreach (var subtitleTrack in nonBurnedSubtitles)
{
if (!subtitleTrack.Burned)
{
this.Remove(subtitleTrack);
}
}
}
else if (result == MessageBoxResult.Cancel)
{
return false;
}
else
{
return false;
}
}
return true;
}
#endregion
#region Methods
protected virtual void OnTabStatusChanged(TabStatusEventArgs e)
{
this.TabStatusChanged?.Invoke(this, e);
}
///
/// Add a subtitle track.
/// The Source track is set based on the following order. If null, it will skip to the next option.
/// 1. Passed in Subitle param
/// 2. First preferred Subtitle from source
/// 3. First subtitle from source.
/// Will not add a subtitle if the source has none.
///
///
/// The subtitle. Use null to add preferred, or first from source (based on user preference)
///
private void Add(Subtitle subtitle)
{
Subtitle source = subtitle
?? ((this.SourceTracks != null)
? (this.SourceTracks.FirstOrDefault(l => l.Language == this.GetPreferredSubtitleTrackLanguage())
?? this.SourceTracks.FirstOrDefault(
s => s.SubtitleType != SubtitleType.ForeignAudioSearch))
: null);
if (source == null)
{
source = foreignAudioSearchTrack;
}
SubtitleTrack track = new SubtitleTrack
{
SubtitleType = source.SubtitleType,
SourceTrack = source,
};
// Burn-in Behaviours
if (this.SubtitleBehaviours.SelectedBurnInBehaviour == SubtitleBurnInBehaviourModes.ForeignAudio
|| this.SubtitleBehaviours.SelectedBurnInBehaviour == SubtitleBurnInBehaviourModes.ForeignAudioPreferred)
{
if (subtitle != null && subtitle.SubtitleType == SubtitleType.ForeignAudioSearch)
{
track.Burned = true;
this.SetBurnedToFalseForAllExcept(track);
}
}
// For MP4, PGS Subtitles must be burned in.
if (!track.Burned && (source.SubtitleType == SubtitleType.PGS) && this.Task != null && this.Task.OutputFormat == OutputFormat.Mp4)
{
if (this.Task.SubtitleTracks.Any(a => a.Burned))
{
return; // We can't add any more burned in tracks.
}
if (track.CanBeBurned)
{
track.Burned = true;
this.SetBurnedToFalseForAllExcept(track);
}
}
var encodeTask = this.Task;
if (encodeTask != null)
{
encodeTask.SubtitleTracks.Add(track);
}
}
///
/// Gets a list of source tracks for the users selected languages.
///
///
/// A list of source subtitle tracks.
///
private List GetSelectedLanguagesTracks()
{
// Translate to Iso Codes
List iso6392Codes = new List();
if (this.SubtitleBehaviours.SelectedLangauges.Contains(Constants.Any))
{
iso6392Codes = LanguageUtilities.GetIsoCodes();
iso6392Codes = LanguageUtilities.OrderIsoCodes(iso6392Codes, this.SubtitleBehaviours.SelectedLangauges);
}
else
{
iso6392Codes = LanguageUtilities.GetLanguageCodes(this.SubtitleBehaviours.SelectedLangauges.ToArray());
}
List orderedSubtitles = new List();
foreach (string code in iso6392Codes)
{
orderedSubtitles.AddRange(this.SourceTracks.Where(subtitle => subtitle.LanguageCodeClean == code ));
}
return orderedSubtitles;
}
///
/// The get preferred subtitle track, or the first if none available.
///
///
/// The users preferred language, or the first if none available.
///
private string GetPreferredSubtitleTrackLanguage()
{
string langName = this.SubtitleBehaviours.SelectedLangauges.FirstOrDefault(w => w != Constants.Any);
string langCode = LanguageUtilities.GetLanguageCode(langName);
return langCode;
}
///
/// Gets a list of Source subtitle tracks that are not currently used.
///
///
/// The subtitles. (Optional). If null, works on the full source subtitle collection
///
///
/// An IEnumerable collection of subtitles
///
private IEnumerable SourceTitlesSubset(IEnumerable subtitles)
{
return subtitles != null
? subtitles.Where(subtitle => !this.Task.SubtitleTracks.Any(track => Equals(track.SourceTrack, subtitle))).ToList()
: this.SourceTracks.Where(subtitle => !this.Task.SubtitleTracks.Any(track => Equals(track.SourceTrack, subtitle))).ToList();
}
private void AddInputSubtitles(string[] filenames)
{
foreach (var srtFile in filenames)
{
if (!File.Exists(srtFile))
{
continue;
}
SubtitleTrack track = new SubtitleTrack
{
SrtFileName = Path.GetFileNameWithoutExtension(srtFile),
SrtOffset = 0,
SrtCharCode = "UTF-8",
SrtLang = "English",
SubtitleType = SubtitleType.SRT,
SrtPath = srtFile
};
this.Task.SubtitleTracks.Add(track);
}
}
#endregion
}
}