diff --git a/src/dsc/Microsoft.PowerToys.Configure/examples/installAndConfiguration.dsc.yaml b/src/dsc/Microsoft.PowerToys.Configure/examples/installAndConfiguration.dsc.yaml index 9b9b8c2408..8a282c7785 100644 --- a/src/dsc/Microsoft.PowerToys.Configure/examples/installAndConfiguration.dsc.yaml +++ b/src/dsc/Microsoft.PowerToys.Configure/examples/installAndConfiguration.dsc.yaml @@ -24,4 +24,17 @@ properties: FancyzonesEditorHotkey: "Shift+Ctrl+Alt+F" FileLocksmith: Enabled: false + ImageResizer: + ImageResizerSizes: + - Name: Square2x + Width: 200 + Height: 200 + Unit: "Percent" + Fit: "Stretch" + - Name: MyInchSize + Width: 1024 + Height: 1024 + Unit: "Inch" + Fit: "Fit" + configurationVersion: 0.2.0 diff --git a/src/dsc/PowerToys.Settings.DSC.Schema.Generator/DSCGeneration.cs b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/DSCGeneration.cs index f0fef8bd29..f1763899b4 100644 --- a/src/dsc/PowerToys.Settings.DSC.Schema.Generator/DSCGeneration.cs +++ b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/DSCGeneration.cs @@ -21,7 +21,7 @@ internal sealed class DSCGeneration public string Type; } - private static readonly Dictionary AdditionalPropertiesInfoPerModule = new Dictionary { { "PowerLauncher", new AdditionalPropertiesInfo { Name = "Plugins", Type = "Hashtable[]" } } }; + private static readonly Dictionary AdditionalPropertiesInfoPerModule = new Dictionary { { "PowerLauncher", new AdditionalPropertiesInfo { Name = "Plugins", Type = "Hashtable[]" } }, { "ImageResizer", new AdditionalPropertiesInfo { Name = "ImageresizerSizes", Type = "Hashtable[]" } } }; private static string EmitEnumDefinition(Type type) { diff --git a/src/settings-ui/Settings.UI.Library/ImageSize.cs b/src/settings-ui/Settings.UI.Library/ImageSize.cs index 8e766d38cc..7bbad59726 100644 --- a/src/settings-ui/Settings.UI.Library/ImageSize.cs +++ b/src/settings-ui/Settings.UI.Library/ImageSize.cs @@ -16,38 +16,38 @@ namespace Microsoft.PowerToys.Settings.UI.Library { Id = id; Name = string.Empty; - Fit = (int)ResizeFit.Fit; + Fit = ResizeFit.Fit; Width = 0; Height = 0; - Unit = (int)ResizeUnit.Pixel; + Unit = ResizeUnit.Pixel; } public ImageSize() { Id = 0; Name = string.Empty; - Fit = (int)ResizeFit.Fit; + Fit = ResizeFit.Fit; Width = 0; Height = 0; - Unit = (int)ResizeUnit.Pixel; + Unit = ResizeUnit.Pixel; } public ImageSize(int id, string name, ResizeFit fit, double width, double height, ResizeUnit unit) { Id = id; Name = name; - Fit = (int)fit; + Fit = fit; Width = width; Height = height; - Unit = (int)unit; + Unit = unit; } private int _id; private string _name; - private int _fit; + private ResizeFit _fit; private double _height; private double _width; - private int _unit; + private ResizeUnit _unit; public int Id { @@ -70,7 +70,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library { get { - if (Unit == 2 && Fit != 2) + if (Unit == ResizeUnit.Percent && Fit != ResizeFit.Stretch) { return 0; } @@ -85,7 +85,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library { get { - if (Unit == 2 && Fit != 2) + if (Unit == ResizeUnit.Percent && Fit != ResizeFit.Stretch) { return false; } @@ -115,7 +115,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library } [JsonPropertyName("fit")] - public int Fit + public ResizeFit Fit { get { @@ -193,7 +193,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library } [JsonPropertyName("unit")] - public int Unit + public ResizeUnit Unit { get { diff --git a/src/settings-ui/Settings.UI.Library/Utilities/SetAdditionalSettingsCommandLineCommand.cs b/src/settings-ui/Settings.UI.Library/Utilities/SetAdditionalSettingsCommandLineCommand.cs index 2e8d99ec7b..122dccfd4b 100644 --- a/src/settings-ui/Settings.UI.Library/Utilities/SetAdditionalSettingsCommandLineCommand.cs +++ b/src/settings-ui/Settings.UI.Library/Utilities/SetAdditionalSettingsCommandLineCommand.cs @@ -3,8 +3,11 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Globalization; +using System.IO; using System.Linq; using System.Reflection; using System.Text.Json; @@ -28,18 +31,25 @@ public sealed class SetAdditionalSettingsCommandLineCommand private struct AdditionalPropertyInfo { - public string RootPropertyName; - public JsonValueKind RootObjectType; + // A path to the property starting from the root module Settings object in the following format: "RootPropertyA.NestedPropertyB[...]" + public string PropertyPath; + + // Property Type hint so we know how to handle it + public JsonValueKind PropertyType; } - private static readonly Dictionary SupportedAdditionalPropertiesInfoForModules = new Dictionary { { "PowerLauncher", new AdditionalPropertyInfo { RootPropertyName = "Plugins", RootObjectType = JsonValueKind.Array } } }; + private static readonly Dictionary SupportedAdditionalPropertiesInfoForModules = new Dictionary { { "PowerLauncher", new AdditionalPropertyInfo { PropertyPath = "Plugins", PropertyType = JsonValueKind.Array } }, { "ImageResizer", new AdditionalPropertyInfo { PropertyPath = "Properties.ImageresizerSizes.Value", PropertyType = JsonValueKind.Array } } }; - private static void ExecuteRootArray(JsonElement.ArrayEnumerator properties, IEnumerable currentPropertyValuesArray) + private static IEnumerable ExecuteRootArray(IEnumerable properties, IEnumerable currentPropertyValuesArray) { - // In case it's an array of object -> combine the existing values with the provided - var currentPropertyValueType = currentPropertyValuesArray.FirstOrDefault()?.GetType(); + // In case it's an array of objects -> combine the existing values with the provided + var result = currentPropertyValuesArray; + var currentPropertyValueType = GetUnderlyingTypeOfCollection(currentPropertyValuesArray); object matchedElement = null; + + object newKeyPropertyValue = null; + foreach (var arrayElement in properties) { var newElementPropertyValues = new Dictionary(); @@ -47,15 +57,16 @@ public sealed class SetAdditionalSettingsCommandLineCommand { var elementPropertyName = elementProperty.Name; var elementPropertyType = currentPropertyValueType.GetProperty(elementPropertyName).PropertyType; - var elemePropertyValue = ICmdLineRepresentable.ParseFor(elementPropertyType, elementProperty.Value.ToString()); + var elementNewPropertyValue = ICmdLineRepresentable.ParseFor(elementPropertyType, elementProperty.Value.ToString()); if (elementPropertyName == KeyPropertyName) { + newKeyPropertyValue = elementNewPropertyValue; foreach (var currentElementValue in currentPropertyValuesArray) { var currentElementType = currentElementValue.GetType(); var keyPropertyNameInfo = currentElementType.GetProperty(KeyPropertyName); - var keyPropertyValue = keyPropertyNameInfo.GetValue(currentElementValue); - if (string.Equals(keyPropertyValue, elemePropertyValue)) + var currentKeyPropertyValue = keyPropertyNameInfo.GetValue(currentElementValue); + if (string.Equals(currentKeyPropertyValue, elementNewPropertyValue)) { matchedElement = currentElementValue; break; @@ -64,7 +75,18 @@ public sealed class SetAdditionalSettingsCommandLineCommand } else { - newElementPropertyValues.Add(elementPropertyName, elemePropertyValue); + newElementPropertyValues.Add(elementPropertyName, elementNewPropertyValue); + } + } + + // Appending a new element -> create it first using a default ctor with 0 args and append it to the result + if (matchedElement == null) + { + newElementPropertyValues.Add(KeyPropertyName, newKeyPropertyValue); + matchedElement = Activator.CreateInstance(currentPropertyValueType); + if (matchedElement != null) + { + result = result.Append(matchedElement); } } @@ -76,6 +98,148 @@ public sealed class SetAdditionalSettingsCommandLineCommand propertyInfo.SetValue(matchedElement, overriddenProperty.Value); } } + + matchedElement = null; + } + + return result; + } + + private static object GetNestedPropertyValue(object obj, string propertyPath) + { + if (obj == null || string.IsNullOrWhiteSpace(propertyPath)) + { + return null; + } + + var properties = propertyPath.Split('.'); + object currentObject = obj; + PropertyInfo currentProperty = null; + + foreach (var property in properties) + { + if (currentObject == null) + { + return null; + } + + currentProperty = currentObject.GetType().GetProperty(property); + if (currentProperty == null) + { + return null; + } + + currentObject = currentProperty.GetValue(currentObject); + } + + return currentObject; + } + + // To apply changes to a generic collection, we must recreate it and assign it to the property + private static object CreateCompatibleCollection(Type collectionType, Type elementType, IEnumerable newValues) + { + if (typeof(IList<>).MakeGenericType(elementType).IsAssignableFrom(collectionType) || + typeof(ObservableCollection<>).MakeGenericType(elementType).IsAssignableFrom(collectionType)) + { + var concreteType = typeof(List<>).MakeGenericType(elementType); + if (typeof(ObservableCollection<>).MakeGenericType(elementType).IsAssignableFrom(collectionType)) + { + concreteType = typeof(ObservableCollection<>).MakeGenericType(elementType); + } + else if (collectionType.IsInterface || collectionType.IsAbstract) + { + concreteType = typeof(List<>).MakeGenericType(elementType); + } + + var list = (IList)Activator.CreateInstance(concreteType); + foreach (var newValue in newValues) + { + list.Add(Convert.ChangeType(newValue, elementType, CultureInfo.InvariantCulture)); + } + + return list; + } + else if (typeof(IEnumerable<>).MakeGenericType(elementType).IsAssignableFrom(collectionType)) + { + var listType = typeof(List<>).MakeGenericType(elementType); + var list = (IList)Activator.CreateInstance(listType); + foreach (var newValue in newValues) + { + list.Add(Convert.ChangeType(newValue, elementType, CultureInfo.InvariantCulture)); + } + + return list; + } + + return null; + } + + private static void SetNestedPropertyValue(object obj, string propertyPath, IEnumerable newValues) + { + if (obj == null || string.IsNullOrWhiteSpace(propertyPath)) + { + return; + } + + var properties = propertyPath.Split('.'); + object currentObject = obj; + PropertyInfo currentProperty = null; + + for (int i = 0; i < properties.Length - 1; i++) + { + if (currentObject == null) + { + return; + } + + currentProperty = currentObject.GetType().GetProperty(properties[i]); + if (currentProperty == null) + { + return; + } + + currentObject = currentProperty.GetValue(currentObject); + } + + if (currentObject == null) + { + return; + } + + currentProperty = currentObject.GetType().GetProperty(properties.Last()); + if (currentProperty == null) + { + return; + } + + var propertyType = currentProperty.PropertyType; + var elementType = propertyType.GetGenericArguments()[0]; + + var newCollection = CreateCompatibleCollection(propertyType, elementType, newValues); + + if (newCollection != null) + { + currentProperty.SetValue(currentObject, newCollection); + } + } + + private static Type GetUnderlyingTypeOfCollection(IEnumerable currentPropertyValuesArray) + { + Type collectionType = currentPropertyValuesArray.GetType(); + + if (!collectionType.IsGenericType) + { + throw new ArgumentException("Invalid json data supplied"); + } + + Type[] genericArguments = collectionType.GetGenericArguments(); + if (genericArguments.Length > 0) + { + return genericArguments[0]; + } + else + { + throw new ArgumentException("Invalid json data supplied"); } } @@ -91,14 +255,39 @@ public sealed class SetAdditionalSettingsCommandLineCommand return; } - var propertyValueInfo = settingsConfigType.GetProperty(additionalPropertiesInfo.RootPropertyName); - var currentPropertyValue = propertyValueInfo.GetValue(settingsConfig); + var currentPropertyValue = GetNestedPropertyValue(settingsConfig, additionalPropertiesInfo.PropertyPath); // For now, only a certain data shapes are supported - switch (additionalPropertiesInfo.RootObjectType) + switch (additionalPropertiesInfo.PropertyType) { case JsonValueKind.Array: - ExecuteRootArray(settings.RootElement.EnumerateArray(), currentPropertyValue as IEnumerable); + if (currentPropertyValue == null) + { + currentPropertyValue = new JsonArray(); + } + + IEnumerable propertiesToSet = null; + + // Powershell ConvertTo-Json call omits wrapping a single value in an array, so we must do it here + if (settings.RootElement.ValueKind == JsonValueKind.Object) + { + var wrapperArray = new JsonArray(); + wrapperArray.Add(settings.RootElement); + propertiesToSet = (IEnumerable)wrapperArray.GetEnumerator(); + } + else if (settings.RootElement.ValueKind == JsonValueKind.Array) + { + propertiesToSet = settings.RootElement.EnumerateArray().AsEnumerable(); + } + else + { + throw new ArgumentException("Invalid json data supplied"); + } + + var newPropertyValue = ExecuteRootArray(propertiesToSet, currentPropertyValue as IEnumerable); + + SetNestedPropertyValue(settingsConfig, additionalPropertiesInfo.PropertyPath, newPropertyValue); + break; default: throw new NotImplementedException(); diff --git a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/ImageResizer.cs b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/ImageResizer.cs index 530fd59d25..4bd392008c 100644 --- a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/ImageResizer.cs +++ b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/ImageResizer.cs @@ -233,10 +233,10 @@ namespace ViewModelTests // Assert ImageSize newTestSize = viewModel.Sizes.First(x => x.Id == 0); Assert.AreEqual(newTestSize.Name, "New size 1"); - Assert.AreEqual(newTestSize.Fit, (int)ResizeFit.Fit); + Assert.AreEqual(newTestSize.Fit, ResizeFit.Fit); Assert.AreEqual(newTestSize.Width, 854); Assert.AreEqual(newTestSize.Height, 480); - Assert.AreEqual(newTestSize.Unit, (int)ResizeUnit.Pixel); + Assert.AreEqual(newTestSize.Unit, ResizeUnit.Pixel); } [TestMethod] @@ -287,10 +287,10 @@ namespace ViewModelTests { Id = 0, Name = "Test", - Fit = (int)ResizeFit.Fit, + Fit = ResizeFit.Fit, Width = 30, Height = 30, - Unit = (int)ResizeUnit.Pixel, + Unit = ResizeUnit.Pixel, }; double negativeWidth = -2.0; diff --git a/src/settings-ui/Settings.UI/Converters/ImageResizerFitToIntConverter.cs b/src/settings-ui/Settings.UI/Converters/ImageResizerFitToIntConverter.cs new file mode 100644 index 0000000000..675ad8a13f --- /dev/null +++ b/src/settings-ui/Settings.UI/Converters/ImageResizerFitToIntConverter.cs @@ -0,0 +1,32 @@ +// 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 Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.UI.Xaml.Data; + +namespace Microsoft.PowerToys.Settings.UI.Converters; + +public sealed class ImageResizerFitToIntConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is ResizeFit) + { + return (int)value; + } + + return 0; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + if (value is int intValue) + { + return (ResizeFit)intValue; + } + + return ResizeFit.Fill; + } +} diff --git a/src/settings-ui/Settings.UI/Converters/ImageResizerUnitToIntConverter.cs b/src/settings-ui/Settings.UI/Converters/ImageResizerUnitToIntConverter.cs new file mode 100644 index 0000000000..682b03dafc --- /dev/null +++ b/src/settings-ui/Settings.UI/Converters/ImageResizerUnitToIntConverter.cs @@ -0,0 +1,32 @@ +// 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 Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.UI.Xaml.Data; + +namespace Microsoft.PowerToys.Settings.UI.Converters; + +public sealed class ImageResizerUnitToIntConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is ResizeUnit) + { + return (int)value; + } + + return 0; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + if (value is int intValue) + { + return (ResizeUnit)intValue; + } + + return ResizeUnit.Centimeter; + } +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml index e7d4b55f6f..c004be195d 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml @@ -15,7 +15,9 @@ mc:Ignorable="d"> + + + SelectedIndex="{x:Bind Fit, Mode=TwoWay, Converter={StaticResource ImageResizerFitToIntConverter}}"> @@ -146,7 +148,7 @@ + SelectedIndex="{Binding Unit, Mode=TwoWay, Converter={StaticResource ImageResizerUnitToIntConverter}}">