[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. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; 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; field = value;
Name = string.Empty; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
Fit = ResizeFit.Fit; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AccessibleTextHelper)));
Width = 0;
Height = 0;
Unit = ResizeUnit.Pixel;
} }
public ImageSize() return changed;
{
Id = 0;
Name = string.Empty;
Fit = ResizeFit.Fit;
Width = 0;
Height = 0;
Unit = ResizeUnit.Pixel;
} }
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; Id = id;
Name = name; Name = name;
@ -51,85 +48,38 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public int Id public int Id
{ {
get get => _id;
{ set => SetProperty(ref _id, value);
return _id;
} }
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) // 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.
_id = value; get => !(Unit == ResizeUnit.Percent && Fit != ResizeFit.Stretch);
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;
}
}
} }
[JsonPropertyName("name")] [JsonPropertyName("name")]
public string Name public string Name
{ {
get get => _name;
{ set => SetProperty(ref _name, value);
return _name;
}
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged();
}
}
} }
[JsonPropertyName("fit")] [JsonPropertyName("fit")]
public ResizeFit Fit public ResizeFit Fit
{ {
get get => _fit;
{
return _fit;
}
set set
{ {
if (_fit != value) if (SetProperty(ref _fit, value))
{ {
_fit = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsHeightUsed)));
OnPropertyChanged();
OnPropertyChanged(nameof(ExtraBoxOpacity));
OnPropertyChanged(nameof(EnableEtraBoxes));
} }
} }
} }
@ -137,107 +87,35 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("width")] [JsonPropertyName("width")]
public double Width public double Width
{ {
get get => _width;
{ set => SetProperty(ref _width, value < 0 || double.IsNaN(value) ? 0 : value);
return _width;
}
set
{
double newWidth = -1;
if (value < 0 || double.IsNaN(value))
{
newWidth = 0;
}
else
{
newWidth = value;
}
if (_width != newWidth)
{
_width = newWidth;
OnPropertyChanged();
}
}
} }
[JsonPropertyName("height")] [JsonPropertyName("height")]
public double Height public double Height
{ {
get get => _height;
{ set => SetProperty(ref _height, value < 0 || double.IsNaN(value) ? 0 : value);
return _height;
}
set
{
double newHeight = -1;
if (value < 0 || double.IsNaN(value))
{
newHeight = 0;
}
else
{
newHeight = value;
}
if (_height != newHeight)
{
_height = newHeight;
OnPropertyChanged();
}
}
} }
[JsonPropertyName("unit")] [JsonPropertyName("unit")]
public ResizeUnit Unit public ResizeUnit Unit
{ {
get get => _unit;
{
return _unit;
}
set set
{ {
if (_unit != value) if (SetProperty(ref _unit, value))
{ {
_unit = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsHeightUsed)));
OnPropertyChanged();
OnPropertyChanged(nameof(ExtraBoxOpacity));
OnPropertyChanged(nameof(EnableEtraBoxes));
} }
} }
} }
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) public string ToJsonString() => JsonSerializer.Serialize(this);
{
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);
}
}
} }

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

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

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

@ -3,36 +3,34 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Windows;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml.Data; 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) public object Convert(object value, Type targetType, object parameter, string language)
{ {
var toLower = false; if (value is ResizeFit fit && FitToText.TryGetValue(fit, out string fitText))
if ((string)parameter == "ToLower")
{ {
toLower = true; return parameter is string lowerParam && lowerParam == "ToLower" ?
fitText.ToLower(CultureInfo.CurrentCulture) :
fitText;
} }
string targetValue = string.Empty; return DependencyProperty.UnsetValue;
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;
} }
public object ConvertBack(object value, Type targetType, object parameter, string language) public object ConvertBack(object value, Type targetType, object parameter, string language)
@ -40,4 +38,3 @@ namespace Microsoft.PowerToys.Settings.UI.Converters
return value; 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,37 +3,35 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Windows;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml.Data; 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) public object Convert(object value, Type targetType, object parameter, string language)
{ {
var toLower = false; if (value is ResizeUnit unit && UnitToText.TryGetValue(unit, out string unitText))
if ((string)parameter == "ToLower")
{ {
toLower = true; return parameter is string lowerParam && lowerParam == "ToLower" ?
unitText.ToLower(CultureInfo.CurrentCulture) :
unitText;
} }
string targetValue = string.Empty; return DependencyProperty.UnsetValue;
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;
} }
public object ConvertBack(object value, Type targetType, object parameter, string language) public object ConvertBack(object value, Type targetType, object parameter, string language)
@ -41,4 +39,3 @@ namespace Microsoft.PowerToys.Settings.UI.Converters
return value; return value;
} }
} }
}

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

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

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

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

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

@ -1169,10 +1169,6 @@
<data name="ImageResizer_Size.Header" xml:space="preserve"> <data name="ImageResizer_Size.Header" xml:space="preserve">
<value>Unit</value> <value>Unit</value>
</data> </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"> <data name="RemoveItem.Text" xml:space="preserve">
<value>Delete</value> <value>Delete</value>
</data> </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"> <data name="ImageResizer_Unit_Pixel" xml:space="preserve">
<value>Pixels</value> <value>Pixels</value>
</data> </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> <value>Edit</value>
</data> </data>
<data name="ImageResizer_EditSize.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve"> <data name="ImageResizer_EditSize.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Edit size</value> <value>Edit size</value>
</data> </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"> <data name="No" xml:space="preserve">
<value>No</value> <value>No</value>
<comment>Label of a cancel button</comment> <comment>Label of a cancel button</comment>

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

@ -3,21 +3,46 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using global::PowerToys.GPOWrapper; using global::PowerToys.GPOWrapper;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.ViewModels namespace Microsoft.PowerToys.Settings.UI.ViewModels;
{
public class ImageResizerViewModel : Observable public partial 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 GeneralSettings GeneralSettingsConfig { get; set; }
private readonly ISettingsUtils _settingsUtils; 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) public ImageResizerViewModel(ISettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, Func<string, int> ipcMSGCallBackFunc, Func<string, string> resourceLoader)
{ {
_isInitializing = true;
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils)); _settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
// To obtain the general settings configurations of PowerToys. // To obtain the general settings configurations of PowerToys.
@ -59,21 +86,17 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
InitializeEnabledValue(); InitializeEnabledValue();
_advancedSizes = Settings.Properties.ImageresizerSizes.Value; Sizes = new ObservableCollection<ImageSize>(Settings.Properties.ImageresizerSizes.Value);
_jpegQualityLevel = Settings.Properties.ImageresizerJpegQualityLevel.Value; JPEGQualityLevel = Settings.Properties.ImageresizerJpegQualityLevel.Value;
_pngInterlaceOption = Settings.Properties.ImageresizerPngInterlaceOption.Value; PngInterlaceOption = Settings.Properties.ImageresizerPngInterlaceOption.Value;
_tiffCompressOption = Settings.Properties.ImageresizerTiffCompressOption.Value; TiffCompressOption = Settings.Properties.ImageresizerTiffCompressOption.Value;
_fileName = Settings.Properties.ImageresizerFileName.Value; FileName = Settings.Properties.ImageresizerFileName.Value;
_keepDateModified = Settings.Properties.ImageresizerKeepDateModified.Value; KeepDateModified = Settings.Properties.ImageresizerKeepDateModified.Value;
_encoderGuidId = GetEncoderIndex(Settings.Properties.ImageresizerFallbackEncoder.Value); Encoder = GetEncoderIndex(Settings.Properties.ImageresizerFallbackEncoder.Value);
int i = 0; _customSize = Settings.Properties.ImageresizerCustomSize.Value;
foreach (ImageSize size in _advancedSizes)
{ _isInitializing = false;
size.Id = i;
i++;
size.PropertyChanged += SizePropertyChanged;
}
} }
private void InitializeEnabledValue() private void InitializeEnabledValue()
@ -94,7 +117,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private GpoRuleConfigured _enabledGpoRuleConfiguration; private GpoRuleConfigured _enabledGpoRuleConfiguration;
private bool _enabledStateIsGPOConfigured; private bool _enabledStateIsGPOConfigured;
private bool _isEnabled; private bool _isEnabled;
private ObservableCollection<ImageSize> _advancedSizes = new ObservableCollection<ImageSize>(); private ObservableCollection<ImageSize> _sizes = [];
private int _jpegQualityLevel; private int _jpegQualityLevel;
private int _pngInterlaceOption; private int _pngInterlaceOption;
private int _tiffCompressOption; private int _tiffCompressOption;
@ -106,10 +129,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public bool IsEnabled public bool IsEnabled
{ {
get get => _isEnabled;
{
return _isEnabled;
}
set set
{ {
@ -139,155 +159,163 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public ObservableCollection<ImageSize> Sizes public ObservableCollection<ImageSize> Sizes
{ {
get get => _sizes;
{
return _advancedSizes;
}
set set
{ {
SavesImageSizes(value); if (_sizes != null)
_advancedSizes = value; {
_sizes.CollectionChanged -= Sizes_CollectionChanged;
UnsubscribeFromItemPropertyChanged(_sizes);
}
_sizes = value;
if (_sizes != null)
{
_sizes.CollectionChanged += Sizes_CollectionChanged;
SubscribeToItemPropertyChanged(_sizes);
}
OnPropertyChanged(nameof(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 public int JPEGQualityLevel
{ {
get get => _jpegQualityLevel;
{
return _jpegQualityLevel;
}
set set
{ {
if (_jpegQualityLevel != value) SetProperty(ref _jpegQualityLevel, value, v => Settings.Properties.ImageresizerJpegQualityLevel.Value = v);
{
_jpegQualityLevel = value;
Settings.Properties.ImageresizerJpegQualityLevel.Value = value;
_settingsUtils.SaveSettings(Settings.ToJsonString(), ModuleName);
OnPropertyChanged(nameof(JPEGQualityLevel));
}
} }
} }
public int PngInterlaceOption public int PngInterlaceOption
{ {
get get => _pngInterlaceOption;
{
return _pngInterlaceOption;
}
set set
{ {
if (_pngInterlaceOption != value) SetProperty(ref _pngInterlaceOption, value, v => Settings.Properties.ImageresizerPngInterlaceOption.Value = v);
{
_pngInterlaceOption = value;
Settings.Properties.ImageresizerPngInterlaceOption.Value = value;
_settingsUtils.SaveSettings(Settings.ToJsonString(), ModuleName);
OnPropertyChanged(nameof(PngInterlaceOption));
}
} }
} }
public int TiffCompressOption public int TiffCompressOption
{ {
get get => _tiffCompressOption;
{
return _tiffCompressOption;
}
set set
{ {
if (_tiffCompressOption != value) SetProperty(ref _tiffCompressOption, value, v => Settings.Properties.ImageresizerTiffCompressOption.Value = v);
{
_tiffCompressOption = value;
Settings.Properties.ImageresizerTiffCompressOption.Value = value;
_settingsUtils.SaveSettings(Settings.ToJsonString(), ModuleName);
OnPropertyChanged(nameof(TiffCompressOption));
}
} }
} }
public string FileName public string FileName
{ {
get get => _fileName;
{
return _fileName;
}
set set
{ {
if (!string.IsNullOrWhiteSpace(value)) if (!string.IsNullOrWhiteSpace(value))
{ {
_fileName = value; SetProperty(ref _fileName, value, v => Settings.Properties.ImageresizerFileName.Value = v);
Settings.Properties.ImageresizerFileName.Value = value;
_settingsUtils.SaveSettings(Settings.ToJsonString(), ModuleName);
OnPropertyChanged(nameof(FileName));
} }
} }
} }
public bool KeepDateModified public bool KeepDateModified
{ {
get get => _keepDateModified;
{
return _keepDateModified;
}
set set
{ {
_keepDateModified = value; SetProperty(ref _keepDateModified, value, v => Settings.Properties.ImageresizerKeepDateModified.Value = v);
Settings.Properties.ImageresizerKeepDateModified.Value = value;
_settingsUtils.SaveSettings(Settings.ToJsonString(), ModuleName);
OnPropertyChanged(nameof(KeepDateModified));
} }
} }
public int Encoder public int Encoder
{ {
get get => _encoderGuidId;
{
return _encoderGuidId;
}
set set
{ {
if (_encoderGuidId != value) SetProperty(ref _encoderGuidId, value, v => Settings.Properties.ImageresizerFallbackEncoder.Value = GetEncoderGuid(v));
{
_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);
} }
} }
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. int index = EncoderGuids.IndexOf(encoderGuid);
/// If the parameter is unexpectedly empty or null, we fill the parameter with a non-localized string. return index == -1 ? throw new ArgumentException("Encoder GUID not found.", nameof(encoderGuid)) : index;
/// 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;
ObservableCollection<ImageSize> imageSizes = Sizes; public void AddImageSize(string namePrefix = "")
int maxId = imageSizes.Count > 0 ? imageSizes.OrderBy(x => x.Id).Last().Id : -1; {
string sizeName = GenerateNameForNewSize(imageSizes, sizeNamePrefix); if (string.IsNullOrEmpty(namePrefix))
{
namePrefix = DefaultPresetNamePrefix;
}
ImageSize newSize = new ImageSize(maxId + 1, sizeName, ResizeFit.Fit, 854, 480, ResizeUnit.Pixel); int maxId = Sizes.Count > 0 ? Sizes.Max(x => x.Id) : -1;
newSize.PropertyChanged += SizePropertyChanged; string sizeName = GenerateNameForNewSize(namePrefix);
imageSizes.Add(newSize);
_advancedSizes = imageSizes; Sizes.Add(new ImageSize(maxId + 1, GenerateNameForNewSize(namePrefix), _customSize.Fit, _customSize.Width, _customSize.Height, _customSize.Unit));
SavesImageSizes(imageSizes);
// Set the focus requested flag to indicate that an add operation has occurred during the ContainerContentChanging event // Set the focus requested flag to indicate that an add operation has occurred during the ContainerContentChanging event
IsListViewFocusRequested = true; IsListViewFocusRequested = true;
@ -295,131 +323,35 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public void DeleteImageSize(int id) public void DeleteImageSize(int id)
{ {
ImageSize size = _advancedSizes.First(x => x.Id == id); ImageSize size = _sizes.First(x => x.Id == id);
ObservableCollection<ImageSize> imageSizes = Sizes; Sizes.Remove(size);
imageSizes.Remove(size);
_advancedSizes = imageSizes;
SavesImageSizes(imageSizes);
} }
public void SavesImageSizes(ObservableCollection<ImageSize> imageSizes) public void SaveImageSizes()
{ {
Settings.Properties.ImageresizerSizes = new ImageResizerSizes(Sizes);
_settingsUtils.SaveSettings(Settings.Properties.ImageresizerSizes.ToJsonString(), ModuleName, "sizes.json"); _settingsUtils.SaveSettings(Settings.Properties.ImageresizerSizes.ToJsonString(), ModuleName, "sizes.json");
Settings.Properties.ImageresizerSizes = new ImageResizerSizes(imageSizes);
_settingsUtils.SaveSettings(Settings.ToJsonString(), ModuleName); _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) public void SizePropertyChanged(object sender, PropertyChangedEventArgs e)
{ {
ImageSize modifiedSize = (ImageSize)sender; SaveImageSizes();
ObservableCollection<ImageSize> imageSizes = Sizes;
imageSizes.First(x => x.Id == modifiedSize.Id).Update(modifiedSize);
_advancedSizes = imageSizes;
SavesImageSizes(imageSizes);
} }
private static string GenerateNameForNewSize(in ObservableCollection<ImageSize> sizesList, in string namePrefix) private string GenerateNameForNewSize(string namePrefix)
{ {
int newSizeCounter = 0; 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) &&
int.TryParse(name.AsSpan(namePrefix.Length), out int number) &&
if (name.StartsWith(namePrefix, StringComparison.InvariantCulture)) newSizeCounter < number)
{
if (int.TryParse(name.AsSpan(namePrefix.Length), out int number))
{
if (newSizeCounter < number)
{ {
newSizeCounter = number; newSizeCounter = number;
} }
} }
}
}
return $"{namePrefix} {++newSizeCounter}"; return $"{namePrefix} {++newSizeCounter}";
} }
@ -430,4 +362,3 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
OnPropertyChanged(nameof(IsEnabled)); OnPropertyChanged(nameof(IsEnabled));
} }
} }
}