mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-08-29 10:09:43 +02:00
[Settings][Image Resizer] Edit/add presets in a ContentDialog instead of a Flyout (#49161)
## Summary of the Pull Request Replaces the inline preset-edit **Flyout** on the Image Resizer settings page with a **ContentDialog**, matching the add/edit pattern already used on the **Color Picker** page. This aligns the experience with the Windows 11 / Fluent paradigm and fixes preset settings being saved on every intermediate change. Editing now happens on a **working copy** of the preset (`ImageSize.Clone()`), which is committed only when the user presses **Save/Update**. As a side effect, the intermediate width/height spinner changes no longer persist `settings.json` / `sizes.json` on every value change — resolving #36938. The per-row **delete** action moves from an inline trash button into a **"..." (More options)** `MenuFlyout`, again matching the Color Picker page. Historically this editing was a Flyout rather than a ContentDialog due to known `ContentDialog` / `XamlRoot` issues back when Settings was a UWP app. Now that Settings is on WinUI 3 / Windows App SDK, `ContentDialog` works reliably (as Color Picker's `ColorFormatDialog` demonstrates), so the original constraint no longer applies. https://github.com/user-attachments/assets/bf71b0a9-c3f8-4078-95c7-c7ee9dc7b24b ## PR Checklist - [x] Closes: #49157 - [x] Closes: #36938 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [x] **Localization:** All end-user-facing strings can be localized (added to `Resources.resw`; reused existing keys for the delete menu) - [ ] **Dev docs:** Added/updated - [x] **New binaries:** None added ## Detailed Description of the Pull Request / Additional comments - **Dialog:** Clicking a preset card (or **Add new size**) opens an `EditSizeDialog` `ContentDialog`. Fields are bound with compiled `{x:Bind}` against a working-copy `EditingSize`. The dimensions field header is dynamic — **"Width"** when height is used, **"Size"** for aspect-ratio-preserving percentage scaling — so the label isn't misleading. The dialog is widened for a less cramped layout. - **Save semantics:** - `ImageSize.Clone()` — builds the editable working copy. - `ImageResizerViewModel.CreateNewImageSizeModel()` — builds a default-valued model for the add dialog without adding it to `Sizes`. - `ImageResizerViewModel.AddImageSize(ImageSize)` — commits a new preset with the next unique ID. - `ImageResizerViewModel.UpdateImageSize(original, updated)` — applies edited values back onto the original, temporarily detaching the per-item `PropertyChanged` save handler so it persists **once** instead of on every property. This is what fixes the "saved too often" behavior in #36938. - **Delete:** per-row `Button` → `MenuFlyout` with a Delete `MenuFlyoutItem`; the `ImageSize` is passed via `CommandParameter="{x:Bind}"` (robust inside a flyout popup) and the Yes/No confirmation dialog is preserved. ## Validation Steps Performed - Built `PowerToys.Settings` (x64/Debug) — clean (exit 0). - Ran the runner from this build and manually validated in Settings → Image Resizer: - **Add new size** opens the dialog pre-filled; Save adds the preset; Cancel discards. - **Editing** a preset in the dialog and pressing Cancel leaves the original untouched (working-copy clone). - Selecting **Percent** shows the **Size** header (not a misleading "Width"). - Spinning width/height inside the dialog no longer writes settings files on each change; a single save occurs on Update (#36938). - The **"..."** menu shows **Delete**, with the confirmation dialog intact. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -129,5 +129,11 @@ public partial class ImageSize : INotifyPropertyChanged, IHasId
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public ImageSize AccessibleTextHelper => this;
|
public ImageSize AccessibleTextHelper => this;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a copy of this <see cref="ImageSize"/>. Used to build a working copy for editing
|
||||||
|
/// so changes can be discarded (on cancel) without touching the original preset.
|
||||||
|
/// </summary>
|
||||||
|
public ImageSize Clone() => new ImageSize(_id, _name, _fit, _width, _height, _unit);
|
||||||
|
|
||||||
public string ToJsonString() => JsonSerializer.Serialize(this);
|
public string ToJsonString() => JsonSerializer.Serialize(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -240,6 +240,71 @@ namespace ViewModelTests
|
|||||||
Assert.AreEqual(ResizeUnit.Pixel, newTestSize.Unit);
|
Assert.AreEqual(ResizeUnit.Pixel, newTestSize.Unit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void CreateNewImageSizeModelShouldNotAddToCollection()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
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);
|
||||||
|
int sizeOfOriginalArray = viewModel.Sizes.Count;
|
||||||
|
|
||||||
|
// act
|
||||||
|
ImageSize workingCopy = viewModel.CreateNewImageSizeModel("New size");
|
||||||
|
|
||||||
|
// Assert - the working copy is populated but not committed to the collection
|
||||||
|
Assert.IsNotNull(workingCopy);
|
||||||
|
Assert.AreEqual("New size 1", workingCopy.Name);
|
||||||
|
Assert.AreEqual(sizeOfOriginalArray, viewModel.Sizes.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void AddImageSizeWithModelShouldCommitPreparedSize()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
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);
|
||||||
|
int sizeOfOriginalArray = viewModel.Sizes.Count;
|
||||||
|
ImageSize workingCopy = viewModel.CreateNewImageSizeModel("New size");
|
||||||
|
|
||||||
|
// act
|
||||||
|
viewModel.AddImageSize(workingCopy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(sizeOfOriginalArray + 1, viewModel.Sizes.Count);
|
||||||
|
Assert.IsTrue(viewModel.Sizes.Contains(workingCopy));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void UpdateImageSizeShouldApplyWorkingCopyValuesToOriginal()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
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.AddImageSize("Original");
|
||||||
|
ImageSize original = viewModel.Sizes.First(x => x.Id == 0);
|
||||||
|
|
||||||
|
ImageSize edited = original.Clone();
|
||||||
|
edited.Name = "Edited";
|
||||||
|
edited.Fit = ResizeFit.Stretch;
|
||||||
|
edited.Width = 320;
|
||||||
|
edited.Height = 240;
|
||||||
|
edited.Unit = ResizeUnit.Percent;
|
||||||
|
|
||||||
|
// act
|
||||||
|
viewModel.UpdateImageSize(original, edited);
|
||||||
|
|
||||||
|
// Assert - the edits are applied to the original preset in place
|
||||||
|
Assert.AreEqual("Edited", original.Name);
|
||||||
|
Assert.AreEqual(ResizeFit.Stretch, original.Fit);
|
||||||
|
Assert.AreEqual(320, original.Width);
|
||||||
|
Assert.AreEqual(240, original.Height);
|
||||||
|
Assert.AreEqual(ResizeUnit.Percent, original.Unit);
|
||||||
|
Assert.AreSame(original, viewModel.Sizes.First(x => x.Id == 0));
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void DeleteImageSizeShouldDeleteImageSizeWhenSuccessful()
|
public void DeleteImageSizeShouldDeleteImageSizeWhenSuccessful()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ public sealed partial class ImageResizerSizeToAccessibleTextConverter : IValueCo
|
|||||||
private static readonly Dictionary<string, string> AccessibilityFormats = new()
|
private static readonly Dictionary<string, string> AccessibilityFormats = new()
|
||||||
{
|
{
|
||||||
{ "Edit", Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_EditButton_Accessibility_Name") },
|
{ "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 ImageResizerFitToStringConverter _fitConverter = new();
|
||||||
|
|||||||
@@ -59,9 +59,16 @@
|
|||||||
SelectionMode="None">
|
SelectionMode="None">
|
||||||
<ListView.ItemTemplate>
|
<ListView.ItemTemplate>
|
||||||
<DataTemplate x:Name="SingleLineDataTemplate" x:DataType="models:ImageSize">
|
<DataTemplate x:Name="SingleLineDataTemplate" x:DataType="models:ImageSize">
|
||||||
<tkcontrols:SettingsCard Header="{x:Bind Name, Mode=OneWay}">
|
<tkcontrols:SettingsCard
|
||||||
|
AutomationProperties.FullDescription="{x:Bind AccessibleTextHelper, Mode=OneWay, Converter={StaticResource ImageResizerSizeToAccessibleTextConverter}, ConverterParameter='Edit'}"
|
||||||
|
AutomationProperties.Name="{x:Bind Name, Mode=OneWay, Converter={StaticResource ImageResizerSizeToAccessibleTextConverter}, ConverterParameter='Edit'}"
|
||||||
|
Click="EditSize_Click"
|
||||||
|
Header="{x:Bind Name, Mode=OneWay}"
|
||||||
|
IsActionIconVisible="False"
|
||||||
|
IsClickEnabled="True">
|
||||||
<tkcontrols:SettingsCard.Resources>
|
<tkcontrols:SettingsCard.Resources>
|
||||||
<x:Double x:Key="SettingsCardLeftIndention">42</x:Double>
|
<x:Double x:Key="SettingsCardLeftIndention">42</x:Double>
|
||||||
|
<x:Double x:Key="SettingsCardActionButtonWidth">0</x:Double>
|
||||||
</tkcontrols:SettingsCard.Resources>
|
</tkcontrols:SettingsCard.Resources>
|
||||||
<tkcontrols:SettingsCard.Description>
|
<tkcontrols:SettingsCard.Description>
|
||||||
<StackPanel
|
<StackPanel
|
||||||
@@ -98,89 +105,27 @@
|
|||||||
Text="{x:Bind Unit, Mode=OneWay, Converter={StaticResource ImageResizerUnitToStringConverter}, ConverterParameter=ToLower}" />
|
Text="{x:Bind Unit, Mode=OneWay, Converter={StaticResource ImageResizerUnitToStringConverter}, ConverterParameter=ToLower}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</tkcontrols:SettingsCard.Description>
|
</tkcontrols:SettingsCard.Description>
|
||||||
<StackPanel
|
<Button
|
||||||
Grid.Column="2"
|
x:Uid="More_Options_Button"
|
||||||
|
Width="40"
|
||||||
|
Height="36"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
Orientation="Horizontal"
|
Content=""
|
||||||
Spacing="8">
|
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||||
<Button
|
Style="{StaticResource SubtleButtonStyle}">
|
||||||
x:Uid="ImageResizer_EditButton"
|
<Button.Flyout>
|
||||||
Width="40"
|
<MenuFlyout>
|
||||||
Height="36"
|
<MenuFlyoutItem
|
||||||
AutomationProperties.FullDescription="{x:Bind AccessibleTextHelper, Mode=OneWay, Converter={StaticResource ImageResizerSizeToAccessibleTextConverter}, ConverterParameter='Edit'}"
|
x:Uid="RemoveItem"
|
||||||
AutomationProperties.Name="{x:Bind Name, Mode=OneWay, Converter={StaticResource ImageResizerSizeToAccessibleTextConverter}, ConverterParameter='Edit'}"
|
Click="DeleteCustomSize"
|
||||||
Content=""
|
CommandParameter="{x:Bind}"
|
||||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
Icon="{ui:FontIcon Glyph=}" />
|
||||||
Style="{StaticResource SubtleButtonStyle}">
|
</MenuFlyout>
|
||||||
<ToolTipService.ToolTip>
|
</Button.Flyout>
|
||||||
<TextBlock x:Uid="EditTooltip" />
|
<ToolTipService.ToolTip>
|
||||||
</ToolTipService.ToolTip>
|
<TextBlock x:Uid="More_Options_ButtonTooltip" />
|
||||||
<Button.Flyout>
|
</ToolTipService.ToolTip>
|
||||||
<Flyout x:Uid="ImageResizer_EditSize" ShouldConstrainToRootBounds="False">
|
</Button>
|
||||||
<StackPanel Spacing="16">
|
|
||||||
<TextBox
|
|
||||||
x:Uid="ImageResizer_Name"
|
|
||||||
Width="240"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
Text="{x:Bind Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
|
|
||||||
<ComboBox
|
|
||||||
x:Uid="ImageResizer_Fit"
|
|
||||||
Width="240"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
SelectedIndex="{x:Bind Fit, Mode=TwoWay, Converter={StaticResource ImageResizerFitToIntConverter}}">
|
|
||||||
<ComboBoxItem x:Uid="ImageResizer_Sizes_Fit_Fill" />
|
|
||||||
<ComboBoxItem x:Uid="ImageResizer_Sizes_Fit_Fit" />
|
|
||||||
<ComboBoxItem x:Uid="ImageResizer_Sizes_Fit_Stretch" />
|
|
||||||
</ComboBox>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
|
||||||
<controls:ImageResizerDimensionsNumberBox
|
|
||||||
x:Uid="ImageResizer_Width"
|
|
||||||
Width="116"
|
|
||||||
Minimum="0"
|
|
||||||
SpinButtonPlacementMode="Compact"
|
|
||||||
Value="{x:Bind Width, Mode=TwoWay, Converter={StaticResource ImageResizerNumberBoxValueConverter}}" />
|
|
||||||
|
|
||||||
<controls:ImageResizerDimensionsNumberBox
|
|
||||||
x:Uid="ImageResizer_Height"
|
|
||||||
Width="116"
|
|
||||||
Minimum="0"
|
|
||||||
SpinButtonPlacementMode="Compact"
|
|
||||||
Visibility="{x:Bind IsHeightUsed, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"
|
|
||||||
Value="{x:Bind Height, Mode=TwoWay, Converter={StaticResource ImageResizerNumberBoxValueConverter}}" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<ComboBox
|
|
||||||
x:Uid="ImageResizer_Size"
|
|
||||||
Width="240"
|
|
||||||
SelectedIndex="{Binding Unit, Mode=TwoWay, Converter={StaticResource ImageResizerUnitToIntConverter}}">
|
|
||||||
<ComboBoxItem x:Uid="ImageResizer_Sizes_Units_CM" />
|
|
||||||
<ComboBoxItem x:Uid="ImageResizer_Sizes_Units_Inches" />
|
|
||||||
<ComboBoxItem x:Uid="ImageResizer_Sizes_Units_Percent" />
|
|
||||||
<ComboBoxItem x:Uid="ImageResizer_Sizes_Units_Pixels" />
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
</Flyout>
|
|
||||||
</Button.Flyout>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
x:Uid="ImageResizer_RemoveButton"
|
|
||||||
Width="40"
|
|
||||||
Height="36"
|
|
||||||
AutomationProperties.FullDescription="{x:Bind AccessibleTextHelper, Mode=OneWay, Converter={StaticResource ImageResizerSizeToAccessibleTextConverter}, ConverterParameter='Remove'}"
|
|
||||||
AutomationProperties.Name="{x:Bind Name, Mode=OneWay, Converter={StaticResource ImageResizerSizeToAccessibleTextConverter}, ConverterParameter='Remove'}"
|
|
||||||
Click="DeleteCustomSize"
|
|
||||||
CommandParameter="{Binding Id}"
|
|
||||||
Content=""
|
|
||||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
|
||||||
Style="{StaticResource SubtleButtonStyle}">
|
|
||||||
<ToolTipService.ToolTip>
|
|
||||||
<TextBlock x:Uid="RemoveTooltip" />
|
|
||||||
</ToolTipService.ToolTip>
|
|
||||||
</Button>
|
|
||||||
</StackPanel>
|
|
||||||
</tkcontrols:SettingsCard>
|
</tkcontrols:SettingsCard>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListView.ItemTemplate>
|
</ListView.ItemTemplate>
|
||||||
@@ -283,6 +228,60 @@
|
|||||||
</ComboBox>
|
</ComboBox>
|
||||||
</tkcontrols:SettingsCard>
|
</tkcontrols:SettingsCard>
|
||||||
</controls:SettingsGroup>
|
</controls:SettingsGroup>
|
||||||
|
|
||||||
|
<ContentDialog
|
||||||
|
x:Name="EditSizeDialog"
|
||||||
|
x:Uid="ImageResizer_EditSizeDialog"
|
||||||
|
PrimaryButtonStyle="{ThemeResource AccentButtonStyle}">
|
||||||
|
<StackPanel Width="400" Spacing="16">
|
||||||
|
<TextBox
|
||||||
|
x:Uid="ImageResizer_Name"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Text="{x:Bind EditingSize.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
|
||||||
|
<ComboBox
|
||||||
|
x:Uid="ImageResizer_Fit"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
SelectedIndex="{x:Bind EditingSize.Fit, Mode=TwoWay, Converter={StaticResource ImageResizerFitToIntConverter}}">
|
||||||
|
<ComboBoxItem x:Uid="ImageResizer_Sizes_Fit_Fill" />
|
||||||
|
<ComboBoxItem x:Uid="ImageResizer_Sizes_Fit_Fit" />
|
||||||
|
<ComboBoxItem x:Uid="ImageResizer_Sizes_Fit_Stretch" />
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<Grid ColumnSpacing="8">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<controls:ImageResizerDimensionsNumberBox
|
||||||
|
Grid.Column="0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Header="{x:Bind GetDimensionHeader(EditingSize.IsHeightUsed), Mode=OneWay}"
|
||||||
|
Minimum="0"
|
||||||
|
SpinButtonPlacementMode="Compact"
|
||||||
|
Value="{x:Bind EditingSize.Width, Mode=TwoWay, Converter={StaticResource ImageResizerNumberBoxValueConverter}}" />
|
||||||
|
|
||||||
|
<controls:ImageResizerDimensionsNumberBox
|
||||||
|
x:Uid="ImageResizer_Height"
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Minimum="0"
|
||||||
|
SpinButtonPlacementMode="Compact"
|
||||||
|
Visibility="{x:Bind EditingSize.IsHeightUsed, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"
|
||||||
|
Value="{x:Bind EditingSize.Height, Mode=TwoWay, Converter={StaticResource ImageResizerNumberBoxValueConverter}}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<ComboBox
|
||||||
|
x:Uid="ImageResizer_Size"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
SelectedIndex="{x:Bind EditingSize.Unit, Mode=TwoWay, Converter={StaticResource ImageResizerUnitToIntConverter}}">
|
||||||
|
<ComboBoxItem x:Uid="ImageResizer_Sizes_Units_CM" />
|
||||||
|
<ComboBoxItem x:Uid="ImageResizer_Sizes_Units_Inches" />
|
||||||
|
<ComboBoxItem x:Uid="ImageResizer_Sizes_Units_Percent" />
|
||||||
|
<ComboBoxItem x:Uid="ImageResizer_Sizes_Units_Pixels" />
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
</ContentDialog>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
</controls:SettingsPageControl.ModuleContent>
|
</controls:SettingsPageControl.ModuleContent>
|
||||||
|
|||||||
@@ -3,26 +3,58 @@
|
|||||||
// 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.Globalization;
|
using System.ComponentModel;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
|
using CommunityToolkit.WinUI.Controls;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||||
using Microsoft.PowerToys.Settings.UI.Library;
|
using Microsoft.PowerToys.Settings.UI.Library;
|
||||||
using Microsoft.PowerToys.Settings.UI.ViewModels;
|
using Microsoft.PowerToys.Settings.UI.ViewModels;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.Windows.ApplicationModel.Resources;
|
||||||
|
|
||||||
namespace Microsoft.PowerToys.Settings.UI.Views
|
namespace Microsoft.PowerToys.Settings.UI.Views
|
||||||
{
|
{
|
||||||
public sealed partial class ImageResizerPage : NavigablePage, IRefreshablePage
|
public sealed partial class ImageResizerPage : NavigablePage, IRefreshablePage, INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
|
||||||
public ImageResizerViewModel ViewModel { get; set; }
|
public ImageResizerViewModel ViewModel { get; set; }
|
||||||
|
|
||||||
|
public ICommand AddCommand => new RelayCommand(Add);
|
||||||
|
|
||||||
|
public ICommand UpdateCommand => new RelayCommand(Update);
|
||||||
|
|
||||||
|
private readonly ResourceLoader resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
||||||
|
|
||||||
|
// Working copy shown in the edit dialog, bound via x:Bind. Edits happen on this copy so a
|
||||||
|
// cancel simply discards it; for edits it is a clone of the original, for adds a new model.
|
||||||
|
private ImageSize _editingSize = new ImageSize();
|
||||||
|
|
||||||
|
// The original preset being edited, or null when adding a new one.
|
||||||
|
private ImageSize _editOriginal;
|
||||||
|
|
||||||
|
public ImageSize EditingSize
|
||||||
|
{
|
||||||
|
get => _editingSize;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_editingSize = value;
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(EditingSize)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header for the dimensions field. When Height isn't used (e.g. percentage scaling that keeps
|
||||||
|
// the aspect ratio) the single value scales the whole image, so "Width" would be misleading.
|
||||||
|
public string GetDimensionHeader(bool isHeightUsed) =>
|
||||||
|
resourceLoader.GetString(isHeightUsed ? "ImageResizer_Dimensions_Width" : "ImageResizer_Dimensions_Size");
|
||||||
|
|
||||||
public ImageResizerPage()
|
public ImageResizerPage()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
var settingsUtils = SettingsUtils.Default;
|
var settingsUtils = SettingsUtils.Default;
|
||||||
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
|
||||||
Func<string, string> loader = resourceLoader.GetString;
|
Func<string, string> loader = resourceLoader.GetString;
|
||||||
|
|
||||||
ViewModel = new ImageResizerViewModel(settingsUtils, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage, loader);
|
ViewModel = new ImageResizerViewModel(settingsUtils, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage, loader);
|
||||||
@@ -31,49 +63,81 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
|||||||
|
|
||||||
public async void DeleteCustomSize(object sender, RoutedEventArgs e)
|
public async void DeleteCustomSize(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
Button deleteRowButton = (Button)sender;
|
if (sender is not MenuFlyoutItem menuItem || menuItem.CommandParameter is not ImageSize size)
|
||||||
|
|
||||||
if (deleteRowButton != null)
|
|
||||||
{
|
{
|
||||||
ImageSize x = (ImageSize)deleteRowButton.DataContext;
|
return;
|
||||||
var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
|
|
||||||
|
|
||||||
ContentDialog dialog = new ContentDialog();
|
|
||||||
dialog.XamlRoot = RootPage.XamlRoot;
|
|
||||||
dialog.Title = x.Name;
|
|
||||||
dialog.PrimaryButtonText = resourceLoader.GetString("Yes");
|
|
||||||
dialog.CloseButtonText = resourceLoader.GetString("No");
|
|
||||||
dialog.DefaultButton = ContentDialogButton.Primary;
|
|
||||||
dialog.Content = new TextBlock() { Text = resourceLoader.GetString("Delete_Dialog_Description") };
|
|
||||||
dialog.PrimaryButtonClick += (s, args) =>
|
|
||||||
{
|
|
||||||
// Using InvariantCulture since this is internal and expected to be numerical
|
|
||||||
bool success = int.TryParse(deleteRowButton?.CommandParameter?.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int rowNum);
|
|
||||||
if (success)
|
|
||||||
{
|
|
||||||
ViewModel.DeleteImageSize(rowNum);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Logger.LogError("Failed to delete custom image size.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var result = await dialog.ShowAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ContentDialog dialog = new ContentDialog();
|
||||||
|
dialog.XamlRoot = RootPage.XamlRoot;
|
||||||
|
dialog.Title = size.Name;
|
||||||
|
dialog.PrimaryButtonText = resourceLoader.GetString("Yes");
|
||||||
|
dialog.CloseButtonText = resourceLoader.GetString("No");
|
||||||
|
dialog.DefaultButton = ContentDialogButton.Primary;
|
||||||
|
dialog.Content = new TextBlock() { Text = resourceLoader.GetString("Delete_Dialog_Description") };
|
||||||
|
dialog.PrimaryButtonClick += (s, args) =>
|
||||||
|
{
|
||||||
|
ViewModel.DeleteImageSize(size.Id);
|
||||||
|
};
|
||||||
|
await dialog.ShowAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddSizeButton_Click(object sender, RoutedEventArgs e)
|
private async void AddSizeButton_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ViewModel.AddImageSize();
|
_editOriginal = null;
|
||||||
|
EditingSize = ViewModel.CreateNewImageSizeModel();
|
||||||
|
EditSizeDialog.Title = resourceLoader.GetString("ImageResizer_EditSizeDialog_AddTitle");
|
||||||
|
EditSizeDialog.PrimaryButtonText = resourceLoader.GetString("ImageResizer_EditSizeDialog_Save");
|
||||||
|
EditSizeDialog.PrimaryButtonCommand = AddCommand;
|
||||||
|
await EditSizeDialog.ShowAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError("Exception encountered when adding a new image size.", ex);
|
Logger.LogError("Exception encountered when opening the add image size dialog.", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void EditSize_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not SettingsCard card || card.DataContext is not ImageSize original)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Edit a working copy so changes can be discarded on cancel without touching the original.
|
||||||
|
_editOriginal = original;
|
||||||
|
EditingSize = original.Clone();
|
||||||
|
EditSizeDialog.Title = resourceLoader.GetString("ImageResizer_EditSizeDialog_EditTitle");
|
||||||
|
EditSizeDialog.PrimaryButtonText = resourceLoader.GetString("ImageResizer_EditSizeDialog_Update");
|
||||||
|
EditSizeDialog.PrimaryButtonCommand = UpdateCommand;
|
||||||
|
await EditSizeDialog.ShowAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError("Exception encountered when opening the edit image size dialog.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Add()
|
||||||
|
{
|
||||||
|
ViewModel.AddImageSize(EditingSize);
|
||||||
|
EditSizeDialog.Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (_editOriginal != null)
|
||||||
|
{
|
||||||
|
ViewModel.UpdateImageSize(_editOriginal, EditingSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditSizeDialog.Hide();
|
||||||
|
}
|
||||||
|
|
||||||
private void ImagesSizesListView_ContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
|
private void ImagesSizesListView_ContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
|
||||||
{
|
{
|
||||||
if (ViewModel.IsListViewFocusRequested)
|
if (ViewModel.IsListViewFocusRequested)
|
||||||
|
|||||||
@@ -1218,9 +1218,6 @@ opera.exe</value>
|
|||||||
<data name="ImageResizer_Fit.Header" xml:space="preserve">
|
<data name="ImageResizer_Fit.Header" xml:space="preserve">
|
||||||
<value>Fit</value>
|
<value>Fit</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ImageResizer_Width.Header" xml:space="preserve">
|
|
||||||
<value>Width</value>
|
|
||||||
</data>
|
|
||||||
<data name="ImageResizer_Height.Header" xml:space="preserve">
|
<data name="ImageResizer_Height.Header" xml:space="preserve">
|
||||||
<value>Height</value>
|
<value>Height</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1233,6 +1230,29 @@ opera.exe</value>
|
|||||||
<data name="ImageResizer_AddSizeButton.Content" xml:space="preserve">
|
<data name="ImageResizer_AddSizeButton.Content" xml:space="preserve">
|
||||||
<value>Add new size</value>
|
<value>Add new size</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ImageResizer_EditSizeDialog.CloseButtonText" xml:space="preserve">
|
||||||
|
<value>Cancel</value>
|
||||||
|
</data>
|
||||||
|
<data name="ImageResizer_EditSizeDialog_AddTitle" xml:space="preserve">
|
||||||
|
<value>Add new size</value>
|
||||||
|
</data>
|
||||||
|
<data name="ImageResizer_EditSizeDialog_EditTitle" xml:space="preserve">
|
||||||
|
<value>Edit size</value>
|
||||||
|
</data>
|
||||||
|
<data name="ImageResizer_EditSizeDialog_Save" xml:space="preserve">
|
||||||
|
<value>Save</value>
|
||||||
|
</data>
|
||||||
|
<data name="ImageResizer_EditSizeDialog_Update" xml:space="preserve">
|
||||||
|
<value>Update</value>
|
||||||
|
</data>
|
||||||
|
<data name="ImageResizer_Dimensions_Width" xml:space="preserve">
|
||||||
|
<value>Width</value>
|
||||||
|
<comment>Header for the width input field in the Image Resizer edit size dialog</comment>
|
||||||
|
</data>
|
||||||
|
<data name="ImageResizer_Dimensions_Size" xml:space="preserve">
|
||||||
|
<value>Size</value>
|
||||||
|
<comment>Header shown instead of "Width" when a single value scales the whole image (e.g. percentage), in the Image Resizer edit size dialog</comment>
|
||||||
|
</data>
|
||||||
<data name="ImageResizer_Encoding.Header" xml:space="preserve">
|
<data name="ImageResizer_Encoding.Header" xml:space="preserve">
|
||||||
<value>JPEG quality level (%)</value>
|
<value>JPEG quality level (%)</value>
|
||||||
<comment>{Locked="JPEG"}</comment>
|
<comment>{Locked="JPEG"}</comment>
|
||||||
@@ -2379,29 +2399,16 @@ 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="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">
|
<data name="ImageResizer_Presets.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||||
<value>ImageResizer presets</value>
|
<value>ImageResizer presets</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ImageResizer_AddSizeButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
<data name="ImageResizer_AddSizeButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||||
<value>Add a new preset</value>
|
<value>Add a new preset</value>
|
||||||
</data>
|
</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">
|
<data name="ImageResizer_EditButton_Accessibility_Name" xml:space="preserve">
|
||||||
<value>Edit the {0} preset</value>
|
<value>Edit the {0} preset</value>
|
||||||
<comment>Expands to the AutomationProperties.Name value for the Edit button. Example: "Edit the Small preset".</comment>
|
<comment>Expands to the AutomationProperties.Name value for the Edit button. Example: "Edit the Small preset".</comment>
|
||||||
</data>
|
</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>
|
||||||
@@ -2642,9 +2649,6 @@ From there, simply click on one of the supported files in the File Explorer and
|
|||||||
<data name="EditTooltip.Text" xml:space="preserve">
|
<data name="EditTooltip.Text" xml:space="preserve">
|
||||||
<value>Edit</value>
|
<value>Edit</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="RemoveTooltip.Text" xml:space="preserve">
|
|
||||||
<value>Remove</value>
|
|
||||||
</data>
|
|
||||||
<data name="Activation_Shortcut_Cancel" xml:space="preserve">
|
<data name="Activation_Shortcut_Cancel" xml:space="preserve">
|
||||||
<value>Cancel</value>
|
<value>Cancel</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -318,19 +318,41 @@ public partial class ImageResizerViewModel : Observable
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void AddImageSize(string namePrefix = "")
|
public void AddImageSize(string namePrefix = "")
|
||||||
|
{
|
||||||
|
AddImageSize(CreateNewImageSizeModel(namePrefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new preset populated with default values and a generated unique name, without
|
||||||
|
/// adding it to the <see cref="Sizes"/> collection. Used as the working copy for the add dialog
|
||||||
|
/// (so nothing is committed until the user confirms) and as the source for <see cref="AddImageSize(string)"/>.
|
||||||
|
/// </summary>
|
||||||
|
public ImageSize CreateNewImageSizeModel(string namePrefix = "")
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(namePrefix))
|
if (string.IsNullOrEmpty(namePrefix))
|
||||||
{
|
{
|
||||||
namePrefix = DefaultPresetNamePrefix;
|
namePrefix = DefaultPresetNamePrefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
Sizes.Add(new ImageSize(
|
return new ImageSize(
|
||||||
_nextId,
|
_nextId,
|
||||||
GenerateNameForNewSize(namePrefix),
|
GenerateNameForNewSize(namePrefix),
|
||||||
_customSize.Fit,
|
_customSize.Fit,
|
||||||
_customSize.Width,
|
_customSize.Width,
|
||||||
_customSize.Height,
|
_customSize.Height,
|
||||||
_customSize.Unit));
|
_customSize.Unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Commits a preset created via <see cref="CreateNewImageSizeModel"/> to the <see cref="Sizes"/>
|
||||||
|
/// collection, assigning it the next available unique ID.
|
||||||
|
/// </summary>
|
||||||
|
public void AddImageSize(ImageSize size)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(size);
|
||||||
|
|
||||||
|
size.Id = _nextId;
|
||||||
|
Sizes.Add(size);
|
||||||
|
|
||||||
_nextId++;
|
_nextId++;
|
||||||
|
|
||||||
@@ -338,6 +360,29 @@ public partial class ImageResizerViewModel : Observable
|
|||||||
IsListViewFocusRequested = true;
|
IsListViewFocusRequested = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies the values from an edited working copy back onto the original preset, saving once.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateImageSize(ImageSize original, ImageSize updated)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(original);
|
||||||
|
ArgumentNullException.ThrowIfNull(updated);
|
||||||
|
|
||||||
|
// Temporarily detach the per-item save handler so the individual property updates below
|
||||||
|
// don't each trigger a save; persist once at the end instead.
|
||||||
|
original.PropertyChanged -= SizePropertyChanged;
|
||||||
|
|
||||||
|
original.Name = updated.Name;
|
||||||
|
original.Fit = updated.Fit;
|
||||||
|
original.Width = updated.Width;
|
||||||
|
original.Height = updated.Height;
|
||||||
|
original.Unit = updated.Unit;
|
||||||
|
|
||||||
|
original.PropertyChanged += SizePropertyChanged;
|
||||||
|
|
||||||
|
SaveImageSizes();
|
||||||
|
}
|
||||||
|
|
||||||
public void DeleteImageSize(int id)
|
public void DeleteImageSize(int id)
|
||||||
{
|
{
|
||||||
ImageSize size = _sizes.First(x => x.Id == id);
|
ImageSize size = _sizes.First(x => x.Id == id);
|
||||||
|
|||||||
Reference in New Issue
Block a user