Browse Source

Assembly tree Ctrl+LMB stops conflicting with multi-selection

User-reported: Ctrl+LMB on an assembly-tree row both toggled the row in
the multi-selection AND opened a new decompiler tab. ProDataGrid's
Extended-selection mode treats Ctrl+LMB as a toggle, and
OnTreeGridPointerPressed also treated it as "open in new tab", so every
Ctrl+click fired both effects regardless of the e.Handled=true on our
handler — selection lives in the DataGrid template's own pre-bubble
pipeline.

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
746f87598c
  1. 53
      ILSpy.Tests/Input/HeadlessMmbPointerTests.cs
  2. 18
      ILSpy/AssemblyTree/AssemblyListPane.axaml.cs

53
ILSpy.Tests/Input/HeadlessMmbPointerTests.cs

@ -92,15 +92,58 @@ public class HeadlessMmbPointerTests @@ -92,15 +92,58 @@ public class HeadlessMmbPointerTests
(documents.VisibleDockables?.Count ?? 0).Should().Be(before + 1);
}
[AvaloniaTest]
public async Task Ctrl_LMB_On_An_Assembly_Tree_Row_Does_Not_Open_A_New_Tab()
{
// Ctrl+LMB previously did double duty: ProDataGrid's Extended-selection mode treats
// it as "toggle this row in the multi-selection", and our OnTreeGridPointerPressed
// also treated it as "open in new tab". Both fired on a single click, so users
// trying to extend a multi-selection ended up spawning unwanted tabs. The intended
// gesture for "open in new tab" is MMB (matches WPF's DecompileInNewViewCommand
// InputGestureText). Pin the corrected behaviour: Ctrl+LMB must NOT open a tab —
// any future regression that re-adds the branch fails this test.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var pane = await window.WaitForComponent<AssemblyListPane>();
var grid = await pane.WaitForComponent<DataGrid>();
await Waiters.WaitForAsync(() => grid.GetVisualDescendants().OfType<DataGridRow>()
.Any(r => r.DataContext is HierarchicalNode { Item: ILSpyTreeNode }));
var row = grid.GetVisualDescendants().OfType<DataGridRow>()
.First(r => r.DataContext is HierarchicalNode { Item: ILSpyTreeNode });
var documents = ((ILSpyDockFactory)vm.DockWorkspace.Factory).Documents!;
int before = documents.VisibleDockables?.Count ?? 0;
var topLevel = TopLevel.GetTopLevel(row)!;
var rowBounds = row.Bounds;
var centreInRow = new Point(rowBounds.Width / 2, rowBounds.Height / 2);
var centreInTopLevel = row.TranslatePoint(centreInRow, topLevel)
?? throw new System.InvalidOperationException("Row not in TopLevel visual tree");
topLevel.MouseDown(centreInTopLevel, MouseButton.Left, RawInputModifiers.Control);
topLevel.MouseUp(centreInTopLevel, MouseButton.Left, RawInputModifiers.Control);
// Give the (former) "open new tab" path a beat to land if it were going to.
await Task.Delay(250);
global::Avalonia.Threading.Dispatcher.UIThread.RunJobs();
(documents.VisibleDockables?.Count ?? 0).Should().Be(before,
"Ctrl+LMB must be reserved for ProDataGrid's multi-selection toggle — only MMB opens a new tab");
}
[AvaloniaTest]
public async Task LMB_Double_Click_On_An_Internal_Tree_Row_Expands_Without_Opening_A_New_Tab()
{
// LMB double-click is WPF's expand/collapse gesture. The OnTreeGridDoubleTapped handler
// is supposed to be the only path that fires; an earlier patch had OnTreeGridPointerPressed
// also treating double-click as a third "open in new tab" gesture (alongside MMB +
// Ctrl+LMB), which produced both effects at once. Pin the behaviour so it doesn't drift
// back: double-click on an internal (non-leaf) row must change neither the tab count
// nor the dock topology.
// also treating double-click as a "open in new tab" gesture, which produced both effects
// at once. Pin the behaviour so it doesn't drift back: double-click on an internal
// (non-leaf) row must change neither the tab count nor the dock topology.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
@ -140,6 +183,6 @@ public class HeadlessMmbPointerTests @@ -140,6 +183,6 @@ public class HeadlessMmbPointerTests
global::Avalonia.Threading.Dispatcher.UIThread.RunJobs();
(documents.VisibleDockables?.Count ?? 0).Should().Be(before,
"LMB double-click must not open a new tab — only MMB and Ctrl+LMB do");
"LMB double-click must not open a new tab — only MMB does");
}
}

18
ILSpy/AssemblyTree/AssemblyListPane.axaml.cs

@ -393,20 +393,16 @@ namespace ILSpy.AssemblyTree @@ -393,20 +393,16 @@ namespace ILSpy.AssemblyTree
void OnTreeGridPointerPressed(object? sender, PointerPressedEventArgs e)
{
// Two gestures map to "open the row's node in a new tab":
// • MMB single-click — primary, WPF DecompileInNewViewCommand's InputGestureText.
// • Ctrl + LMB single-click — modern editor convention.
// LMB double-click is reserved for expand/collapse — handled by OnTreeGridDoubleTapped.
// MMB doesn't move selection on its own, so we hit-test the row from e.Source
// in every case rather than reading SelectedItem.
// MMB single-click is the only "open the row's node in a new tab" pointer gesture —
// matches WPF's DecompileInNewViewCommand InputGestureText. Ctrl+LMB is left to
// ProDataGrid's Extended-selection mode so multi-selection by toggle isn't ambushed
// by an unwanted new tab. LMB double-click is reserved for expand/collapse,
// handled by OnTreeGridDoubleTapped. MMB doesn't move selection on its own, so we
// hit-test the row from e.Source rather than reading SelectedItem.
if (e.Source is not Visual hit)
return;
var point = e.GetCurrentPoint(hit).Properties;
bool isMmb = point.IsMiddleButtonPressed;
bool isCtrlClick = point.IsLeftButtonPressed
&& (e.KeyModifiers & KeyModifiers.Control) == KeyModifiers.Control
&& e.ClickCount == 1;
if (!isMmb && !isCtrlClick)
if (!point.IsMiddleButtonPressed)
return;
var visual = hit;
while (visual != null && visual.DataContext is not HierarchicalNode)

Loading…
Cancel
Save