Browse Source

Stop the tree auto-scrolling to the selection on in-tree gestures

SharpTreeView is a ListBox with AutoScrollToSelectedItem left at its default
(true), so it chased the selected row whenever its index shifted: expanding an
unrelated node pushed the selection off-screen and the ListBox yanked the
viewport back to it, fighting the expand's own reveal -- the "weird scrolling".

The rule is that a user mutating a control directly should not have the app
mutate the view too; only navigation from a *different* control (search
results, code/metadata links, analyzer nodes, Back/forward) may sync the tree.
The app already does that sync explicitly through the model
(TreeSelectionBinder -> CenterNodeInView), so the ListBox's own auto-scroll is
redundant and wrong for in-tree actions. Disable it.

Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3755/head
Siegfried Pammer 4 weeks ago
parent
commit
2af0289ce4
  1. 56
      ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs
  2. 6
      ILSpy/Controls/TreeView/SharpTreeView.cs

56
ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs

@ -752,6 +752,62 @@ public class AssemblyTreeTests @@ -752,6 +752,62 @@ public class AssemblyTreeTests
"selecting an already-visible row via the model (e.g. Decompile to new tab) must not move the viewport");
}
[AvaloniaTest]
public async Task Expanding_A_Node_Does_Not_Scroll_The_Selection_Back_Into_View()
{
// Rule: when the user mutates the tree directly -- here, expanding an unrelated node -- the
// app must not chase the selection; the viewport follows the user's action, not the selected
// row. Regression: the ListBox's AutoScrollToSelectedItem yanked the (now off-screen)
// selection back into view on every expand, fighting the expand's own reveal ("weird
// scrolling"). Cross-control navigation still reveals (that path goes through the model).
var (window, vm) = await TestHarness.BootAsync(3);
// Tall tree: fully expand System.Linq so the list is taller than the viewport.
var enumerable = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
enumerable.Expand();
var ns = (NamespaceTreeNode)enumerable.Parent!;
ns.Expand();
((AssemblyTreeNode)ns.Parent!).IsExpanded = true;
foreach (var t in ns.Children.OfType<TypeTreeNode>())
t.Expand();
var pane = await window.WaitForComponent<AssemblyListPane>();
var grid = await pane.WaitForComponent<global::ILSpy.Controls.TreeView.SharpTreeView>();
var scrollViewer = await grid.WaitForComponent<ScrollViewer>();
// Select + reveal the type, then let it settle on screen.
vm.AssemblyTreeModel.SelectNode(enumerable);
await Waiters.WaitForAsync(() => ReferenceEquals(vm.AssemblyTreeModel.SelectedItem, enumerable));
for (int i = 0; i < 8; i++)
{
Dispatcher.UIThread.RunJobs();
await Task.Delay(25);
}
grid.UpdateLayout();
(scrollViewer.Extent.Height - scrollViewer.Viewport.Height).Should().BeGreaterThan(50,
"the tree must be taller than the viewport for this test to be meaningful");
grid.IsNodeFullyVisible(enumerable).Should().BeTrue("the selected type is revealed before the expand");
// Act: the user expands an unrelated, earlier node (CoreLib, at the top), inserting many
// rows above the selection and pushing it off-screen.
var coreLib = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(typeof(object).Assembly.GetName().Name!);
coreLib.IsExpanded = true;
for (int i = 0; i < 8; i++)
{
Dispatcher.UIThread.RunJobs();
await Task.Delay(25);
}
grid.UpdateLayout();
// Assert: the app did not chase the selection. The expand reveals the opened node's children;
// the off-screen selection stays off-screen.
grid.IsNodeFullyVisible(enumerable).Should().BeFalse(
"expanding a node the user opened must not auto-scroll the off-screen selection back into view");
}
[AvaloniaTest]
public async Task Save_Code_Command_Dispatches_Single_Selected_Node_Save_Override()
{

6
ILSpy/Controls/TreeView/SharpTreeView.cs

@ -75,6 +75,12 @@ namespace ILSpy.Controls.TreeView @@ -75,6 +75,12 @@ namespace ILSpy.Controls.TreeView
// Take keyboard focus directly so gestures like Ctrl+A work the moment the user tabs
// or clicks into the pane, before any row becomes the current item.
Focusable = true;
// The app reveals the selection explicitly on cross-control navigation (search results,
// code/metadata links, analyzer nodes) via TreeSelectionBinder -> CenterNodeInView. The
// ListBox's own AutoScrollToSelectedItem additionally chases the selected row whenever its
// index shifts -- e.g. when the user expands an unrelated node -- yanking the viewport away
// from what the user is doing. Disable it so in-tree gestures never move the view.
AutoScrollToSelectedItem = false;
SelectionChanged += OnSelectionChanged;
DoubleTapped += OnDoubleTapped;
// Drag-drop is handled generically here and delegated to SharpTreeNode.CanDrop/Drop.

Loading…
Cancel
Save