Browse Source

Fix Shift+Up/Down not shrinking the tree selection

Extending a tree selection with Shift+Down then pressing Shift+Up didn't shrink the range back toward the anchor -- the dropped rows stayed selected, and in the middle of a list the anchor drifted so the range crept the wrong way. Root cause is upstream in ProDataGrid: a shift+nav key moves the grid's current row and anchor correctly, but SelectFromAnchorToCurrent calls SetRowsSelection(start,end), which only ADDS the range and never deselects rows outside it, so a shrink is a no-op. After the grid processes a shift+nav key, TreeKeyboardController prunes the selection to exactly the [anchor..current] range. It deselects out-of-range rows via the internal SetRowSelection(slot, isSelected:false, setAnchorSlot:false) rather than SelectedItems.Remove: removing an item makes the grid re-derive its current row and corrupt the anchor for the next key, whereas SetRowSelection deselects a slot in place and leaves current/anchor intact. The slot members are read reflectively and defensively (falls back to the pre-fix behaviour if a future ProDataGrid renames them). Both the assembly and analyzer trees get it via the shared controller.
pull/3755/head
Siegfried Pammer 1 month ago
parent
commit
9048a76e41
  1. 71
      ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs
  2. 69
      ILSpy/Controls/TreeKeyboardController.cs

71
ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs

@ -921,6 +921,77 @@ public class AssemblyTreeTests @@ -921,6 +921,77 @@ public class AssemblyTreeTests
await Waiters.WaitForAsync(() => assembly.IsExpanded, description: "Numpad * expands");
}
[AvaloniaTest]
public async Task Shift_Up_After_Shift_Down_Shrinks_The_Selection_Toward_The_Anchor()
{
// Anchor at A, Shift+Down twice -> A,B,C. Shift+Up must SHRINK back to A,B (not keep C).
var (window, vm) = await TestHarness.BootAsync(3);
var model = vm.AssemblyTreeModel;
var pane = await window.WaitForComponent<AssemblyListPane>();
var grid = await pane.WaitForComponent<DataGrid>();
grid.Focus();
Dispatcher.UIThread.RunJobs();
var hm = (global::Avalonia.Controls.DataGridHierarchical.IHierarchicalModel)grid.HierarchicalModel!;
var a = (SharpTreeNode)hm.Flattened[0].Item;
var b = (SharpTreeNode)hm.Flattened[1].Item;
var c = (SharpTreeNode)hm.Flattened[2].Item;
model.SelectNode(a);
await Waiters.WaitForAsync(() => ReferenceEquals(model.SelectedItem, a));
window.KeyPress(Key.Down, RawInputModifiers.Shift, PhysicalKey.ArrowDown, null);
Dispatcher.UIThread.RunJobs();
window.KeyPress(Key.Down, RawInputModifiers.Shift, PhysicalKey.ArrowDown, null);
Dispatcher.UIThread.RunJobs();
model.SelectedItems.Should().BeEquivalentTo(new[] { a, b, c }, "precondition: A,B,C selected");
window.KeyPress(Key.Up, RawInputModifiers.Shift, PhysicalKey.ArrowUp, null);
Dispatcher.UIThread.RunJobs();
model.SelectedItems.Should().BeEquivalentTo(new[] { a, b },
"Shift+Up must shrink the range back toward the anchor (A,B), dropping C");
}
[AvaloniaTest]
public async Task Shift_Down_Then_Shift_Up_In_The_Middle_Shrinks_Without_Drifting_The_Anchor()
{
// Start mid-list, Shift+Down x3 then Shift+Up x2. The anchor must stay put, so the net is
// the [anchor..anchor+1] range -- not a range that drifted up past the anchor.
var (window, vm) = await TestHarness.BootAsync(3);
var model = vm.AssemblyTreeModel;
var pane = await window.WaitForComponent<AssemblyListPane>();
var grid = await pane.WaitForComponent<DataGrid>();
grid.Focus();
Dispatcher.UIThread.RunJobs();
var hm = (global::Avalonia.Controls.DataGridHierarchical.IHierarchicalModel)grid.HierarchicalModel!;
// Expand the first assembly so there are plenty of rows and a real "middle".
var firstAsm = (SharpTreeNode)hm.Flattened[0].Item;
firstAsm.EnsureLazyChildren();
firstAsm.IsExpanded = true;
Dispatcher.UIThread.RunJobs();
await Waiters.WaitForAsync(() => hm.Flattened.Count >= 7);
var start = (SharpTreeNode)hm.Flattened[3].Item;
var below = (SharpTreeNode)hm.Flattened[4].Item;
model.SelectNode(start);
await Waiters.WaitForAsync(() => ReferenceEquals(model.SelectedItem, start));
for (int d = 0; d < 3; d++)
{
window.KeyPress(Key.Down, RawInputModifiers.Shift, PhysicalKey.ArrowDown, null);
Dispatcher.UIThread.RunJobs();
}
for (int u = 0; u < 2; u++)
{
window.KeyPress(Key.Up, RawInputModifiers.Shift, PhysicalKey.ArrowUp, null);
Dispatcher.UIThread.RunJobs();
}
model.SelectedItems.Should().BeEquivalentTo(new[] { start, below },
"net of +3/-2 from a fixed anchor is the anchor plus one row below it");
}
[AvaloniaTest]
public async Task Type_Ahead_Jumps_To_The_Node_Matching_The_Typed_Text()
{

69
ILSpy/Controls/TreeKeyboardController.cs

@ -18,6 +18,7 @@ @@ -18,6 +18,7 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Controls.DataGridHierarchical;
@ -54,6 +55,10 @@ namespace ILSpy.Controls @@ -54,6 +55,10 @@ namespace ILSpy.Controls
readonly DispatcherTimer searchResetTimer;
string searchBuffer = string.Empty;
// ProDataGrid internals reached reflectively to fix shift-range shrinking (see OnShiftRangeKey).
static readonly MethodInfo? setRowSelectionMethod =
typeof(DataGrid).GetMethod("SetRowSelection", BindingFlags.NonPublic | BindingFlags.Instance);
public TreeKeyboardController(DataGrid grid)
{
this.grid = grid ?? throw new ArgumentNullException(nameof(grid));
@ -61,6 +66,8 @@ namespace ILSpy.Controls @@ -61,6 +66,8 @@ namespace ILSpy.Controls
searchResetTimer.Tick += (_, _) => { searchResetTimer.Stop(); searchBuffer = string.Empty; };
grid.AddHandler(InputElement.KeyDownEvent, OnKeyDown, RoutingStrategies.Bubble);
grid.AddHandler(InputElement.TextInputEvent, OnTextInput, RoutingStrategies.Bubble);
// Runs even after the DataGrid marks the key handled, so we can repair its selection.
grid.AddHandler(InputElement.KeyDownEvent, OnShiftRangeKey, RoutingStrategies.Bubble, handledEventsToo: true);
}
// The model is the grid's DataContext (the tree's UserControl sets it). Resolved per
@ -174,6 +181,68 @@ namespace ILSpy.Controls @@ -174,6 +181,68 @@ namespace ILSpy.Controls
return null;
}
// Workaround for a ProDataGrid bug: a shift+nav key moves the grid's current row and anchor
// correctly, but SelectFromAnchorToCurrent only ADDS the [anchor..current] range and never
// deselects rows outside it -- so shrinking a shift-selection (Shift+Up after extending down)
// is a no-op and the dropped rows stay selected. After the grid has processed the key, prune
// the selection back to exactly the [anchor..current] range.
//
// The prune uses the internal SetRowSelection(slot, isSelected:false, setAnchorSlot:false),
// NOT SelectedItems.Remove: removing an item makes the grid re-derive its current row (which
// corrupts the anchor for the next key), whereas SetRowSelection deselects a slot in place
// and leaves current/anchor untouched.
void OnShiftRangeKey(object? sender, KeyEventArgs e)
{
if ((e.KeyModifiers & KeyModifiers.Shift) == 0
|| grid.SelectionMode != DataGridSelectionMode.Extended
|| setRowSelectionMethod is null
|| e.Key is not (Key.Up or Key.Down or Key.Home or Key.End or Key.PageUp or Key.PageDown))
return;
Dispatcher.UIThread.Post(PruneToShiftRange);
}
void PruneToShiftRange()
{
if (Model is not { } hm || setRowSelectionMethod is null)
return;
int anchor = ReadGridSlot("AnchorSlot");
int current = ReadGridSlot("CurrentSlot");
if (anchor < 0 || current < 0)
return;
int lo = Math.Min(anchor, current), hi = Math.Max(anchor, current);
var flattened = hm.Flattened;
if (hi >= flattened.Count)
return;
// Collect first (don't mutate the selection while iterating it), then deselect.
var slotsToDrop = new List<int>();
foreach (var item in grid.SelectedItems)
{
var node = item is HierarchicalNode hn ? hn.Item as SharpTreeNode : item as SharpTreeNode;
if (node is null)
continue;
int slot = IndexOf(flattened, node);
if (slot >= 0 && (slot < lo || slot > hi))
slotsToDrop.Add(slot);
}
foreach (int slot in slotsToDrop)
setRowSelectionMethod.Invoke(grid, new object[] { slot, false, false });
}
// Reads a DataGrid slot property/field by name; -1 if unavailable (defensive against a
// future ProDataGrid that renames or removes it -- the worst case is the pre-fix behaviour).
int ReadGridSlot(string name)
{
var type = grid.GetType();
var member = (object?)type.GetProperty(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
?? type.GetField(name, BindingFlags.NonPublic | BindingFlags.Instance);
object? value = member switch {
PropertyInfo pi => pi.GetValue(grid),
FieldInfo fi => fi.GetValue(grid),
_ => null,
};
return value is int slot ? slot : -1;
}
// Numpad-* recursive expand. Recurses only into children that opt in via
// SharpTreeNode.CanExpandRecursively (false for lazy-loading nodes), so it stays bounded.
static void ExpandRecursively(IHierarchicalModel hm, HierarchicalNode node)

Loading…
Cancel
Save