Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ That paragraph is not decoration: the release workflow copies each entry verbati
the GitHub release body and the announcement discussion, so it is the first thing a
prospective user reads. CI fails a pull request whose newest entry is missing it.

## [1.130.34] - 2026-10-21

**The Delete key no longer takes a row off a list while nothing changes on the PC.**

### Fixed

- **Pressing Delete on Startup Manager, Uninstaller and six other lists removed the selected row and did nothing
else.** Windows' list control takes the selected row off its list on Delete unless it is told not to, and the
eight lists that can be ticked or switched never told it: Startup Manager, Uninstaller, Right-click Menu,
Environment Variables, Preinstalled Apps, App Updates, Browser Cleaner and Windows Update. The startup program
still started with Windows and the app stayed installed. On Environment Variables, Apply then deleted nothing,
and on Preinstalled Apps the hidden row stayed ticked, so Remove selected could remove an app no longer on
screen. Delete now does nothing in every list, and a row goes only through its own button and confirmation.
Since each list was added, the oldest in 0.3.0.

## [1.130.33] - 2026-10-21

**CSV exports open in Excel in your own region, with accented names intact.**
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@ or uninstalls is reachable from a bare keypress. On Drivers, Windows Update and
drivers, List updates and Analyze. Pressing it during a refresh that is already running does nothing
rather than starting a second one.

**`Delete` in a list does nothing.** A startup program, an app, a variable or a menu entry is removed only
through its own button and that button's confirmation, so a stray key press never takes a row off a list
while it stays on the PC.

**Focus stays where you were when a list refreshes.** A refresh rebuilds a list's rows, and the button or cell
you were on used to go with them, leaving focus at the top of the window. Now focus comes back to the same
control in the same row: after F5 on Services, after putting a change back on Undo Changes, and on every table
Expand Down
76 changes: 76 additions & 0 deletions SysManager/SysManager.IntegrationTests/StartupViewUiTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SysManager · StartupViewUiTests
// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager
// License: MIT

using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Controls;
using System.Windows.Media;
using SysManager.Models;
using SysManager.Views;

namespace SysManager.IntegrationTests;

/// <summary>
/// The Delete key on Startup Manager's list leaves the list alone (#2809).
/// </summary>
/// <remarks>
/// WPF's <c>DataGrid</c> takes the selected row off its collection on Delete unless it is told not to, and the row
/// vanished while the program still started with Windows. The guard reads the attribute on every editable grid; this
/// presses the key on the real XAML, so the attribute is seen to do what the guard trusts it to.
/// <para>The view is given only the list it shows, not a <c>StartupViewModel</c>: the view model's scan hands its
/// result to the application's dispatcher once a test has made one, and nothing pumps that dispatcher on
/// <see cref="StaHelper"/>'s thread, so waiting for the scan would wait forever. The grid is what is under test, and
/// it binds the list by name.</para>
/// </remarks>
[Collection("Network")]
public sealed class StartupViewUiTests
{
/// <summary>What the view binds the grid to, and nothing else.</summary>
private sealed class OnlyTheList
{
public ObservableCollection<StartupEntry> Entries { get; } =
[
new() { Name = "Contoso Updater", Command = @"C:\Program Files\Contoso\update.exe", Publisher = "Contoso",
Source = StartupSource.RegistryCurrentUser },
new() { Name = "Fabrikam Sync", Command = @"C:\Program Files\Fabrikam\sync.exe", Publisher = "Fabrikam",
Source = StartupSource.RegistryCurrentUser },
];
}

[Fact]
public void TheDeleteKey_LeavesTheListAlone()
{
StaHelper.Run(() =>
{
AppResources.Ensure();
var shown = new OnlyTheList();
var view = new StartupView { DataContext = shown };
view.Measure(new Size(1400, 1000));
view.Arrange(new Rect(0, 0, 1400, 1000));
view.UpdateLayout();

var grid = Assert.Single(Descendants<DataGrid>(view),
g => AutomationProperties.GetName(g) == "Startup programs");
Assert.Equal(2, grid.Items.Count);
grid.SelectedIndex = 0;

Assert.False(DataGrid.DeleteCommand.CanExecute(null, grid));
DataGrid.DeleteCommand.Execute(null, grid);

Assert.Equal(["Contoso Updater", "Fabrikam Sync"], shown.Entries.Select(e => e.Name));
});
}

private static IEnumerable<T> Descendants<T>(DependencyObject root) where T : DependencyObject
{
var count = VisualTreeHelper.GetChildrenCount(root);
for (var i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(root, i);
if (child is T match) yield return match;
foreach (var deeper in Descendants<T>(child)) yield return deeper;
}
}
}
47 changes: 47 additions & 0 deletions SysManager/SysManager.Tests/ArchitectureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,53 @@ public void EveryCsvExport_IsWrittenThroughCsv_InTheUsersRegion()
[GeneratedRegex(@"(?<!\bstring\s+)\bToCsv\([^;]*;")]
private static partial Regex ToCsvCall();

/// <summary>
/// Every grid a user can edit turns off the Delete key, which would otherwise take the selected row off the list.
/// </summary>
/// <remarks>
/// WPF's <c>DataGrid</c> removes the selected rows from its collection on Delete whenever it is not read-only and
/// <c>CanUserDeleteRows</c> keeps its default of true. Eight grids that are editable for a tick or a switch never
/// turned it off, so the row vanished while nothing changed on the PC: the program still started with Windows,
/// Environment Variables' Apply deleted nothing, and Preinstalled Apps could remove an app no longer on screen
/// (#2809). A read-only grid needs nothing, because WPF refuses Delete on one.
/// </remarks>
[Fact]
public void EveryEditableGrid_TurnsOffTheDeleteKey()
{
var views = Path.Combine(TestPaths.AppProject(), "Views");
var offenders = new List<string>();
var grids = 0;
var editable = 0;

foreach (var file in Directory.EnumerateFiles(views, "*.xaml").Order(StringComparer.Ordinal))
{
var doc = XDocument.Parse(WithoutXamlComments(File.ReadAllText(file)));
foreach (var grid in doc.Descendants().Where(e => e.Name.LocalName == "DataGrid"))
{
grids++;
if ((string?)grid.Attribute("IsReadOnly") == "True") continue;
editable++;
if ((string?)grid.Attribute("CanUserDeleteRows") != "False")
{
var name = (string?)grid.Attributes()
.FirstOrDefault(a => a.Name.LocalName == "AutomationProperties.Name")?.Value ?? "a grid";
offenders.Add($"{Path.GetFileName(file)}: {name}");
}
}
}

// Vacuity floor: 35 grids when measured, 11 of them editable — App Blocker, App Updates, Browser Cleaner,
// Right-click Menu, Preinstalled Apps, DNS & Hosts, Environment Variables, Shortcut Cleaner, Startup Manager,
// Uninstaller and Windows Update.
Assert.True(grids >= 35 && editable >= 11,
$"{grids} grids and {editable} editable ones were found, out of 35 and 11 measured — the parse is out of "
+ "date, so a pass proves nothing.");

Assert.True(offenders.Count == 0,
"an editable grid leaves the Delete key on, so pressing it takes the selected row off the list while "
+ "nothing changes on the PC. Set CanUserDeleteRows=\"False\":\n " + string.Join("\n ", offenders));
}

/// <summary>
/// Every <c>StatusFooter</c> names its own progress bar, and no view re-inlines the block it replaced.
/// </summary>
Expand Down
6 changes: 3 additions & 3 deletions SysManager/SysManager/SysManager.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
<NoWarn>$(NoWarn);NU1603;NU1701</NoWarn>
<!-- The documentation file exists for the compiler's checks; it is not part of what ships. -->
<PublishDocumentationFile>false</PublishDocumentationFile>
<Version>1.130.33</Version>
<FileVersion>1.130.33.0</FileVersion>
<AssemblyVersion>1.130.33.0</AssemblyVersion>
<Version>1.130.34</Version>
<FileVersion>1.130.34.0</FileVersion>
<AssemblyVersion>1.130.34.0</AssemblyVersion>
<Product>SysManager</Product>
<Description>SysManager — Windows system monitoring toolkit by laurentiu021. Network, updates, health, logs, safe deep cleanup.</Description>
<PackageProjectUrl>https://github.com/laurentiu021/SystemManager</PackageProjectUrl>
Expand Down
2 changes: 1 addition & 1 deletion SysManager/SysManager/Views/AppUpdatesView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<Grid>
<DataGrid AutomationProperties.Name="Upgradable packages" ItemsSource="{Binding Packages}"
AutoGenerateColumns="False" HeadersVisibility="Column"
GridLinesVisibility="None" CanUserAddRows="False"
GridLinesVisibility="None" CanUserAddRows="False" CanUserDeleteRows="False"
CanUserSortColumns="True"
CanUserResizeColumns="True"
Background="Transparent" BorderThickness="0"
Expand Down
2 changes: 1 addition & 1 deletion SysManager/SysManager/Views/BrowserCleanerView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
<DataGrid ItemsSource="{Binding Items}"
AutomationProperties.Name="Browser data categories"
AutoGenerateColumns="False" HeadersVisibility="Column"
CanUserAddRows="False" SelectionMode="Single"
CanUserAddRows="False" CanUserDeleteRows="False" SelectionMode="Single"
Background="Transparent" BorderThickness="0"
GridLinesVisibility="None"
VirtualizingPanel.IsVirtualizing="True"
Expand Down
2 changes: 1 addition & 1 deletion SysManager/SysManager/Views/ContextMenuView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@
<DataGrid AutomationProperties.Name="Right-click menu entries"
ItemsSource="{Binding Entries}"
AutoGenerateColumns="False" HeadersVisibility="Column"
CanUserAddRows="False"
CanUserAddRows="False" CanUserDeleteRows="False"
Background="Transparent" BorderThickness="0"
GridLinesVisibility="None" IsReadOnly="False"
SelectionMode="Single" CanUserSortColumns="True"
Expand Down
2 changes: 1 addition & 1 deletion SysManager/SysManager/Views/DebloaterView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
<DataGrid ItemsSource="{Binding FilteredApps}"
AutomationProperties.Name="Installed Store apps"
AutoGenerateColumns="False" HeadersVisibility="Column"
CanUserAddRows="False" SelectionMode="Single"
CanUserAddRows="False" CanUserDeleteRows="False" SelectionMode="Single"
Background="Transparent" BorderThickness="0"
GridLinesVisibility="None"
VirtualizingPanel.IsVirtualizing="True"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
SelectedItem="{Binding SelectedVariable}"
AutomationProperties.Name="Environment variables"
AutoGenerateColumns="False" HeadersVisibility="Column"
CanUserAddRows="False" SelectionMode="Single"
CanUserAddRows="False" CanUserDeleteRows="False" SelectionMode="Single"
Background="Transparent" BorderThickness="0"
GridLinesVisibility="None"
VirtualizingPanel.IsVirtualizing="True"
Expand Down
2 changes: 1 addition & 1 deletion SysManager/SysManager/Views/StartupView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
<Grid>
<DataGrid AutomationProperties.Name="Startup programs" ItemsSource="{Binding Entries}"
AutoGenerateColumns="False" HeadersVisibility="Column"
CanUserAddRows="False"
CanUserAddRows="False" CanUserDeleteRows="False"
Background="Transparent" BorderThickness="0"
GridLinesVisibility="None" IsReadOnly="False"
SelectionMode="Single" CanUserSortColumns="True"
Expand Down
2 changes: 1 addition & 1 deletion SysManager/SysManager/Views/UninstallerView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@
AutoGenerateColumns="False" HeadersVisibility="Column"
Background="Transparent" BorderThickness="0"
GridLinesVisibility="None" IsReadOnly="False"
CanUserAddRows="False"
CanUserAddRows="False" CanUserDeleteRows="False"
SelectionMode="Single" CanUserSortColumns="True"
CanUserResizeColumns="True"
VirtualizingPanel.IsVirtualizing="True"
Expand Down
2 changes: 1 addition & 1 deletion SysManager/SysManager/Views/WindowsUpdateView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@
GridLinesVisibility="None"
SelectionMode="Single" CanUserSortColumns="True"
CanUserResizeColumns="True"
CanUserAddRows="False"
CanUserAddRows="False" CanUserDeleteRows="False"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
Visibility="{Binding UpdateCount, Converter={StaticResource GreaterThanZero}}">
Expand Down