Compare commits

...

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
42b067b056 Address code review feedback in test code
- Changed magic number to const InitialTemplateCount
- Fixed potential index out of bounds by using All() with proper null check
- Added System.Linq using directive

Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
2025-10-25 07:02:22 +00:00
copilot-swe-agent[bot]
29096b9650 Add UI tests for template deletion functionality
- Added DeleteTemplateLayout test to verify template deletion works
- Added CannotDeleteBlankTemplate test to ensure Blank template cannot be deleted
- Tests verify both UI behavior and file persistence

Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
2025-10-25 06:57:15 +00:00
copilot-swe-agent[bot]
69a554cb79 Add template deletion functionality to FancyZones Editor
- Created LayoutTypeDeletableToVisibilityConverter to show delete button for templates (except Blank)
- Updated MainWindow.xaml to use new converter for delete buttons and menu items
- Modified LayoutModel.Delete() to handle both custom and template layout deletion
- Changed TemplateModels from IList to ObservableCollection for UI updates
- Updated all index-based TemplateModels access to use LINQ queries
- Enhanced DefaultLayoutsModel.Reset() to handle missing templates gracefully
- Added fallback logic when templates are deleted

Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
2025-10-25 06:56:07 +00:00
copilot-swe-agent[bot]
5723f6b998 Initial plan 2025-10-25 06:43:40 +00:00
7 changed files with 144 additions and 42 deletions

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Linq;
using FancyZonesEditor.Models;
using FancyZonesEditorCommon.Data;
using Microsoft.FancyZonesEditor.UnitTests.Utils;
@@ -363,5 +364,42 @@ namespace Microsoft.FancyZonesEditor.UITests
Assert.AreEqual(0, layoutHotkeyCount);
}
[TestMethod("FancyZonesEditor.Basic.DeleteTemplateLayout")]
[TestCategory("FancyZones Editor #5")]
public void DeleteTemplateLayout()
{
var deletedLayoutName = TestConstants.TemplateLayoutNames[LayoutType.Focus];
const int InitialTemplateCount = 6; // Blank, Focus, Columns, Rows, Grid, PriorityGrid
// Delete Focus template layout via context menu
FancyZonesEditorHelper.ClickContextMenuItem(Session, deletedLayoutName, FancyZonesEditorHelper.ElementName.Delete);
Session.SendKeySequence(Key.Tab, Key.Enter);
// verify the template layout is removed from UI
Assert.IsTrue(Session.FindAll<Element>(deletedLayoutName).Count == 0);
// check the template layouts file
var layoutTemplates = new LayoutTemplates();
var data = layoutTemplates.Read(layoutTemplates.File);
Assert.AreEqual(InitialTemplateCount - 1, data.LayoutTemplates.Count);
Assert.IsFalse(data.LayoutTemplates.Exists(x => x.Type == LayoutType.Focus.TypeToString()));
}
[TestMethod("FancyZonesEditor.Basic.CannotDeleteBlankTemplate")]
[TestCategory("FancyZones Editor #5")]
public void CannotDeleteBlankTemplate()
{
var blankLayoutName = TestConstants.TemplateLayoutNames[LayoutType.Blank];
// Try to open edit dialog on Blank template - should not have delete option
Session.Find<Element>(blankLayoutName).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
// Verify delete button is not visible for Blank layout
var deleteButtons = Session.FindAll<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.DeleteLayoutButton));
Assert.IsTrue(deleteButtons.Count == 0 || deleteButtons.All(b => !b.Displayed));
Session.Find<Button>(ElementName.Cancel).Click();
}
}
}

View File

@@ -0,0 +1,28 @@
// 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.Globalization;
using System.Windows;
using System.Windows.Data;
using FancyZonesEditor.Models;
namespace FancyZonesEditor.Converters
{
public class LayoutTypeDeletableToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// Allow deletion for custom layouts and all template layouts except Blank
LayoutType type = (LayoutType)value;
return (type == LayoutType.Custom || (type != LayoutType.Blank && type != LayoutType.Custom)) ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
}

View File

@@ -33,6 +33,7 @@
<Converters:LayoutTypeCustomToVisibilityConverter x:Key="LayoutTypeCustomToVisibilityConverter" />
<Converters:LayoutTypeTemplateToVisibilityConverter x:Key="LayoutTypeTemplateToVisibilityConverter" />
<Converters:LayoutModelTypeBlankToVisibilityConverter x:Key="LayoutModelTypeBlankToVisibilityConverter" />
<Converters:LayoutTypeDeletableToVisibilityConverter x:Key="LayoutTypeDeletableToVisibilityConverter" />
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<ContextMenu x:Key="LayoutContextMenu" Visibility="{Binding Path=Type, Converter={StaticResource LayoutModelTypeBlankToVisibilityConverter}}">
@@ -68,11 +69,11 @@
<ui:FontIcon Glyph="&#xE8C8;" />
</MenuItem.Icon>
</MenuItem>
<Separator Visibility="{Binding Path=Type, Converter={StaticResource LayoutTypeCustomToVisibilityConverter}}" />
<Separator Visibility="{Binding Path=Type, Converter={StaticResource LayoutTypeDeletableToVisibilityConverter}}" />
<MenuItem
Click="DeleteLayout_Click"
Header="{x:Static props:Resources.Delete}"
Visibility="{Binding Path=Type, Converter={StaticResource LayoutTypeCustomToVisibilityConverter}}">
Visibility="{Binding Path=Type, Converter={StaticResource LayoutTypeDeletableToVisibilityConverter}}">
<MenuItem.Icon>
<ui:FontIcon Glyph="&#xE74D;" />
</MenuItem.Icon>
@@ -437,7 +438,7 @@
Click="DeleteLayout_Click"
Style="{StaticResource IconOnlyButtonStyle}"
ToolTip="{x:Static props:Resources.Delete}"
Visibility="{Binding Path=Type, Converter={StaticResource LayoutTypeCustomToVisibilityConverter}}">
Visibility="{Binding Path=Type, Converter={StaticResource LayoutTypeDeletableToVisibilityConverter}}">
<Button.Content>
<TextBlock
AutomationProperties.Name="{x:Static props:Resources.Delete}"

View File

@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
namespace FancyZonesEditor.Models
@@ -23,27 +24,39 @@ namespace FancyZonesEditor.Models
public void Reset(MonitorConfigurationType type)
{
LayoutModel defaultLayout = null;
switch (type)
{
case MonitorConfigurationType.Horizontal:
Set(MainWindowSettingsModel.TemplateModels[(int)LayoutType.PriorityGrid], type);
// Try to get PriorityGrid, fallback to first available template or Blank
defaultLayout = MainWindowSettingsModel.TemplateModels.FirstOrDefault(m => m.Type == LayoutType.PriorityGrid)
?? MainWindowSettingsModel.TemplateModels.FirstOrDefault(m => m.Type != LayoutType.Blank)
?? MainWindowSettingsModel.TemplateModels.FirstOrDefault(m => m.Type == LayoutType.Blank);
break;
case MonitorConfigurationType.Vertical:
Set(MainWindowSettingsModel.TemplateModels[(int)LayoutType.Rows], type);
// Try to get Rows, fallback to first available template or Blank
defaultLayout = MainWindowSettingsModel.TemplateModels.FirstOrDefault(m => m.Type == LayoutType.Rows)
?? MainWindowSettingsModel.TemplateModels.FirstOrDefault(m => m.Type != LayoutType.Blank)
?? MainWindowSettingsModel.TemplateModels.FirstOrDefault(m => m.Type == LayoutType.Blank);
break;
}
if (defaultLayout != null)
{
Set(defaultLayout, type);
}
}
public void Reset(string uuid)
{
if (Layouts[MonitorConfigurationType.Horizontal].Uuid == uuid)
{
Set(MainWindowSettingsModel.TemplateModels[(int)LayoutType.PriorityGrid], MonitorConfigurationType.Horizontal);
Reset(MonitorConfigurationType.Horizontal);
}
if (Layouts[MonitorConfigurationType.Vertical].Uuid == uuid)
{
Set(MainWindowSettingsModel.TemplateModels[(int)LayoutType.Rows], MonitorConfigurationType.Vertical);
Reset(MonitorConfigurationType.Vertical);
}
}

View File

@@ -335,7 +335,7 @@ namespace FancyZonesEditor.Models
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
// Removes this Layout from the registry and the loaded CustomModels list
// Removes this Layout from the registry and the loaded CustomModels list or TemplateModels list
public void Delete()
{
var customModels = MainWindowSettingsModel.CustomModels;
@@ -353,6 +353,16 @@ namespace FancyZonesEditor.Models
{
customModels.RemoveAt(i);
}
else
{
// Try to remove from template models if it's a template layout
var templateModels = MainWindowSettingsModel.TemplateModels;
i = templateModels.IndexOf(this);
if (i != -1)
{
templateModels.RemoveAt(i);
}
}
}
public void RestoreTo(LayoutModel layout)

View File

@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using FancyZonesEditor.Models;
@@ -51,11 +52,11 @@ namespace FancyZonesEditor
TemplateZoneCount = 0,
SensitivityRadius = 0,
};
TemplateModels.Insert((int)LayoutType.Blank, blankModel);
TemplateModels.Add(blankModel);
var focusModel = new CanvasLayoutModel(Properties.Resources.Template_Layout_Focus, LayoutType.Focus);
focusModel.InitTemplateZones();
TemplateModels.Insert((int)LayoutType.Focus, focusModel);
TemplateModels.Add(focusModel);
var columnsModel = new GridLayoutModel(Properties.Resources.Template_Layout_Columns, LayoutType.Columns)
{
@@ -63,7 +64,7 @@ namespace FancyZonesEditor
RowPercents = new List<int>(1) { GridLayoutModel.GridMultiplier },
};
columnsModel.InitTemplateZones();
TemplateModels.Insert((int)LayoutType.Columns, columnsModel);
TemplateModels.Add(columnsModel);
var rowsModel = new GridLayoutModel(Properties.Resources.Template_Layout_Rows, LayoutType.Rows)
{
@@ -71,15 +72,15 @@ namespace FancyZonesEditor
ColumnPercents = new List<int>(1) { GridLayoutModel.GridMultiplier },
};
rowsModel.InitTemplateZones();
TemplateModels.Insert((int)LayoutType.Rows, rowsModel);
TemplateModels.Add(rowsModel);
var gridModel = new GridLayoutModel(Properties.Resources.Template_Layout_Grid, LayoutType.Grid);
gridModel.InitTemplateZones();
TemplateModels.Insert((int)LayoutType.Grid, gridModel);
TemplateModels.Add(gridModel);
var priorityGridModel = new GridLayoutModel(Properties.Resources.Template_Layout_Priority_Grid, LayoutType.PriorityGrid);
priorityGridModel.InitTemplateZones();
TemplateModels.Insert((int)LayoutType.PriorityGrid, priorityGridModel);
TemplateModels.Add(priorityGridModel);
// set default layouts
DefaultLayouts.Set(rowsModel, MonitorConfigurationType.Vertical);
@@ -130,11 +131,11 @@ namespace FancyZonesEditor
{
get
{
return TemplateModels[(int)LayoutType.Blank];
return TemplateModels.FirstOrDefault(m => m.Type == LayoutType.Blank);
}
}
public static IList<LayoutModel> TemplateModels { get; } = new List<LayoutModel>(6);
public static ObservableCollection<LayoutModel> TemplateModels { get; } = new ObservableCollection<LayoutModel>();
public static ObservableCollection<LayoutModel> CustomModels
{

View File

@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Windows;
@@ -648,15 +649,18 @@ namespace FancyZonesEditor.Utils
// replace deleted layout with the Blank layout
if (!existingLayout)
{
LayoutModel blankLayout = MainWindowSettingsModel.TemplateModels[(int)LayoutType.Blank];
settings.ZonesetUuid = blankLayout.Uuid;
settings.Type = blankLayout.Type;
settings.ZoneCount = blankLayout.TemplateZoneCount;
settings.SensitivityRadius = blankLayout.SensitivityRadius;
LayoutModel blankLayout = MainWindowSettingsModel.TemplateModels.FirstOrDefault(m => m.Type == LayoutType.Blank);
if (blankLayout != null)
{
settings.ZonesetUuid = blankLayout.Uuid;
settings.Type = blankLayout.Type;
settings.ZoneCount = blankLayout.TemplateZoneCount;
settings.SensitivityRadius = blankLayout.SensitivityRadius;
// grid layout settings, just resetting them
settings.ShowSpacing = false;
settings.Spacing = 0;
// grid layout settings, just resetting them
settings.ShowSpacing = false;
settings.Spacing = 0;
}
}
bool unused = true;
@@ -748,18 +752,21 @@ namespace FancyZonesEditor.Utils
foreach (var wrapper in templateLayouts)
{
LayoutType type = JsonTagToLayoutType(wrapper.Type);
LayoutModel layout = MainWindowSettingsModel.TemplateModels[(int)type];
LayoutModel layout = MainWindowSettingsModel.TemplateModels.FirstOrDefault(m => m.Type == type);
layout.SensitivityRadius = wrapper.SensitivityRadius;
layout.TemplateZoneCount = wrapper.ZoneCount;
if (layout is GridLayoutModel grid)
if (layout != null)
{
grid.ShowSpacing = wrapper.ShowSpacing;
grid.Spacing = wrapper.Spacing;
}
layout.SensitivityRadius = wrapper.SensitivityRadius;
layout.TemplateZoneCount = wrapper.ZoneCount;
layout.InitTemplateZones();
if (layout is GridLayoutModel grid)
{
grid.ShowSpacing = wrapper.ShowSpacing;
grid.Spacing = wrapper.Spacing;
}
layout.InitTemplateZones();
}
}
return true;
@@ -807,17 +814,21 @@ namespace FancyZonesEditor.Utils
else
{
LayoutType layoutType = JsonTagToLayoutType(layout.Layout.Type);
defaultLayoutModel = MainWindowSettingsModel.TemplateModels[(int)layoutType];
defaultLayoutModel.TemplateZoneCount = layout.Layout.ZoneCount;
defaultLayoutModel.SensitivityRadius = layout.Layout.SensitivityRadius;
if (defaultLayoutModel is GridLayoutModel gridDefaultLayoutModel)
defaultLayoutModel = MainWindowSettingsModel.TemplateModels.FirstOrDefault(m => m.Type == layoutType);
if (defaultLayoutModel != null)
{
gridDefaultLayoutModel.ShowSpacing = layout.Layout.ShowSpacing;
gridDefaultLayoutModel.Spacing = layout.Layout.Spacing;
}
defaultLayoutModel.TemplateZoneCount = layout.Layout.ZoneCount;
defaultLayoutModel.SensitivityRadius = layout.Layout.SensitivityRadius;
MainWindowSettingsModel.DefaultLayouts.Set(defaultLayoutModel, type);
if (defaultLayoutModel is GridLayoutModel gridDefaultLayoutModel)
{
gridDefaultLayoutModel.ShowSpacing = layout.Layout.ShowSpacing;
gridDefaultLayoutModel.Spacing = layout.Layout.Spacing;
}
MainWindowSettingsModel.DefaultLayouts.Set(defaultLayoutModel, type);
}
}
if (defaultLayoutModel != null)