[Settings]ImageResizer settings accessibility updates, fixes and refactor (#36903)

* Fix issue with missing Image Resizer unit and fit information in settings description.

* Fix accessibility issues on Edit and Remove buttons. Fix various issues and refactor view model and ImageSize. New resources for accessibility text formats.

* Fix unit test because of change to new preset width and height. Fix 2 unit tests having incorrect expected/actual orderings.

* Post-review update: accessibility strings now formatted within the converter, instead of via format strings; simplified encoder GUID collection declaration and retrieval.

* Minor example text fix.
This commit is contained in:
Dave Rayment 2025-01-21 11:55:02 +00:00 коммит произвёл GitHub
Родитель b33e0be178
Коммит 438d17302e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
9 изменённых файлов: 615 добавлений и 715 удалений

Просмотреть файл

@ -3,36 +3,33 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using Settings.UI.Library.Resources;
namespace Microsoft.PowerToys.Settings.UI.Library
namespace Microsoft.PowerToys.Settings.UI.Library;
public partial class ImageSize : INotifyPropertyChanged
{
public class ImageSize : INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
public ImageSize(int id)
bool changed = !EqualityComparer<T>.Default.Equals(field, value);
if (changed)
{
Id = id;
Name = string.Empty;
Fit = ResizeFit.Fit;
Width = 0;
Height = 0;
Unit = ResizeUnit.Pixel;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AccessibleTextHelper)));
}
public ImageSize()
{
Id = 0;
Name = string.Empty;
Fit = ResizeFit.Fit;
Width = 0;
Height = 0;
Unit = ResizeUnit.Pixel;
return changed;
}
public ImageSize(int id, string name, ResizeFit fit, double width, double height, ResizeUnit unit)
public ImageSize(int id = 0, string name = "", ResizeFit fit = ResizeFit.Fit, double width = 0, double height = 0, ResizeUnit unit = ResizeUnit.Pixel)
{
Id = id;
Name = name;
@ -51,85 +48,38 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public int Id
{
get
{
return _id;
get => _id;
set => SetProperty(ref _id, value);
}
set
/// <summary>
/// Gets a value indicating whether the <see cref="Height"/> property is used. When false, the
/// <see cref="Width"/> property is used to evenly scale the image in both X and Y dimensions.
/// </summary>
[JsonIgnore]
public bool IsHeightUsed
{
if (_id != value)
{
_id = value;
OnPropertyChanged();
}
}
}
public int ExtraBoxOpacity
{
get
{
if (Unit == ResizeUnit.Percent && Fit != ResizeFit.Stretch)
{
return 0;
}
else
{
return 100;
}
}
}
public bool EnableEtraBoxes
{
get
{
if (Unit == ResizeUnit.Percent && Fit != ResizeFit.Stretch)
{
return false;
}
else
{
return true;
}
}
// Height is ignored when using percentage scaling where the aspect ratio is maintained
// (i.e. non-stretch fits). In all other cases, both Width and Height are needed.
get => !(Unit == ResizeUnit.Percent && Fit != ResizeFit.Stretch);
}
[JsonPropertyName("name")]
public string Name
{
get
{
return _name;
}
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged();
}
}
get => _name;
set => SetProperty(ref _name, value);
}
[JsonPropertyName("fit")]
public ResizeFit Fit
{
get
{
return _fit;
}
get => _fit;
set
{
if (_fit != value)
if (SetProperty(ref _fit, value))
{
_fit = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ExtraBoxOpacity));
OnPropertyChanged(nameof(EnableEtraBoxes));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsHeightUsed)));
}
}
}
@ -137,107 +87,35 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("width")]
public double Width
{
get
{
return _width;
}
set
{
double newWidth = -1;
if (value < 0 || double.IsNaN(value))
{
newWidth = 0;
}
else
{
newWidth = value;
}
if (_width != newWidth)
{
_width = newWidth;
OnPropertyChanged();
}
}
get => _width;
set => SetProperty(ref _width, value < 0 || double.IsNaN(value) ? 0 : value);
}
[JsonPropertyName("height")]
public double Height
{
get
{
return _height;
}
set
{
double newHeight = -1;
if (value < 0 || double.IsNaN(value))
{
newHeight = 0;
}
else
{
newHeight = value;
}
if (_height != newHeight)
{
_height = newHeight;
OnPropertyChanged();
}
}
get => _height;
set => SetProperty(ref _height, value < 0 || double.IsNaN(value) ? 0 : value);
}
[JsonPropertyName("unit")]
public ResizeUnit Unit
{
get
{
return _unit;
}
get => _unit;
set
{
if (_unit != value)
if (SetProperty(ref _unit, value))
{
_unit = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ExtraBoxOpacity));
OnPropertyChanged(nameof(EnableEtraBoxes));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsHeightUsed)));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Gets access to all properties for formatting accessibility descriptions.
/// </summary>
[JsonIgnore]
public ImageSize AccessibleTextHelper => this;
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public void Update(ImageSize modifiedSize)
{
ArgumentNullException.ThrowIfNull(modifiedSize);
Id = modifiedSize.Id;
Name = modifiedSize.Name;
Fit = modifiedSize.Fit;
Width = modifiedSize.Width;
Height = modifiedSize.Height;
Unit = modifiedSize.Unit;
}
public string ToJsonString()
{
return JsonSerializer.Serialize(this);
}
}
public string ToJsonString() => JsonSerializer.Serialize(this);
}

Просмотреть файл

@ -205,7 +205,7 @@ namespace ViewModelTests
}
[TestMethod]
public void AddRowShouldAddNewImageSizeWhenSuccessful()
public void AddImageSizeShouldAddNewImageSizeWhenSuccessful()
{
// arrange
var mockSettingsUtils = ISettingsUtilsMocks.GetStubSettingsUtils<ImageResizerSettings>();
@ -214,7 +214,7 @@ namespace ViewModelTests
int sizeOfOriginalArray = viewModel.Sizes.Count;
// act
viewModel.AddRow("New size");
viewModel.AddImageSize();
// Assert
Assert.AreEqual(sizeOfOriginalArray + 1, viewModel.Sizes.Count);
@ -229,15 +229,15 @@ namespace ViewModelTests
ImageResizerViewModel viewModel = new ImageResizerViewModel(mockSettingsUtils.Object, SettingsRepository<GeneralSettings>.GetInstance(_mockGeneralSettingsUtils.Object), sendMockIPCConfigMSG, (string name) => name);
// act
viewModel.AddRow("New size");
viewModel.AddImageSize("New size");
// Assert
ImageSize newTestSize = viewModel.Sizes.First(x => x.Id == 0);
Assert.AreEqual(newTestSize.Name, "New size 1");
Assert.AreEqual(newTestSize.Fit, ResizeFit.Fit);
Assert.AreEqual(newTestSize.Width, 854);
Assert.AreEqual(newTestSize.Height, 480);
Assert.AreEqual(newTestSize.Unit, ResizeUnit.Pixel);
Assert.AreEqual("New size 1", newTestSize.Name);
Assert.AreEqual(ResizeFit.Fit, newTestSize.Fit);
Assert.AreEqual(1024, newTestSize.Width);
Assert.AreEqual(640, newTestSize.Height);
Assert.AreEqual(ResizeUnit.Pixel, newTestSize.Unit);
}
[TestMethod]
@ -247,7 +247,7 @@ namespace ViewModelTests
var mockSettingsUtils = ISettingsUtilsMocks.GetStubSettingsUtils<ImageResizerSettings>();
Func<string, int> sendMockIPCConfigMSG = msg => { return 0; };
ImageResizerViewModel viewModel = new ImageResizerViewModel(mockSettingsUtils.Object, SettingsRepository<GeneralSettings>.GetInstance(_mockGeneralSettingsUtils.Object), sendMockIPCConfigMSG, (string name) => name);
viewModel.AddRow("New Size");
viewModel.AddImageSize("New Size");
int sizeOfOriginalArray = viewModel.Sizes.Count;
ImageSize deleteCandidate = viewModel.Sizes.First(x => x.Id == 0);
@ -268,16 +268,16 @@ namespace ViewModelTests
ImageResizerViewModel viewModel = new ImageResizerViewModel(mockSettingsUtils.Object, SettingsRepository<GeneralSettings>.GetInstance(_mockGeneralSettingsUtils.Object), sendMockIPCConfigMSG, (string name) => name);
// act
viewModel.AddRow("New size"); // Add: "New size 1"
viewModel.AddRow("New size"); // Add: "New size 2"
viewModel.AddRow("New size"); // Add: "New size 3"
viewModel.AddImageSize("New size"); // Add: "New size 1"
viewModel.AddImageSize("New size"); // Add: "New size 2"
viewModel.AddImageSize("New size"); // Add: "New size 3"
viewModel.DeleteImageSize(1); // Delete: "New size 2"
viewModel.AddRow("New size"); // Add: "New Size 4"
viewModel.AddImageSize("New size"); // Add: "New Size 4"
// Assert
Assert.AreEqual(viewModel.Sizes[0].Name, "New size 1");
Assert.AreEqual(viewModel.Sizes[1].Name, "New size 3");
Assert.AreEqual(viewModel.Sizes[2].Name, "New size 4");
Assert.AreEqual("New size 1", viewModel.Sizes[0].Name);
Assert.AreEqual("New size 3", viewModel.Sizes[1].Name);
Assert.AreEqual("New size 4", viewModel.Sizes[2].Name);
}
[TestMethod]

Просмотреть файл

@ -3,41 +3,38 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml.Data;
namespace Microsoft.PowerToys.Settings.UI.Converters
namespace Microsoft.PowerToys.Settings.UI.Converters;
public sealed partial class ImageResizerFitToStringConverter : IValueConverter
{
public sealed partial class ImageResizerFitToStringConverter : IValueConverter
// Maps each ResizeFit to its localized string.
private static readonly Dictionary<ResizeFit, string> FitToText = new()
{
{ ResizeFit.Fill, Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_Fit_Fill_ThirdPersonSingular") },
{ ResizeFit.Fit, Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_Fit_Fit_ThirdPersonSingular") },
{ ResizeFit.Stretch, Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_Fit_Stretch_ThirdPersonSingular") },
};
public object Convert(object value, Type targetType, object parameter, string language)
{
var toLower = false;
if ((string)parameter == "ToLower")
if (value is ResizeFit fit && FitToText.TryGetValue(fit, out string fitText))
{
toLower = true;
return parameter is string lowerParam && lowerParam == "ToLower" ?
fitText.ToLower(CultureInfo.CurrentCulture) :
fitText;
}
string targetValue = string.Empty;
switch (value)
{
case 0: targetValue = Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_Fit_Fill_ThirdPersonSingular"); break;
case 1: targetValue = Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_Fit_Fit_ThirdPersonSingular"); break;
case 2: targetValue = Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_Fit_Stretch_ThirdPersonSingular"); break;
}
if (toLower)
{
targetValue = targetValue.ToLower(CultureInfo.CurrentCulture);
}
return targetValue;
return DependencyProperty.UnsetValue;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
return value;
}
}
}

Просмотреть файл

@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml.Data;
namespace Microsoft.PowerToys.Settings.UI.Converters;
/// <summary>
/// Creates accessibility text for controls related to <see cref="ImageSize"/> properties.
/// </summary>
/// <example>(Name) "Edit the Small preset"</example>
/// <example>(FullDescription) "Large - Fits within 1920 × 1080 pixels"</example>"
public sealed partial class ImageResizerSizeToAccessibleTextConverter : IValueConverter
{
private const char TimesGlyph = '\u00D7'; // Unicode "MULTIPLICATION SIGN"
/// <summary>
/// Maps the supplied accessibility identifier to the format string of the localized accessible text.
/// </summary>
private static readonly Dictionary<string, string> AccessibilityFormats = new()
{
{ "Edit", Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_EditButton_Accessibility_Name") },
{ "Remove", Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_RemoveButton_Accessibility_Name") },
};
private readonly ImageResizerFitToStringConverter _fitConverter = new();
private readonly ImageResizerUnitToStringConverter _unitConverter = new();
public object Convert(object value, Type targetType, object parameter, string language)
{
return (value, parameter) switch
{
(string presetName, string nameId) => FormatNameText(presetName, nameId),
(ImageSize preset, string _) => FormatDescriptionText(preset),
_ => DependencyProperty.UnsetValue,
};
}
private object FormatNameText(string presetName, string nameId)
{
return AccessibilityFormats.TryGetValue(nameId, out string format) ?
string.Format(CultureInfo.CurrentCulture, format, presetName) :
DependencyProperty.UnsetValue;
}
private object FormatDescriptionText(ImageSize preset)
{
if (preset == null)
{
return DependencyProperty.UnsetValue;
}
string fitText = _fitConverter.Convert(preset.Fit, typeof(string), null, null) as string;
string unitText = _unitConverter.Convert(preset.Unit, typeof(string), null, null) as string;
return preset.IsHeightUsed ?
$"{preset.Name} - {fitText} {preset.Width} {TimesGlyph} {preset.Height} {unitText}" :
$"{preset.Name} - {fitText} {preset.Width} {unitText}";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

Просмотреть файл

@ -3,42 +3,39 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml.Data;
namespace Microsoft.PowerToys.Settings.UI.Converters
namespace Microsoft.PowerToys.Settings.UI.Converters;
public sealed partial class ImageResizerUnitToStringConverter : IValueConverter
{
public sealed partial class ImageResizerUnitToStringConverter : IValueConverter
// Maps each ResizeUnit value to its localized string.
private static readonly Dictionary<ResizeUnit, string> UnitToText = new()
{
{ ResizeUnit.Centimeter, Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_Unit_Centimeter") },
{ ResizeUnit.Inch, Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_Unit_Inch") },
{ ResizeUnit.Percent, Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_Unit_Percent") },
{ ResizeUnit.Pixel, Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_Unit_Pixel") },
};
public object Convert(object value, Type targetType, object parameter, string language)
{
var toLower = false;
if ((string)parameter == "ToLower")
if (value is ResizeUnit unit && UnitToText.TryGetValue(unit, out string unitText))
{
toLower = true;
return parameter is string lowerParam && lowerParam == "ToLower" ?
unitText.ToLower(CultureInfo.CurrentCulture) :
unitText;
}
string targetValue = string.Empty;
switch (value)
{
case 0: targetValue = Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_Unit_Centimeter"); break;
case 1: targetValue = Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_Unit_Inch"); break;
case 2: targetValue = Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_Unit_Percent"); break;
case 3: targetValue = Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_Unit_Pixel"); break;
}
if (toLower)
{
targetValue = targetValue.ToLower(CultureInfo.CurrentCulture);
}
return targetValue;
return DependencyProperty.UnsetValue;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
return value;
}
}
}

Просмотреть файл

@ -18,6 +18,7 @@
<converters:ImageResizerFitToIntConverter x:Key="ImageResizerFitToIntConverter" />
<converters:ImageResizerUnitToStringConverter x:Key="ImageResizerUnitToStringConverter" />
<converters:ImageResizerUnitToIntConverter x:Key="ImageResizerUnitToIntConverter" />
<converters:ImageResizerSizeToAccessibleTextConverter x:Key="ImageResizerSizeToAccessibleTextConverter" />
<toolkitconverters:BoolToObjectConverter
x:Key="BoolToComboBoxIndexConverter"
FalseValue="1"
@ -85,13 +86,13 @@
FontSize="10"
Style="{ThemeResource SecondaryTextStyle}"
Text="&#xE947;"
Visibility="{x:Bind EnableEtraBoxes, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
Visibility="{x:Bind IsHeightUsed, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<TextBlock
Margin="0,0,4,0"
FontWeight="SemiBold"
Style="{ThemeResource SecondaryTextStyle}"
Text="{x:Bind Height, Mode=OneWay}"
Visibility="{x:Bind EnableEtraBoxes, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
Visibility="{x:Bind IsHeightUsed, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<TextBlock
Margin="0,0,4,0"
Style="{ThemeResource SecondaryTextStyle}"
@ -104,12 +105,20 @@
Orientation="Horizontal"
Spacing="8">
<Button
x:Uid="EditButton"
x:Uid="ImageResizer_EditButton"
Width="40"
Height="36"
Content="&#xE70F;"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Style="{StaticResource SubtleButtonStyle}">
Style="{StaticResource SubtleButtonStyle}"
AutomationProperties.Name="{x:Bind Name,
Mode=OneWay,
Converter={StaticResource ImageResizerSizeToAccessibleTextConverter},
ConverterParameter='Edit'}"
AutomationProperties.FullDescription="{x:Bind AccessibleTextHelper,
Mode=OneWay,
Converter={StaticResource ImageResizerSizeToAccessibleTextConverter},
ConverterParameter='Edit'}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="EditTooltip" />
</ToolTipService.ToolTip>
@ -145,10 +154,10 @@
Width="116"
Minimum="0"
SpinButtonPlacementMode="Compact"
Visibility="{x:Bind EnableEtraBoxes, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"
Visibility="{x:Bind IsHeightUsed, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"
Value="{x:Bind Height, Mode=TwoWay}" />
</StackPanel>
<ComboBox
x:Uid="ImageResizer_Size"
Width="240"
@ -164,15 +173,22 @@
</Button>
<Button
x:Name="RemoveButton"
x:Uid="RemoveButton"
x:Uid="ImageResizer_RemoveButton"
Width="40"
Height="36"
Click="DeleteCustomSize"
CommandParameter="{Binding Id}"
Content="&#xE74D;"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Style="{StaticResource SubtleButtonStyle}">
Style="{StaticResource SubtleButtonStyle}"
AutomationProperties.Name="{x:Bind Name,
Mode=OneWay,
Converter={StaticResource ImageResizerSizeToAccessibleTextConverter},
ConverterParameter='Remove'}"
AutomationProperties.FullDescription="{x:Bind AccessibleTextHelper,
Mode=OneWay,
Converter={StaticResource ImageResizerSizeToAccessibleTextConverter},
ConverterParameter='Remove'}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="RemoveTooltip" />
</ToolTipService.ToolTip>

Просмотреть файл

@ -22,11 +22,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
{
InitializeComponent();
var settingsUtils = new SettingsUtils();
var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
Func<string, string> loader = (string name) =>
{
return resourceLoader.GetString(name);
};
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
Func<string, string> loader = resourceLoader.GetString;
ViewModel = new ImageResizerViewModel(settingsUtils, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage, loader);
DataContext = ViewModel;
@ -69,7 +66,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
{
try
{
ViewModel.AddRow(Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_DefaultSize_NewSizePrefix"));
ViewModel.AddImageSize();
}
catch (Exception ex)
{

Просмотреть файл

@ -1169,10 +1169,6 @@
<data name="ImageResizer_Size.Header" xml:space="preserve">
<value>Unit</value>
</data>
<data name="RemoveButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Remove</value>
<comment>Removes a user defined setting group for Image Resizer</comment>
</data>
<data name="RemoveItem.Text" xml:space="preserve">
<value>Delete</value>
</data>
@ -2352,12 +2348,29 @@ From there, simply click on one of the supported files in the File Explorer and
<data name="ImageResizer_Unit_Pixel" xml:space="preserve">
<value>Pixels</value>
</data>
<data name="EditButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<data name="ImageResizer_EditButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Edit</value>
</data>
<data name="ImageResizer_EditSize.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Edit size</value>
</data>
<data name="ImageResizer_Presets.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>ImageResizer presets</value>
</data>
<data name="ImageResizer_AddSizeButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Add a new preset</value>
</data>
<data name="ImageResizer_RemoveButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Remove</value>
</data>
<data name="ImageResizer_EditButton_Accessibility_Name" xml:space="preserve">
<value>Edit the {0} preset</value>
<comment>Expands to the AutomationProperties.Name value for the Edit button. Example: "Edit the Small preset".</comment>
</data>
<data name="ImageResizer_RemoveButton_Accessibility_Name" xml:space="preserve">
<value>Remove the {0} preset</value>
<comment>Expands to the AutomationProperties.Name value for the Remove button. Example: "Remove the Large preset".</comment>
</data>
<data name="No" xml:space="preserve">
<value>No</value>
<comment>Label of a cancel button</comment>

Просмотреть файл

@ -3,21 +3,46 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using global::PowerToys.GPOWrapper;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
namespace Microsoft.PowerToys.Settings.UI.ViewModels;
public partial class ImageResizerViewModel : Observable
{
public class ImageResizerViewModel : Observable
{
private static readonly string DefaultPresetNamePrefix =
Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_DefaultSize_NewSizePrefix");
private static readonly List<string> EncoderGuids =
[
"1b7cfaf4-713f-473c-bbcd-6137425faeaf", // PNG Encoder
"0af1d87e-fcfe-4188-bdeb-a7906471cbe3", // Bitmap Encoder
"19e4a5aa-5662-4fc5-a0c0-1758028e1057", // JPEG Encoder
"163bcc30-e2e9-4f0b-961d-a3e9fdb788a3", // TIFF Encoder
"57a37caa-367a-4540-916b-f183c5093a4b", // TIFF Encoder
"1f8a5601-7d4d-4cbd-9c82-1bc8d4eeb9a5", // GIF Encoder
];
/// <summary>
/// Used to skip saving settings to file during initialization.
/// </summary>
private readonly bool _isInitializing;
/// <summary>
/// Holds defaults for new presets.
/// </summary>
private readonly ImageSize _customSize;
private GeneralSettings GeneralSettingsConfig { get; set; }
private readonly ISettingsUtils _settingsUtils;
@ -30,6 +55,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public ImageResizerViewModel(ISettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, Func<string, int> ipcMSGCallBackFunc, Func<string, string> resourceLoader)
{
_isInitializing = true;
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
// To obtain the general settings configurations of PowerToys.
@ -59,21 +86,17 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
InitializeEnabledValue();
_advancedSizes = Settings.Properties.ImageresizerSizes.Value;
_jpegQualityLevel = Settings.Properties.ImageresizerJpegQualityLevel.Value;
_pngInterlaceOption = Settings.Properties.ImageresizerPngInterlaceOption.Value;
_tiffCompressOption = Settings.Properties.ImageresizerTiffCompressOption.Value;
_fileName = Settings.Properties.ImageresizerFileName.Value;
_keepDateModified = Settings.Properties.ImageresizerKeepDateModified.Value;
_encoderGuidId = GetEncoderIndex(Settings.Properties.ImageresizerFallbackEncoder.Value);
Sizes = new ObservableCollection<ImageSize>(Settings.Properties.ImageresizerSizes.Value);
JPEGQualityLevel = Settings.Properties.ImageresizerJpegQualityLevel.Value;
PngInterlaceOption = Settings.Properties.ImageresizerPngInterlaceOption.Value;
TiffCompressOption = Settings.Properties.ImageresizerTiffCompressOption.Value;
FileName = Settings.Properties.ImageresizerFileName.Value;
KeepDateModified = Settings.Properties.ImageresizerKeepDateModified.Value;
Encoder = GetEncoderIndex(Settings.Properties.ImageresizerFallbackEncoder.Value);
int i = 0;
foreach (ImageSize size in _advancedSizes)
{
size.Id = i;
i++;
size.PropertyChanged += SizePropertyChanged;
}
_customSize = Settings.Properties.ImageresizerCustomSize.Value;
_isInitializing = false;
}
private void InitializeEnabledValue()
@ -94,7 +117,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private GpoRuleConfigured _enabledGpoRuleConfiguration;
private bool _enabledStateIsGPOConfigured;
private bool _isEnabled;
private ObservableCollection<ImageSize> _advancedSizes = new ObservableCollection<ImageSize>();
private ObservableCollection<ImageSize> _sizes = [];
private int _jpegQualityLevel;
private int _pngInterlaceOption;
private int _tiffCompressOption;
@ -106,10 +129,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public bool IsEnabled
{
get
{
return _isEnabled;
}
get => _isEnabled;
set
{
@ -139,155 +159,163 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public ObservableCollection<ImageSize> Sizes
{
get
{
return _advancedSizes;
}
get => _sizes;
set
{
SavesImageSizes(value);
_advancedSizes = value;
if (_sizes != null)
{
_sizes.CollectionChanged -= Sizes_CollectionChanged;
UnsubscribeFromItemPropertyChanged(_sizes);
}
_sizes = value;
if (_sizes != null)
{
_sizes.CollectionChanged += Sizes_CollectionChanged;
SubscribeToItemPropertyChanged(_sizes);
}
OnPropertyChanged(nameof(Sizes));
if (!_isInitializing)
{
SaveImageSizes();
}
}
}
private void Sizes_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
SubscribeToItemPropertyChanged(e.NewItems?.Cast<ImageSize>());
UnsubscribeFromItemPropertyChanged(e.OldItems?.Cast<ImageSize>());
SaveImageSizes();
}
private void SubscribeToItemPropertyChanged(IEnumerable<ImageSize> items)
{
if (items != null)
{
foreach (var item in items)
{
item.PropertyChanged += SizePropertyChanged;
}
}
}
private void UnsubscribeFromItemPropertyChanged(IEnumerable<ImageSize> items)
{
if (items != null)
{
foreach (var item in items)
{
item.PropertyChanged -= SizePropertyChanged;
}
}
}
private void SetProperty<T>(ref T backingField, T value, Action<T> updateSettingsAction, [CallerMemberName] string propertyName = null)
{
if (!EqualityComparer<T>.Default.Equals(backingField, value))
{
backingField = value;
if (!_isInitializing)
{
updateSettingsAction(value);
_settingsUtils.SaveSettings(Settings.ToJsonString(), ModuleName);
}
OnPropertyChanged(propertyName);
}
}
public int JPEGQualityLevel
{
get
{
return _jpegQualityLevel;
}
get => _jpegQualityLevel;
set
{
if (_jpegQualityLevel != value)
{
_jpegQualityLevel = value;
Settings.Properties.ImageresizerJpegQualityLevel.Value = value;
_settingsUtils.SaveSettings(Settings.ToJsonString(), ModuleName);
OnPropertyChanged(nameof(JPEGQualityLevel));
}
SetProperty(ref _jpegQualityLevel, value, v => Settings.Properties.ImageresizerJpegQualityLevel.Value = v);
}
}
public int PngInterlaceOption
{
get
{
return _pngInterlaceOption;
}
get => _pngInterlaceOption;
set
{
if (_pngInterlaceOption != value)
{
_pngInterlaceOption = value;
Settings.Properties.ImageresizerPngInterlaceOption.Value = value;
_settingsUtils.SaveSettings(Settings.ToJsonString(), ModuleName);
OnPropertyChanged(nameof(PngInterlaceOption));
}
SetProperty(ref _pngInterlaceOption, value, v => Settings.Properties.ImageresizerPngInterlaceOption.Value = v);
}
}
public int TiffCompressOption
{
get
{
return _tiffCompressOption;
}
get => _tiffCompressOption;
set
{
if (_tiffCompressOption != value)
{
_tiffCompressOption = value;
Settings.Properties.ImageresizerTiffCompressOption.Value = value;
_settingsUtils.SaveSettings(Settings.ToJsonString(), ModuleName);
OnPropertyChanged(nameof(TiffCompressOption));
}
SetProperty(ref _tiffCompressOption, value, v => Settings.Properties.ImageresizerTiffCompressOption.Value = v);
}
}
public string FileName
{
get
{
return _fileName;
}
get => _fileName;
set
{
if (!string.IsNullOrWhiteSpace(value))
{
_fileName = value;
Settings.Properties.ImageresizerFileName.Value = value;
_settingsUtils.SaveSettings(Settings.ToJsonString(), ModuleName);
OnPropertyChanged(nameof(FileName));
SetProperty(ref _fileName, value, v => Settings.Properties.ImageresizerFileName.Value = v);
}
}
}
public bool KeepDateModified
{
get
{
return _keepDateModified;
}
get => _keepDateModified;
set
{
_keepDateModified = value;
Settings.Properties.ImageresizerKeepDateModified.Value = value;
_settingsUtils.SaveSettings(Settings.ToJsonString(), ModuleName);
OnPropertyChanged(nameof(KeepDateModified));
SetProperty(ref _keepDateModified, value, v => Settings.Properties.ImageresizerKeepDateModified.Value = v);
}
}
public int Encoder
{
get
{
return _encoderGuidId;
}
get => _encoderGuidId;
set
{
if (_encoderGuidId != value)
{
_encoderGuidId = value;
_settingsUtils.SaveSettings(Settings.Properties.ImageresizerSizes.ToJsonString(), ModuleName, "sizes.json");
Settings.Properties.ImageresizerFallbackEncoder.Value = GetEncoderGuid(value);
_settingsUtils.SaveSettings(Settings.ToJsonString(), ModuleName);
OnPropertyChanged(nameof(Encoder));
}
}
}
public string EncoderGuid
{
get
{
return ImageResizerViewModel.GetEncoderGuid(_encoderGuidId);
SetProperty(ref _encoderGuidId, value, v => Settings.Properties.ImageresizerFallbackEncoder.Value = GetEncoderGuid(v));
}
}
public void AddRow(string sizeNamePrefix)
public string EncoderGuid => GetEncoderGuid(_encoderGuidId);
public static string GetEncoderGuid(int index) =>
index < 0 || index >= EncoderGuids.Count ? throw new ArgumentOutOfRangeException(nameof(index)) : EncoderGuids[index];
public static int GetEncoderIndex(string encoderGuid)
{
/// This is a fallback validation to eliminate the warning "CA1062:Validate arguments of public methods" when using the parameter (variable) "sizeNamePrefix" in the code.
/// If the parameter is unexpectedly empty or null, we fill the parameter with a non-localized string.
/// Normally the parameter "sizeNamePrefix" can't be null or empty because it is filled with a localized string when we call this method from <see cref="UI.Views.ImageResizerPage.AddSizeButton_Click"/>.
sizeNamePrefix = string.IsNullOrEmpty(sizeNamePrefix) ? "New Size" : sizeNamePrefix;
int index = EncoderGuids.IndexOf(encoderGuid);
return index == -1 ? throw new ArgumentException("Encoder GUID not found.", nameof(encoderGuid)) : index;
}
ObservableCollection<ImageSize> imageSizes = Sizes;
int maxId = imageSizes.Count > 0 ? imageSizes.OrderBy(x => x.Id).Last().Id : -1;
string sizeName = GenerateNameForNewSize(imageSizes, sizeNamePrefix);
public void AddImageSize(string namePrefix = "")
{
if (string.IsNullOrEmpty(namePrefix))
{
namePrefix = DefaultPresetNamePrefix;
}
ImageSize newSize = new ImageSize(maxId + 1, sizeName, ResizeFit.Fit, 854, 480, ResizeUnit.Pixel);
newSize.PropertyChanged += SizePropertyChanged;
imageSizes.Add(newSize);
_advancedSizes = imageSizes;
SavesImageSizes(imageSizes);
int maxId = Sizes.Count > 0 ? Sizes.Max(x => x.Id) : -1;
string sizeName = GenerateNameForNewSize(namePrefix);
Sizes.Add(new ImageSize(maxId + 1, GenerateNameForNewSize(namePrefix), _customSize.Fit, _customSize.Width, _customSize.Height, _customSize.Unit));
// Set the focus requested flag to indicate that an add operation has occurred during the ContainerContentChanging event
IsListViewFocusRequested = true;
@ -295,131 +323,35 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public void DeleteImageSize(int id)
{
ImageSize size = _advancedSizes.First(x => x.Id == id);
ObservableCollection<ImageSize> imageSizes = Sizes;
imageSizes.Remove(size);
_advancedSizes = imageSizes;
SavesImageSizes(imageSizes);
ImageSize size = _sizes.First(x => x.Id == id);
Sizes.Remove(size);
}
public void SavesImageSizes(ObservableCollection<ImageSize> imageSizes)
public void SaveImageSizes()
{
Settings.Properties.ImageresizerSizes = new ImageResizerSizes(Sizes);
_settingsUtils.SaveSettings(Settings.Properties.ImageresizerSizes.ToJsonString(), ModuleName, "sizes.json");
Settings.Properties.ImageresizerSizes = new ImageResizerSizes(imageSizes);
_settingsUtils.SaveSettings(Settings.ToJsonString(), ModuleName);
}
public static string GetEncoderGuid(int value)
{
// PNG Encoder guid
if (value == 0)
{
return "1b7cfaf4-713f-473c-bbcd-6137425faeaf";
}
// Bitmap Encoder guid
else if (value == 1)
{
return "0af1d87e-fcfe-4188-bdeb-a7906471cbe3";
}
// JPEG Encoder guid
else if (value == 2)
{
return "19e4a5aa-5662-4fc5-a0c0-1758028e1057";
}
// Tiff encoder guid.
else if (value == 3)
{
return "163bcc30-e2e9-4f0b-961d-a3e9fdb788a3";
}
// Tiff encoder guid.
else if (value == 4)
{
return "57a37caa-367a-4540-916b-f183c5093a4b";
}
// Gif encoder guid.
else if (value == 5)
{
return "1f8a5601-7d4d-4cbd-9c82-1bc8d4eeb9a5";
}
return null;
}
public static int GetEncoderIndex(string value)
{
// PNG Encoder guid
if (value == "1b7cfaf4-713f-473c-bbcd-6137425faeaf")
{
return 0;
}
// Bitmap Encoder guid
else if (value == "0af1d87e-fcfe-4188-bdeb-a7906471cbe3")
{
return 1;
}
// JPEG Encoder guid
else if (value == "19e4a5aa-5662-4fc5-a0c0-1758028e1057")
{
return 2;
}
// Tiff encoder guid.
else if (value == "163bcc30-e2e9-4f0b-961d-a3e9fdb788a3")
{
return 3;
}
// Tiff encoder guid.
else if (value == "57a37caa-367a-4540-916b-f183c5093a4b")
{
return 4;
}
// Gif encoder guid.
else if (value == "1f8a5601-7d4d-4cbd-9c82-1bc8d4eeb9a5")
{
return 5;
}
return -1;
}
public void SizePropertyChanged(object sender, PropertyChangedEventArgs e)
{
ImageSize modifiedSize = (ImageSize)sender;
ObservableCollection<ImageSize> imageSizes = Sizes;
imageSizes.First(x => x.Id == modifiedSize.Id).Update(modifiedSize);
_advancedSizes = imageSizes;
SavesImageSizes(imageSizes);
SaveImageSizes();
}
private static string GenerateNameForNewSize(in ObservableCollection<ImageSize> sizesList, in string namePrefix)
private string GenerateNameForNewSize(string namePrefix)
{
int newSizeCounter = 0;
foreach (ImageSize imgSize in sizesList)
foreach (var name in Sizes.Select(x => x.Name))
{
string name = imgSize.Name;
if (name.StartsWith(namePrefix, StringComparison.InvariantCulture))
{
if (int.TryParse(name.AsSpan(namePrefix.Length), out int number))
{
if (newSizeCounter < number)
if (name.StartsWith(namePrefix, StringComparison.InvariantCulture) &&
int.TryParse(name.AsSpan(namePrefix.Length), out int number) &&
newSizeCounter < number)
{
newSizeCounter = number;
}
}
}
}
return $"{namePrefix} {++newSizeCounter}";
}
@ -429,5 +361,4 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
InitializeEnabledValue();
OnPropertyChanged(nameof(IsEnabled));
}
}
}