Browse Source

Keep the hover popup open while the user interacts with it

Selecting text inside the popup closed it: the selection drag captures
the pointer, so sweeping past the popup edge fired the content's
PointerExited with IsPointerOver already false. The close veto now
covers pointer-over, keyboard-focus-within (so a finished selection
survives for Ctrl+C, as the WPF tooltip did), and pointer capture held
inside the popup; only a document change force-closes.

Assisted-by: Claude:claude-fable-5:Claude Code
pull/3776/head
Siegfried Pammer 4 weeks ago committed by Siegfried Pammer
parent
commit
5f2e448a09
  1. 52
      ILSpy.Tests/Editor/DocumentationLinkTests.cs
  2. 22
      ILSpy/TextView/DecompilerTextView.axaml.cs

52
ILSpy.Tests/Editor/DocumentationLinkTests.cs

@ -104,6 +104,58 @@ public class DocumentationLinkTests @@ -104,6 +104,58 @@ public class DocumentationLinkTests
"the hosting popup closes itself when a link is followed");
}
[AvaloniaTest]
public async Task Pressing_Inside_The_Popup_To_Select_Does_Not_Close_It()
{
var (window, vm) = await TestHarness.BootAsync();
var coreLibName = typeof(object).Assembly.GetName().Name!;
var stringNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(coreLibName, "System", "System.String");
vm.AssemblyTreeModel.SelectNode(stringNode);
await vm.DockWorkspace.WaitForDecompiledTextAsync();
window.UpdateLayout();
var view = window.GetVisualDescendants().OfType<DecompilerTextView>().First();
var renderer = new DocumentationRenderer(
new CSharpAmbience(),
new FontFamily("Consolas, Menlo, Monospace"),
12);
renderer.AddXmlDocumentation(
"""<summary>Some text the user may want to select and copy.</summary>""",
declaringEntity: null,
resolver: _ => null);
var content = renderer.CreateView();
view.OpenRichPopup(content);
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
Avalonia.Threading.Dispatcher.UIThread.RunJobs();
window.UpdateLayout();
var popup = view.FindControl<Avalonia.Controls.Primitives.Popup>("RichHoverPopup")!;
popup.IsOpen.Should().BeTrue("setup precondition — the popup must be open before the press");
var paragraph = content.GetVisualDescendants().OfType<SelectableTextBlock>().First();
var inside = paragraph.TranslatePoint(
new Point(10, paragraph.Bounds.Height / 2), window)!.Value;
window.MouseDown(inside, MouseButton.Left);
popup.IsOpen.Should().BeTrue("holding LMB inside the popup starts a text selection, not a dismiss");
window.MouseMove(inside + new Point(25, 0));
popup.IsOpen.Should().BeTrue("dragging a selection inside the popup must not close it");
// Selecting to the end of a line routinely sweeps the pointer past the popup edge
// while the button is still held — the popup must survive that.
var farOutside = inside + new Point(600, 80);
window.MouseMove(farOutside);
popup.IsOpen.Should().BeTrue("a selection drag sweeping past the popup edge must not close it");
// Finish the selection back over the popup and release: the pointer is still over the
// popup, so it stays open for the copy. (Releasing far outside the popup is left to
// normal leave-to-close behaviour, which differs by platform focus model.)
window.MouseMove(inside + new Point(40, 0));
window.MouseUp(inside + new Point(40, 0), MouseButton.Left);
Avalonia.Threading.Dispatcher.UIThread.RunJobs();
popup.IsOpen.Should().BeTrue("the selection just finished over the popup — it stays for the copy");
}
[AvaloniaTest]
public void Unresolvable_Cref_Falls_Back_To_Plain_Text()
{

22
ILSpy/TextView/DecompilerTextView.axaml.cs

@ -850,10 +850,14 @@ namespace ILSpy.TextView @@ -850,10 +850,14 @@ namespace ILSpy.TextView
{
if (richPopup.IsOpen)
{
// TODO: refuse to close while the popup has keyboard focus (the user is
// reading / clicking links inside it). Not tracked yet — for now the popup
// is always replaceable.
_ = mouseClick;
// While the pointer is over the popup or keyboard focus is inside it (the
// user is reading, selecting text to copy, or reaching for a link), only a
// forced close (document change) may take it down.
if (!mouseClick && richPopup.Child is { } child
&& (child.IsPointerOver || child.IsKeyboardFocusWithin))
{
return false;
}
CloseRichPopup();
return true;
}
@ -907,7 +911,7 @@ namespace ILSpy.TextView @@ -907,7 +911,7 @@ namespace ILSpy.TextView
ToolTip.SetIsOpen(Editor.TextArea.TextView, true);
}
void OpenRichPopup(Control content)
internal void OpenRichPopup(Control content)
{
richPopup.Child = content;
// While the pointer is over the popup, the editor no longer receives the moves
@ -920,6 +924,14 @@ namespace ILSpy.TextView @@ -920,6 +924,14 @@ namespace ILSpy.TextView
void OnRichPopupContentPointerExited(object? sender, PointerEventArgs e)
{
// A selection drag that sweeps past the popup edge keeps the pointer captured
// inside the popup — that's a wide swipe, not a leave. The over/focus vetoes in
// TryCloseExistingPopup don't see the capture, so check it here.
if (e.Pointer.Captured is Visual captured && richPopup.Child is { } child
&& (ReferenceEquals(captured, child) || captured.GetVisualAncestors().Contains(child)))
{
return;
}
TryCloseExistingPopup(mouseClick: false);
}

Loading…
Cancel
Save