// -------------------------------------------------------------------------------------------------------------------- // // This file is part of the HandBrake source code - It may be used under the terms of the GNU General Public License. // // // The Picture Settings View Model // // -------------------------------------------------------------------------------------------------------------------- namespace HandBrakeWPF.ViewModels { using System; using System.Collections.Generic; using System.Globalization; using HandBrake.ApplicationServices.Interop.Model; using HandBrake.ApplicationServices.Interop.Model.Encoding; using HandBrakeWPF.Helpers; using HandBrakeWPF.Properties; using HandBrakeWPF.Services.Presets.Model; using HandBrakeWPF.Services.Scan.Model; using HandBrakeWPF.Utilities; using HandBrakeWPF.ViewModels.Interfaces; using EncodeTask = HandBrakeWPF.Services.Encode.Model.EncodeTask; using PresetPictureSettingsMode = HandBrakeWPF.Model.Picture.PresetPictureSettingsMode; /// /// The Picture Settings View Model /// public class PictureSettingsViewModel : ViewModelBase, IPictureSettingsViewModel { /* * TODO: * - We are not handling cropping correctly within the UI. * - The Height is not correctly set when using no Anamorphic * - Maintain Aspect ratio needs corrected. * - Custom Anamorphic. * */ #region Constants and Fields /// /// The display size. /// private string displaySize; /// /// Backing field for for height control enabled /// private bool heightControlEnabled = true; /// /// Backing field for show custom anamorphic controls /// private bool showCustomAnamorphicControls; /// /// The source info. /// private string sourceInfo; /// /// Source Par Values /// private Size sourceParValues; /// /// Source Resolution /// private Size sourceResolution; /// /// Backing field for width control enabled. /// private bool widthControlEnabled = true; /// /// Backing field for the show modulus field /// private bool showModulus; /// /// Backing field for showing display size. /// private bool showDisplaySize; /// /// Backing field for max height /// private int maxHeight; /// /// Backing field for max width /// private int maxWidth; /// /// The show keep ar backing field. /// private bool showKeepAr = true; /// /// The delayed previewprocessor. /// private DelayedActionProcessor delayedPreviewprocessor = new DelayedActionProcessor(); /// /// The current title. /// private Title currentTitle; /// /// The scanned source. /// private Source scannedSource; #endregion #region Constructors and Destructors /// /// Initializes a new instance of the class. /// public PictureSettingsViewModel() { this.sourceResolution = new Size(0, 0); this.Task = new EncodeTask(); this.Init(); } #endregion #region Properties /// /// Gets or sets the static preview view model. /// public IStaticPreviewViewModel StaticPreviewViewModel { get; set; } /// /// Gets AnamorphicModes. /// public IEnumerable AnamorphicModes { get { return new List { Anamorphic.None, Anamorphic.Automatic, Anamorphic.Loose }; // , Anamorphic.Custom TODO Re-enable one the UI is re-worked. } } /// /// Gets or sets DisplaySize. /// public string DisplaySize { get { return this.displaySize; } set { this.displaySize = value; this.NotifyOfPropertyChange(() => this.DisplaySize); } } /// /// Gets or sets a value indicating whether HeightControlEnabled. /// public bool HeightControlEnabled { get { return this.heightControlEnabled; } set { this.heightControlEnabled = value; this.NotifyOfPropertyChange(() => this.HeightControlEnabled); } } /// /// Gets ModulusValues. /// public IEnumerable ModulusValues { get { return new List { 16, 8, 4, 2 }; } } /// /// Gets or sets a value indicating whether ShowCustomAnamorphicControls. /// public bool ShowCustomAnamorphicControls { get { return this.showCustomAnamorphicControls; } set { this.showCustomAnamorphicControls = value; this.NotifyOfPropertyChange(() => this.ShowCustomAnamorphicControls); } } /// /// Gets or sets SourceInfo. /// public string SourceInfo { get { return this.sourceInfo; } set { this.sourceInfo = value; this.NotifyOfPropertyChange(() => this.SourceInfo); } } /// /// Gets or sets Task. /// public EncodeTask Task { get; set; } /// /// Gets or sets a value indicating whether WidthControlEnabled. /// public bool WidthControlEnabled { get { return this.widthControlEnabled; } set { this.widthControlEnabled = value; this.NotifyOfPropertyChange(() => this.WidthControlEnabled); } } /// /// Gets or sets a value indicating whether ShowModulus. /// public bool ShowModulus { get { return this.showModulus; } set { this.showModulus = value; this.NotifyOfPropertyChange(() => this.ShowModulus); } } /// /// Gets or sets a value indicating whether ShowDisplaySize. /// public bool ShowDisplaySize { get { return this.showDisplaySize; } set { this.showDisplaySize = value; this.NotifyOfPropertyChange(() => this.ShowDisplaySize); } } /// /// Gets or sets MaxHeight. /// public int MaxHeight { get { return this.maxHeight; } set { this.maxHeight = value; this.NotifyOfPropertyChange(() => this.MaxHeight); } } /// /// Gets or sets MinHeight. /// public int MaxWidth { get { return this.maxWidth; } set { this.maxWidth = value; this.NotifyOfPropertyChange(() => this.MaxWidth); } } /// /// Gets or sets a value indicating whether show keep ar. /// public bool ShowKeepAR { get { return this.showKeepAr; } set { this.showKeepAr = value; this.NotifyOfPropertyChange(() => this.ShowKeepAR); } } #endregion #region Task Properties /// /// Gets or sets CropBottom. /// public int CropBottom { get { return this.Task.Cropping.Bottom; } set { this.Task.Cropping.Bottom = value; this.NotifyOfPropertyChange(() => this.CropBottom); this.RecaulcatePictureSettingsProperties(ChangedPictureField.Crop); } } /// /// Gets or sets CropLeft. /// public int CropLeft { get { return this.Task.Cropping.Left; } set { this.Task.Cropping.Left = value; this.NotifyOfPropertyChange(() => this.CropLeft); this.RecaulcatePictureSettingsProperties(ChangedPictureField.Crop); } } /// /// Gets or sets CropRight. /// public int CropRight { get { return this.Task.Cropping.Right; } set { this.Task.Cropping.Right = value; this.NotifyOfPropertyChange(() => this.CropRight); this.RecaulcatePictureSettingsProperties(ChangedPictureField.Crop); } } /// /// Gets or sets CropTop. /// public int CropTop { get { return this.Task.Cropping.Top; } set { this.Task.Cropping.Top = value; this.NotifyOfPropertyChange(() => this.CropTop); this.RecaulcatePictureSettingsProperties(ChangedPictureField.Crop); } } /// /// Gets or sets a value indicating whether IsCustomCrop. /// public bool IsCustomCrop { get { return this.Task.HasCropping; } set { this.Task.HasCropping = value; this.NotifyOfPropertyChange(() => this.IsCustomCrop); } } /// /// Gets or sets DisplayWidth. /// public int DisplayWidth { get { return this.Task.DisplayWidth.HasValue ? int.Parse(Math.Round(this.Task.DisplayWidth.Value, 0).ToString(CultureInfo.InvariantCulture)) : 0; } set { if (!object.Equals(this.Task.DisplayWidth, value)) { this.Task.DisplayWidth = value; this.NotifyOfPropertyChange(() => this.DisplayWidth); this.RecaulcatePictureSettingsProperties(ChangedPictureField.DisplayWidth); } } } /// /// Gets or sets Width. /// public int Width { get { return this.Task.Width.HasValue ? this.Task.Width.Value : 0; } set { if (!object.Equals(this.Task.Width, value)) { this.Task.Width = value; this.NotifyOfPropertyChange(() => this.Width); this.RecaulcatePictureSettingsProperties(ChangedPictureField.Width); } } } /// /// Gets or sets Height. /// public int Height { get { return this.Task.Height.HasValue ? this.Task.Height.Value : 0; } set { if (!object.Equals(this.Task.Height, value)) { this.Task.Height = value; this.NotifyOfPropertyChange(() => this.Height); this.RecaulcatePictureSettingsProperties(ChangedPictureField.Height); } } } /// /// Gets or sets a value indicating whether MaintainAspectRatio. /// public bool MaintainAspectRatio { get { return this.Task.KeepDisplayAspect; } set { this.Task.KeepDisplayAspect = value; this.NotifyOfPropertyChange(() => this.MaintainAspectRatio); this.RecaulcatePictureSettingsProperties(ChangedPictureField.MaintainAspectRatio); } } /// /// Gets or sets ParHeight. /// public int ParHeight { get { return this.Task.PixelAspectY; } set { if (!object.Equals(this.Task.PixelAspectY, value)) { this.Task.PixelAspectY = value; this.NotifyOfPropertyChange(() => this.ParHeight); this.RecaulcatePictureSettingsProperties(ChangedPictureField.ParH); } } } /// /// Gets or sets ParWidth. /// public int ParWidth { get { return this.Task.PixelAspectX; } set { if (!object.Equals(this.Task.PixelAspectX, value)) { this.Task.PixelAspectX = value; this.NotifyOfPropertyChange(() => this.ParWidth); this.RecaulcatePictureSettingsProperties(ChangedPictureField.ParW); } } } /// /// Gets or sets SelectedAnamorphicMode. /// public Anamorphic SelectedAnamorphicMode { get { return this.Task.Anamorphic; } set { if (!object.Equals(this.SelectedAnamorphicMode, value)) { this.Task.Anamorphic = value; this.NotifyOfPropertyChange(() => this.SelectedAnamorphicMode); this.RecaulcatePictureSettingsProperties(ChangedPictureField.Anamorphic); } } } /// /// Gets or sets SelectedModulus. /// public int? SelectedModulus { get { return this.Task.Modulus; } set { this.Task.Modulus = value; this.NotifyOfPropertyChange(() => this.SelectedModulus); this.RecaulcatePictureSettingsProperties(ChangedPictureField.Modulus); } } #endregion #region Public Methods /// /// Setup this tab for the specified preset. /// /// /// The preset. /// /// /// The task. /// public void SetPreset(Preset preset, EncodeTask task) { this.Task = task; // Handle built-in presets. if (preset.IsBuildIn) { preset.PictureSettingsMode = PresetPictureSettingsMode.Custom; } // Setup the Picture Sizes switch (preset.PictureSettingsMode) { default: case PresetPictureSettingsMode.Custom: case PresetPictureSettingsMode.SourceMaximum: // Anamorphic Mode this.SelectedAnamorphicMode = preset.Task.Anamorphic; // Modulus if (preset.Task.Modulus.HasValue) { this.SelectedModulus = preset.Task.Modulus; } // Set the Maintain Aspect ratio. this.MaintainAspectRatio = preset.Task.KeepDisplayAspect; // Set the Maximum so libhb can correctly manage the size. if (preset.PictureSettingsMode == PresetPictureSettingsMode.SourceMaximum) { this.MaxWidth = this.sourceResolution.Width; this.MaxHeight = this.sourceResolution.Height; } else { this.MaxWidth = preset.Task.MaxWidth ?? this.sourceResolution.Width; this.MaxHeight = preset.Task.MaxHeight ?? this.sourceResolution.Height; } // Set the width, then check the height doesn't breach the max height and correct if necessary. int width = this.GetModulusValue(this.GetRes((this.sourceResolution.Width - this.CropLeft - this.CropRight), this.MaxWidth)); int height = this.GetModulusValue(this.GetRes((this.sourceResolution.Height - this.CropTop - this.CropBottom), this.MaxHeight)); // Set the backing fields to avoid triggering recalulation until both are set. this.Task.Width = width; this.Task.Height = height; // Trigger a Recalc this.RecaulcatePictureSettingsProperties(ChangedPictureField.Width); // Update the UI this.NotifyOfPropertyChange(() => this.Width); this.NotifyOfPropertyChange(() => this.Height); break; case PresetPictureSettingsMode.None: // Do Nothing except reset the Max Width/Height this.MaxWidth = this.sourceResolution.Width; this.MaxHeight = this.sourceResolution.Height; this.SelectedAnamorphicMode = preset.Task.Anamorphic; if (this.Width > this.MaxWidth) { // Trigger a Recalc this.Task.Width = this.GetModulusValue(this.GetRes((this.sourceResolution.Width - this.CropLeft - this.CropRight), this.MaxWidth)); this.RecaulcatePictureSettingsProperties(ChangedPictureField.Width); } break; } // Custom Anamorphic if (preset.Task.Anamorphic == Anamorphic.Custom) { this.DisplayWidth = preset.Task.DisplayWidth != null ? int.Parse(preset.Task.DisplayWidth.ToString()) : 0; this.ParWidth = preset.Task.PixelAspectX; this.ParHeight = preset.Task.PixelAspectY; } // Cropping if (preset.Task.HasCropping) { this.IsCustomCrop = true; this.CropLeft = preset.Task.Cropping.Left; this.CropRight = preset.Task.Cropping.Right; this.CropTop = preset.Task.Cropping.Top; this.CropBottom = preset.Task.Cropping.Bottom; } else { this.IsCustomCrop = false; } this.NotifyOfPropertyChange(() => this.Task); this.UpdateVisibileControls(); } /// /// 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.Width); this.NotifyOfPropertyChange(() => this.Height); this.NotifyOfPropertyChange(() => this.SelectedAnamorphicMode); this.NotifyOfPropertyChange(() => this.SelectedModulus); } /// /// 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.currentTitle = title; this.Task = task; this.scannedSource = source; if (title != null) { // Set cached info this.sourceParValues = title.ParVal; this.sourceResolution = title.Resolution; // Update the cropping values, preffering those in the presets. if (!preset.Task.HasCropping) { this.Task.Cropping.Top = title.AutoCropDimensions.Top; this.Task.Cropping.Bottom = title.AutoCropDimensions.Bottom; this.Task.Cropping.Left = title.AutoCropDimensions.Left; this.Task.Cropping.Right = title.AutoCropDimensions.Right; this.IsCustomCrop = false; } else { this.Task.Cropping.Left = preset.Task.Cropping.Left; this.Task.Cropping.Right = preset.Task.Cropping.Right; this.Task.Cropping.Top = preset.Task.Cropping.Top; this.Task.Cropping.Bottom = preset.Task.Cropping.Bottom; this.IsCustomCrop = true; } // Set the Max Width / Height available to the user controls. // Preset Max is null for None / SourceMax this.MaxWidth = preset.Task.MaxWidth ?? this.sourceResolution.Width; if (this.sourceResolution.Width < this.MaxWidth) { this.MaxWidth = this.sourceResolution.Width; } this.MaxHeight = preset.Task.MaxHeight ?? this.sourceResolution.Height; if (this.sourceResolution.Height < this.MaxHeight) { this.MaxHeight = this.sourceResolution.Height; } // Set the W/H if (preset.PictureSettingsMode == PresetPictureSettingsMode.None) { this.Task.Width = this.GetModulusValue(this.sourceResolution.Width - this.CropLeft - this.CropRight); this.Task.Height = this.GetModulusValue(this.sourceResolution.Height - this.CropTop - this.CropBottom); } else if (preset.PictureSettingsMode == PresetPictureSettingsMode.SourceMaximum) { this.Task.Width = this.GetModulusValue(this.sourceResolution.Width - this.CropLeft - this.CropRight); this.Task.Height = this.GetModulusValue(this.sourceResolution.Height - this.CropTop - this.CropBottom); this.MaintainAspectRatio = preset.Task.KeepDisplayAspect; } else { // Custom // Set the Width, and Maintain Aspect ratio. That should calc the Height for us. this.Task.Width = this.GetModulusValue(this.MaxWidth - this.CropLeft - this.CropRight); if (this.SelectedAnamorphicMode != Anamorphic.Loose) { this.Task.Height = this.GetModulusValue(this.MaxHeight - this.CropTop - this.CropBottom); } // If our height is too large, let it downscale the width for us by setting the height to the lower value. if (!this.MaintainAspectRatio && this.Height > this.MaxHeight) { this.Task.Height = this.MaxHeight; } } // Set Screen Controls this.SourceInfo = string.Format( "{0}x{1}, PAR: {2}/{3}", title.Resolution.Width, title.Resolution.Height, title.ParVal.Width, title.ParVal.Height); this.RecaulcatePictureSettingsProperties(ChangedPictureField.Width); } this.NotifyOfPropertyChange(() => this.Task); } #endregion #region Methods /// /// The init. /// private void Init() { this.Task.Modulus = 16; this.Task.KeepDisplayAspect = true; this.NotifyOfPropertyChange(() => this.SelectedModulus); this.NotifyOfPropertyChange(() => this.MaintainAspectRatio); // Default the Max Width / Height to 1080p format this.MaxHeight = 1080; this.MaxWidth = 1920; } /// /// The get picture title info. /// /// /// The . /// private PictureSize.PictureSettingsTitle GetPictureTitleInfo() { PictureSize.PictureSettingsTitle title = new PictureSize.PictureSettingsTitle { Width = this.sourceResolution.Width, Height = this.sourceResolution.Height, ParW = this.sourceParValues.Width, ParH = this.sourceParValues.Height, Aspect = 0 // TODO }; return title; } /// /// The get picture settings. /// /// /// The . /// private PictureSize.PictureSettingsJob GetPictureSettings() { PictureSize.PictureSettingsJob job = new PictureSize.PictureSettingsJob { Width = this.Width, Height = this.Height, ItuPar = false, Modulus = this.SelectedModulus, ParW = this.ParWidth, ParH = this.ParHeight, MaxWidth = this.MaxWidth, MaxHeight = this.MaxHeight, KeepDisplayAspect = this.MaintainAspectRatio, AnamorphicMode = this.SelectedAnamorphicMode, DarWidth = 0, DarHeight = 0, Crop = new Cropping(this.CropTop, this.CropBottom, this.CropLeft, this.CropRight), }; if (this.SelectedAnamorphicMode == Anamorphic.Loose) { job.ParW = sourceParValues.Width; job.ParH = sourceParValues.Height; } return job; } /// /// Recalculate the picture settings when the user changes a particular field defined in the ChangedPictureField enum. /// The properties in this class are dumb. They simply call this method if there is a change. /// It is the job of this method to update all affected private fields and raise change notifications. /// /// /// The changed field. /// private void RecaulcatePictureSettingsProperties(ChangedPictureField changedField) { // Sanity Check if (this.currentTitle == null) { return; } // Step 1, Update what controls are visibile. this.UpdateVisibileControls(); // Step 2, Set sensible defaults if (changedField == ChangedPictureField.Anamorphic && (this.SelectedAnamorphicMode == Anamorphic.None || this.SelectedAnamorphicMode == Anamorphic.Loose)) { this.Task.Width = this.sourceResolution.Width > this.MaxWidth ? this.MaxWidth : this.sourceResolution.Width; this.Task.KeepDisplayAspect = true; } // Choose which setting to keep. PictureSize.KeepSetting setting = PictureSize.KeepSetting.HB_KEEP_WIDTH; switch (changedField) { case ChangedPictureField.Width: setting = PictureSize.KeepSetting.HB_KEEP_WIDTH; break; case ChangedPictureField.Height: setting = PictureSize.KeepSetting.HB_KEEP_HEIGHT; break; } // Step 2, For the changed field, call hb_set_anamorphic_size and process the results. PictureSize.AnamorphicResult result = PictureSize.hb_set_anamorphic_size2(this.GetPictureSettings(), this.GetPictureTitleInfo(), setting); this.Task.Width = result.OutputWidth; this.Task.Height = result.OutputHeight; this.Task.PixelAspectX = (int)Math.Round(result.OutputParWidth, 0); this.Task.PixelAspectY = (int)Math.Round(result.OutputParHeight, 0); // Step 3, Set the display width label to indicate the output. double dispWidth = Math.Round((result.OutputWidth * result.OutputParWidth / result.OutputParHeight), 0); this.DisplaySize = this.sourceResolution == null || this.sourceResolution.IsEmpty ? string.Empty : string.Format(Resources.PictureSettingsViewModel_StorageDisplayLabel, dispWidth, result.OutputHeight, this.ParWidth, this.ParHeight); // Step 4, Force an update on all the UI elements. this.NotifyOfPropertyChange(() => this.Width); this.NotifyOfPropertyChange(() => this.Height); this.NotifyOfPropertyChange(() => this.ParWidth); this.NotifyOfPropertyChange(() => this.ParHeight); this.NotifyOfPropertyChange(() => this.CropTop); this.NotifyOfPropertyChange(() => this.CropBottom); this.NotifyOfPropertyChange(() => this.CropLeft); this.NotifyOfPropertyChange(() => this.CropRight); this.NotifyOfPropertyChange(() => this.SelectedModulus); this.NotifyOfPropertyChange(() => this.MaintainAspectRatio); // Step 5, Update the Preview if (delayedPreviewprocessor != null && this.Task != null && this.StaticPreviewViewModel != null && this.StaticPreviewViewModel.IsOpen) { delayedPreviewprocessor.PerformTask(() => this.StaticPreviewViewModel.UpdatePreviewFrame(this.Task, this.scannedSource), 800); } } /// /// The update visibile controls. /// private void UpdateVisibileControls() { this.ShowDisplaySize = true; this.ShowKeepAR = true; switch (this.SelectedAnamorphicMode) { case Anamorphic.None: this.WidthControlEnabled = true; this.HeightControlEnabled = true; this.ShowCustomAnamorphicControls = false; this.ShowModulus = true; this.ShowDisplaySize = true; this.ShowKeepAR = true; break; case Anamorphic.Automatic: this.WidthControlEnabled = true; this.HeightControlEnabled = true; this.ShowCustomAnamorphicControls = false; this.ShowModulus = true; this.ShowKeepAR = false; break; case Anamorphic.Loose: this.WidthControlEnabled = true; this.HeightControlEnabled = false; this.ShowCustomAnamorphicControls = false; this.ShowModulus = true; this.ShowKeepAR = false; break; case Anamorphic.Custom: this.WidthControlEnabled = true; this.HeightControlEnabled = true; this.ShowCustomAnamorphicControls = true; this.ShowModulus = true; this.ShowDisplaySize = false; this.ShowKeepAR = false; break; } } /// /// For a given value, correct so that it matches the users currently selected modulus value /// /// /// The value. /// /// /// Value corrected so that value % selected modulus == 0 /// private int GetModulusValue(double value) { if (this.SelectedModulus == null) { return 0; } double remainder = value % this.SelectedModulus.Value; if (remainder.Equals(0.0d)) { return (int)Math.Abs(value); } double result = remainder >= ((double)this.SelectedModulus.Value / 2) ? value + (this.SelectedModulus.Value - remainder) : value - remainder; return (int)Math.Abs(result); } /// /// The get res. /// /// /// The value. /// /// /// The max. /// /// /// The . /// private int GetRes(int value, int? max) { return max.HasValue ? (value > max.Value ? max.Value : value) : value; } #endregion } /// /// The changed picture field. /// public enum ChangedPictureField { Width, Height, ParW, ParH, DisplayWidth, Crop, Anamorphic, MaintainAspectRatio, Modulus } }